分布式事務理論:分布式事務
分布式事務解決方案之TCC
TCC是一種比較成熟的分布式事務解決方案,可用于解決跨庫操作的數據一致性問題;
TCC是服務化的兩階段編程模型,其Try、Confirm、Cancel 3個方法均由業務編碼實現;其中Try操作作為一階段,負責資源的檢查和預留,Confirm操作作為二階段提交操作,執行真正的業務,Cancel是預留資源的取消;
如下圖所示,業務實現TCC服務之后,該TCC服務將作為分布式事務的其中一個資源,參與到整個分布式事務中;事務管理器分2階段協調TCC服務,在第一階段調用所有TCC服務的Try方法,在第二階段執行所有TCC服務的Confirm或者Cancel方法;
什么是TCC事務
TCC是Try、Confirm、Cancel三個詞語的縮寫,TCC要求每個分支事務實現三個操作:預處理Try、確認Confirm、撤銷Cancel。
Try操作做業務檢查及資源預留,Confirm做業務確認操作,Cancel實現一個與Try相反的操作即回滾操作。TM首先發起所有的分支事務的try操作,任何一個分支事務的try操作執行失敗,TM將會發起所有分支事務的Cancel操作,若try操作全部成功,TM將會發起所有分支事務的Confirm操作,其中Confirm/Cancel操作若執行失敗,TM會進行重試。
分支事務失敗的情況:
TCC分為三個階段:
1、Try 階段是做業務檢查(一致性)及資源預留(隔離),此階段僅是一個初步操作,它和后續的Confirm 一起才能真正構成一個完整的業務邏輯。
2、Confirm 階段是做確認提交,Try階段所有分支事務執行成功后開始執行 Confirm。通常情況下,采用TCC則認為 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。若Confirm階段真的出錯了,需引入重試機制或人工處理。
3、Cancel 階段是在業務執行錯誤需要回滾的狀態下執行分支事務的業務取消,預留資源釋放。通常情況下,采用TCC則認為Cancel階段也是一定成功的。若Cancel階段真的出錯了,需引入重試機制或人工處理。
- TM事務管理器
TM事務管理器可以實現為獨立的服務,也可以讓全局事務發起方充當TM的角色,TM獨立出來是為了成為公用組件,是為了考慮系統結構和軟件復用。
- TM事務管理器
TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條,用來記錄事務上下文,追蹤和記錄狀態,由于Confirm 和cancel失敗需進行重試,因此需要實現為冪等,冪等性是指同一個操作無論請求多少次,其結果都相同。
TCC 解決方案
目前市面上的TCC框架眾多比如下面這幾種:
Seata也支持TCC,但Seata的TCC模式對Spring Cloud并沒有提供支持。我們的目標是理解TCC原理以及事務協調運作的過程,因此更傾向于輕量級易于理解的框架。
Hmily是一個高性能分布式事務TCC開源框架。基于Java語言來開發(JDK1.8),支持Dubbo,Spring Cloud等RPC框架進行分布式事務。它目前支持以下特性 :
- 支持嵌套事務(Nested transaction support).
- 采用disruptor框架進行事務日志的異步讀寫,與RPC框架的性能毫無差別。
- 支持SpringBoot-starter 項目啟動,使用簡單。
- RPC框架支持 : dubbo,motan,springcloud。
- 本地事務存儲支持 : redis,mongodb,zookeeper,file,mysql。
- 事務日志序列化支持:java,hessian,kryo,protostuff。
- 采用Aspect AOP 切面思想與Spring無縫集成,天然支持集群。
- RPC事務恢復,超時異常恢復等。
Hmily利用AOP對參與分布式事務的本地方法與遠程方法進行攔截處理,通過多方攔截,事務參與者能透明的調用到另一方的Try、Confirm、Cancel方法;傳遞事務上下文;并記錄事務日志,酌情進行補償,重試等。
Hmily不需要事務協調服務,但需要提供一個數據庫(mysql/mongodb/zookeeper/redis/file)來進行日志存
儲。
Hmily實現的TCC服務與普通的服務一樣,只需要暴露一個接口,也就是它的Try業務。Confirm/Cancel業務邏輯,只是因為全局事務提交/回滾的需要才提供的,因此Confirm/Cancel業務只需要被Hmily TCC事務框架發現即可,不需要被調用它的其他業務服務所感知。
官網介紹:https://dromara.org/website/zh-cn/docs/hmily/index.html
GitHub:https://github.com/yu199195/hmily
Gitee:https://gitee.com/shuaiqiyu/hmily
用戶在實現TCC服務時,有以下注意事項
1、業務操作分兩階段完成:
如下圖所示,接入TCC前,業務操作只需要一步就能完成,但是在接入TCC之后,需要考慮如何將其分成2階段完成,把資源的檢查和預留放在一階段的Try操作中進行,把真正的業務操作的執行放在二階段的Confirm操作中進行;
TCC服務要保證第一階段Try操作成功之后,二階段Confirm操作一定能成功;
2、允許空回滾;
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因為丟包而導致的網絡超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;
TCC服務在未收到Try請求的情況下收到Cancel請求,這種場景被稱為空回滾;TCC服務在實現時應當允許空回滾的執行;
在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然后直接返回成功。
出現原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄為失敗,這個時候其實是沒有執行Try階段,當故障恢復后,分布式事務進行回滾則會調用二階段的Cancel方法,從而形成空回滾。
解決思路是關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。前面已經說過TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條。再額外增加一張分支事務記錄表,其中有全局事務 ID 和分支事務 ID,第一階段 Try 方法里會插入一條記錄,表示一階段執行了。Cancel 接口里讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存在,則是空回滾。
3、防懸掛控制;
如下圖所示,事務協調器在調用TCC服務的一階段Try操作時,可能會出現因網絡擁堵而導致的超時,此時事務協調器會觸發二階段回滾,調用TCC服務的Cancel操作;在此之后,擁堵在網絡上的一階段Try數據包被TCC服務收到,出現了二階段Cancel請求比一階段Try請求先執行的情況;
用戶在實現TCC服務時,應當允許空回滾,但是要拒絕執行空回滾之后到來的一階段Try請求;
懸掛就是對于一個分布式事務,其二階段 Cancel 接口比 Try 接口先執行。出現原因是在 RPC 調用分支事務try時,先注冊分支事務,再執行RPC調用,如果此時 RPC 調用的網絡發生擁堵,通常 RPC 調用是有超時時間的,RPC 超時以后,TM就會通知RM回滾該分布式事務,可能回滾完成后,RPC 請求才到達參與者真正執行,而一個 Try 方法預留的業務資源,只有該分布式事務才能使用,該分布式事務第一階段預留的業務資源就再也沒有人能夠處理了,對于這種情況,我們就稱為懸掛,即業務資源預留后沒法繼續處理。
解決思路是如果二階段執行完成,那一階段就不能再繼續執行。在執行一階段事務時判斷在該全局事務下,“分支事務記錄”表中是否已經有二階段事務記錄,如果有則不執行Try。
4、冪等控制:
無論是網絡數據包重傳,還是異常事務的補償執行,都會導致TCC服務的Try、Confirm或者Cancel操作被重復執行;用戶在實現TCC服務時,需要考慮冪等控制,即Try、Confirm、Cancel 執行次和執行多次的業務結果是一樣的;
通過前面介紹已經了解到,為了保證TCC二階段提交重試機制不會引發數據不一致,要求 TCC 的二階段 Try、Confirm 和 Cancel 接口保證冪等,這樣不會重復使用或者釋放資源。如果冪等控制沒有做好,很有可能導致數據不一致等嚴重問題。
解決思路在上述“分支事務記錄”中增加執行狀態,每次執行前都查詢該狀態。
5、業務數據可見性控制;
TCC服務的一階段Try操作會做資源的預留,在二階段操作執行之前,如果其他事務需要讀取被預留的資源數據,那么處于中間狀態的業務數據該如何向用戶展示,需要業務在實現時考慮清楚;通常的設計原則是“寧可不展示、少展示,也不多展示、錯展示”;
6、業務數據并發訪問控制;
TCC服務的一階段Try操作預留資源之后,在二階段操作執行之前,預留的資源都不會被釋放;如果此時其他分布式事務修改這些業務資源,會出現分布式事務的并發問題;
用戶在實現TCC服務時,需要考慮業務數據的并發控制,盡量將邏輯鎖粒度降到最低,以最大限度的提高分布式事務的并發性;
舉例,場景為 A 轉賬 30 元給 B,A和B賬戶在不同的服務。
方案1:
賬戶A
try {
檢查余額是否夠30元
扣減30元
}
confirm {
空
}
cancel {
增加30元
}
賬戶B
try {
增加30元
}
confirm {
空
}
cancel {
減少30元
}
方案1說明:
1、賬戶A,這里的余額就是所謂的業務資源,按照前面提到的原則,在第一階段需要檢查并預留業務資源,因此,我們在扣錢 TCC 資源的 Try 接口里先檢查 A 賬戶余額是否足夠,如果足夠則扣除 30 元。 Confirm 接口表示正式提交,由于業務資源已經在 Try 接口里扣除掉了,那么在第二階段的 Confirm 接口里可以什么都不用做。Cancel接口的執行表示整個事務回滾,賬戶A回滾則需要把 Try 接口里扣除掉的 30 元還給賬戶。
2、賬號B,在第一階段 Try 接口里實現給賬戶B加錢,Cancel 接口的執行表示整個事務回滾,賬戶B回滾則需要把Try 接口里加的 30 元再減去。
方案1的問題分析:
- 1、如果賬戶A的try沒有執行在cancel則就多加了30元。
- 2、由于try,cancel、confirm都是由單獨的線程去調用,且會出現重復調用,所以都需要實現冪等。
- 3、賬號B在try中增加30元,當try執行完成后可能會其它線程給消費了。
- 4、如果賬戶B的try沒有執行在cancel則就多減了30元。
問題解決:
- 1、賬戶A的cancel方法需要判斷try方法是否執行,正常執行try后方可執行cancel。
- 2、try,cancel、confirm方法實現冪等。
- 3、賬號B在try方法中不允許更新賬戶金額,在confirm中更新賬戶金額。
- 4、賬戶B的cancel方法需要判斷try方法是否執行,正常執行try后方可執行cancel。
優化方案:
賬戶A
try {
try冪等校驗
try懸掛處理
檢查余額是否夠30元
扣減30元
}
confirm {
空
}
cancel {
cancel冪等校驗
cancel空回滾處理
增加可用余額30元
}
賬戶B
try {
空
}
confirm {
confirm冪等校驗
正式增加30元
}
cancel {
空
}
Hmily實現TCC事務
本實例通過Hmily實現TCC分布式事務,模擬兩個賬戶的轉賬交易過程。
兩個賬戶分別在不同的微服務,要么一起成功,要么一起失敗,必須是一個整體性的事務。
微服務版本:
Nacos-Server:1.3.1
SpringBoot:2.2.10.RELEASE
spring-cloud-dependencies:Hoxton.SR8
spring-cloud-alibaba-dependencies:2.2.1.RELEASE
hmily-springcloud:2.0.6-RELEASE
引入maven
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.10.RELEASE</version>
<relativePath/>
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<exclusion>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.alibaba.nacos</groupId>
<artifactId>nacos-client</artifactId>
<version>1.3.1</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
<dependency>
<groupId>org.dromara</groupId>
<artifactId>hmily-springcloud</artifactId>
<version>2.0.6-RELEASE</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.2.1</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper-spring-boot-starter</artifactId>
<version>2.1.5</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.18</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.12</version>
<scope>provided</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Hoxton.SR8</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>2.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.mybatis.generator</groupId>
<artifactId>mybatis-generator-maven-plugin</artifactId>
<version>1.3.6</version>
<configuration>
<configurationFile>
${basedir}/src/main/resources/generator/generatorConfig.xml
</configurationFile>
<overwrite>true</overwrite>
<verbose>true</verbose>
</configuration>
<dependencies>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.41</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
</dependencies>
</plugin>
</plugins>
</build>
Hmily的application.properties配置
spring.cloud.nacos.discovery.server-addr=127.0.0.1:8848
org.dromara.hmily.serializer=kryo
org.dromara.hmily.recoverDelayTime=128
org.dromara.hmily.retryMax=30
org.dromara.hmily.scheduledDelay=128
org.dromara.hmily.scheduledThreadMax=10
org.dromara.hmily.repositorySupport=db
# 服務調用方為true,其余為false
org.dromara.hmily.started=true
org.dromara.hmily.hmilyDbConfig.driverClassName=com.mysql.jdbc.Driver
org.dromara.hmily.hmilyDbConfig.url=jdbc:mysql://localhost:3306/hmily?useUnicode=true&useAffectedRows=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=UTC
org.dromara.hmily.hmilyDbConfig.username=root
org.dromara.hmily.hmilyDbConfig.password=yibo
新增配置類接收application.properties中的Hmily配置信息,創建HmilyTransactionBootstrap,并增加@EnableAspectJAutoProxy(proxyTargetClass=true)
注解
@Configuration
@EnableAspectJAutoProxy(proxyTargetClass=true)
public class HmilyConfiguration {
@Autowired
private Environment env;
@Bean
public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
hmilyTransactionBootstrap.setRecoverDelayTime(Integer.parseInt(env.getProperty("org.dromara.hmily.recoverDelayTime")));
hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledThreadMax")));
hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
return hmilyTransactionBootstrap;
}
}
入口類增加org.dromara.hmily
的掃描項
@ComponentScan({"com.yibo.hmily","org.dromara.hmily"})
@MapperScan("com.yibo.hmily.mapper")//掃描mybatis的指定包下的接口
@EnableFeignClients
@EnableDiscoveryClient
@SpringBootApplication
public class Hmily1Application {
public static void main(String[] args) {
SpringApplication.run(Hmily1Application.class,args);
}
}
hmily-bank1實現try和cancel方法,如下:
try {
try冪等校驗
try懸掛處理
檢查余額是夠扣減金額
扣減金額
}
confirm {
空
}
cancel {
cancel冪等校驗
cancel空回滾處理
增加可用余額
}
Controller
@RestController
@RequestMapping("/bank1")
public class Bank1Controller {
@Autowired
private AccountService accountService;
@GetMapping("/transfer/{amount}")
public String transfer(@PathVariable("amount") Long amount){
accountService.updateAccountBalance("1",amount);
return "success";
}
}
service服務的try和cancel方法
@Service
@Slf4j
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Autowired
private Bank2Client bank2Client;
/**
* 賬戶扣款,就是tcc的try方法
* try冪等校驗
* try懸掛處理
* 檢查余額是夠扣減金額
* 扣減金額
* @param accountNo
* @param amount
*/
@Transactional
//只要標記@Hmily就是try方法,在注解中指定confirm、cancel兩個方法的名字
@Hmily(confirmMethod="commit",cancelMethod="rollback")
public void updateAccountBalance(String accountNo, Long amount) {
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank1 try begin 開始執行...xid:{}",transId);
//冪等判斷 判斷local_try_log表中是否有try日志記錄,如果有則不再執行
if(accountInfoMapper.isExistTry(transId) > 0){
log.info("bank1 try 已經執行,無需重復執行,xid:{}",transId);
return ;
}
//try懸掛處理,如果cancel、confirm有一個已經執行了,try不再執行
if(accountInfoMapper.isExistConfirm(transId) > 0 || accountInfoMapper.isExistCancel(transId) > 0){
log.info("bank1 try懸掛處理 cancel或confirm已經執行,不允許執行try,xid:{}",transId);
return ;
}
//扣減金額
if(accountInfoMapper.subtractAccountBalance(accountNo, amount)<=0){
//扣減失敗
throw new RuntimeException("bank1 try 扣減金額失敗,xid:{}"+transId);
}
//插入try執行記錄,用于冪等判斷
accountInfoMapper.addTry(transId);
//遠程調用轉賬
if(!bank2Client.transfer(amount)){
throw new RuntimeException("bank1 遠程調用李四微服務失敗,xid:{}"+transId);
}
if(amount == 2){
throw new RuntimeException("人為制造異常,xid:{}"+transId);
}
log.info("bank1 try end 結束執行...xid:{}",transId);
}
//confirm方法
@Transactional
public void commit(String accountNo, Double amount){
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank1 confirm begin 開始執行...xid:{},accountNo:{},amount:{}",transId,accountNo,amount);
}
/** cancel方法
* cancel冪等校驗
* cancel空回滾處理
* 增加可用余額
* @param accountNo
* @param amount
*/
@Transactional
public void rollback(String accountNo, Long amount){
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank1 cancel begin 開始執行...xid:{}",transId);
// cancel冪等校驗
if(accountInfoMapper.isExistCancel(transId) > 0){
log.info("bank1 cancel 已經執行,無需重復執行,xid:{}",transId);
return ;
}
//cancel空回滾處理,如果try沒有執行,cancel不允許執行
if(accountInfoMapper.isExistTry(transId)<=0){
log.info("bank1 空回滾處理,try沒有執行,不允許cancel執行,xid:{}",transId);
return ;
}
//增加可用余額
accountInfoMapper.addAccountBalance(accountNo,amount);
//插入一條cancel的執行記錄
accountInfoMapper.addCancel(transId);
log.info("bank1 cancel end 結束執行...xid:{}",transId);
}
}
feignClient
@FeignClient(value="hmily-bank2")
public interface Bank2Client {
//遠程調用微服務
@GetMapping("/bank2/transfer/{amount}")
@Hmily
public boolean transfer(@PathVariable("amount") Long amount);
}
mapper
public interface AccountInfoMapper extends Mapper<AccountInfo> {
int subtractAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);
int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);
/**
* 增加某分支事務try執行記錄
* @param localTradeNo 本地事務編號
* @return
*/
int addTry(@Param("txNo") String localTradeNo);
/**
* 增加某分支事務Confirm執行記錄
* @param localTradeNo
* @return
*/
int addConfirm(@Param("txNo") String localTradeNo);
/**
* 增加某分支事務Cancel執行記錄
* @param localTradeNo
* @return
*/
int addCancel(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務try是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistTry(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務confirm是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistConfirm(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務cancel是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistCancel(@Param("txNo") String localTradeNo);
}
mapper.xml
<update id="subtractAccountBalance">
update account_info set account_balance=account_balance - #{amount}
where account_balance>=#{amount} and account_no=#{accountNo}
</update>
<update id="addAccountBalance">
update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo}
</update>
<insert id="addTry">
insert into local_try_log values(#{txNo},now())
</insert>
<insert id="addConfirm">
insert into local_confirm_log values(#{txNo},now())
</insert>
<insert id="addCancel">
insert into local_cancel_log values(#{txNo},now())
</insert>
<select id="isExistTry" resultType="java.lang.Integer">
select count(1) from local_try_log where tx_no = #{txNo}
</select>
<select id="isExistConfirm" resultType="java.lang.Integer">
select count(1) from local_confirm_log where tx_no = #{txNo}
</select>
<select id="isExistCancel" resultType="java.lang.Integer">
select count(1) from local_cancel_log where tx_no = #{txNo}
</select>
hmily-bank2實現try、confirm、cancel功能,如下:
try {
空
}
confirm {
confirm冪等校驗
正式增加金額
}
cancel {
空
}
Controller
@RestController
@RequestMapping("/bank2")
public class Bank2Controller {
@Autowired
private AccountInfoService accountInfoService;
//張三轉賬
@GetMapping("/transfer/{amount}")
public boolean transfer(@PathVariable("amount") Long amount){
accountInfoService.updateAccountBalance("2",amount);
return true;
}
}
Service實現confirm方法
@Service
@Slf4j
public class AccountInfoService {
@Autowired
private AccountInfoMapper accountInfoMapper;
@Hmily(confirmMethod="confirmMethod", cancelMethod="cancelMethod")
public void updateAccountBalance(String accountNo, Long amount) {
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank2 try begin 開始執行...xid:{}",transId);
}
/**
* confirm方法
* confirm冪等校驗
* 正式增加金額
* @param accountNo
* @param amount
*/
@Transactional
public void confirmMethod(String accountNo, Long amount){
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank2 confirm begin 開始執行...xid:{}",transId);
if(accountInfoMapper.isExistConfirm(transId)>0){
log.info("bank2 confirm 已經執行,無需重復執行...xid:{}",transId);
return ;
}
//增加金額
accountInfoMapper.addAccountBalance(accountNo,amount);
//增加一條confirm日志,用于冪等
accountInfoMapper.addConfirm(transId);
log.info("bank2 confirm end 結束執行...xid:{}",transId);
}
/**
* @param accountNo
* @param amount
*/
public void cancelMethod(String accountNo, Long amount){
//獲取全局事務id
String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
log.info("bank2 cancel begin 開始執行...xid:{}",transId);
}
}
Mapper
public interface AccountInfoMapper extends Mapper<AccountInfo> {
int addAccountBalance(@Param("accountNo") String accountNo, @Param("amount") Long amount);
/**
* 增加某分支事務try執行記錄
* @param localTradeNo 本地事務編號
* @return
*/
int addTry(@Param("txNo") String localTradeNo);
/**
* 增加某分支事務Confirm執行記錄
* @param localTradeNo
* @return
*/
int addConfirm(@Param("txNo") String localTradeNo);
/**
* 增加某分支事務Cancel執行記錄
* @param localTradeNo
* @return
*/
int addCancel(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務try是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistTry(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務confirm是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistConfirm(@Param("txNo") String localTradeNo);
/**
* 查詢分支事務cancel是否已執行
* @param localTradeNo 本地事務編號
* @return
*/
int isExistCancel(@Param("txNo") String localTradeNo);
}
Mapper.xml
<update id="addAccountBalance">
update account_info set account_balance=account_balance + #{amount} where account_no=#{accountNo}
</update>
<insert id="addTry">
insert into local_try_log values(#{txNo},now())
</insert>
<insert id="addConfirm">
insert into local_confirm_log values(#{txNo},now())
</insert>
<insert id="addCancel">
iinsert into local_cancel_log values(#{txNo},now())
</insert>
<select id="isExistTry" resultType="java.lang.Integer">
select count(1) from local_try_log where tx_no = #{txNo}
</select>
<select id="isExistConfirm" resultType="java.lang.Integer">
select count(1) from local_confirm_log where tx_no = #{txNo}
</select>
<select id="isExistCancel" resultType="java.lang.Integer">
select count(1) from local_cancel_log where tx_no = #{txNo}
</select>
TCC總結:
如果拿TCC事務的處理流程與2PC兩階段提交做比較,2PC通常都是在跨庫的DB層面,而TCC則是在應用層面的處理,需要通過業務邏輯來實現。
優點:
這種分布式事務的實現方式的優勢在于,可以讓應用自己定義數據操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。
缺點:
而不足之處則在于對應用的侵入性非常強,業務邏輯的每個分支都需要實現try、confirm、cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。
github源碼地址:https://github.com/jjhyb/distributed-transaction
參考:
https://blog.csdn.net/hellozhxy/article/details/92843749
https://dromara.org/zh-cn/docs/hmily/index.html