TCC 分布式事務解決方案


一、什么是 TCC事務


TCC 是TryConfirmCancel三個詞語的縮寫,TCC要求每個分支事務實現三個操作:預處理Try、確認Confirm、撤銷Cancel。Try操作做業務檢查及資源預留,Confirm做業務確認操作Cancel實現一個與 Try或者 Commit相反的操作即回滾操作。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階段真的出錯了,需引入重試機制或人工處理
【4】TM事務管理器 TM事務管理器可以實現為獨立的服務,也可以讓全局事務發起方充當 TM的角色,TM獨立出來是為了成為公用組件,是為了考慮系統結構和軟件復用。TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條,用來記錄事務上下文,追蹤和記錄狀態,由於Confirm 和 Cancel失敗需進行重試,因此需要實現為冪等,冪等性是指同一個操作無論請求多少次,其結果都相同。

、TCC 解決方案


目前市面上的 TCC框架眾多比如下面這幾種:

框架名稱 Gitbub地址 star數量
tcc-transaction https://github.com/changmingxie/tcc-transaction 4351
Hmily https://github.com/yu199195/hmily 2788
ByteTCC https://github.com/liuyangming/ByteTCC 2156
EasyTransaction https://github.com/QNJR-GROUP/EasyTransaction 1907

Seata也支持TCC,但 Seata的 TCC模式對 Spring Cloud並沒有提供支持。因此更請傾向於輕量級易於理解的框架Hmily,來理解 TCC的原理以及事務協調運作的過程。Hmily是一個高性能分布式事務 TCC開源框架。基於Java語言來開發,支持Dubbo,Spring Cloud等。RPC框架進行分布式事務。它目前支持以下特性:
  ■ 支持嵌套事務(Nested transaction support);
  ■ 采用 disruptor框架進行事務日志的異步讀寫,與 RPC框架的性能毫無差別;
  ■ 支持 SpringBoot-starter 項目啟動,使用簡單;
  ■ RPC框架支持 : dubbo,motan,springcloud
  ■ 本地事務存儲支持 : redis,mongodb,zookeeper,fifile,mysql;
  ■ 事務日志序列化支持 :java,hessian,kryo,protostuff;
  ■ 采用 Aspect AOP 切面思想與 Spring無縫集成,天然支持集群;
  ■ RPC事務恢復,超時異常恢復等;

Hmily利用 AOP對參與分布式事務的本地方法與遠程方法進行攔截處理,通過多方攔截,事務參與者能透明的調用到另一方的Try、Confirm、Cancel方法;傳遞事務上下文;並記錄事務日志,酌情進行補償,重試等。Hmily不需要事務協調服務,但需要提供一個數據庫(mysql/mongodb/zookeeper/redis/fifile)來進行日志存儲。Hmily實現的 TCC服務與普通的服務一樣,只需要暴露一個接口,也就是它的 Try業務。Confirm/Cancel業務邏輯,全局事務提交/回滾需要時才提供,因此Confirm/Cancel業務只需要被 Hmily TCC事務框架發現即可,不需要被調用它的其他業務服務所感知。官網介紹

TCC需要注意三種異常處理分別是空回滾冪等懸掛:
【1】空回滾沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然后直接返回成功。出現原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄為失敗,這個時候其實是沒有執行 Try階段,當故障恢復后,分布式事務進行回滾則會調用二階段的 Cancel方法,從而形成空回滾。
解決思路關鍵是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。前面已經說過 TM在發起全局事務時生成全局事務記錄,全局事務ID貫穿整個分布式事務調用鏈條。再額外增加一張分支事務記錄表,其中有全局事務 ID分支事務 ID,第一階段 Try 方法里會插入一條記錄,表示一階段執行了。Cancel 接口里讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存在,則是空回滾。
【2】冪等通過前面介紹已經了解到,為了保證 TCC二階段提交重試機制不會引發數據不一致,要求 TCC 的二階段 Try、Confirm 和 Cancel 接口保證冪等,這樣不會重復使用或者釋放資源。如果冪等控制沒有做好,很有可能導致數據不一致等嚴重問題。
解決思路在上述“分支事務記錄”中增加執行狀態“事務ID”,每次執行前都查詢該狀態。
【3】懸掛懸掛就是對於一個分布式事務,其二階段 Cancel 接口比 Try 接口先執行。出現原因是在 RPC 調用分支事務 Try時,先注冊分支事務,再執行 RPC調用,如果此時 RPC 調用的網絡發生擁堵,通常 RPC 調用是有超時時間的,RPC 超時以后,TM就會通知 RM回滾該分布式事務,可能回滾完成后,RPC 請求才到達參與者真正執行,而一個 Try 方法預留的業務資源,只有該分布式事務才能使用,該分布式事務第一階段預留的業務資源就再也沒有人能夠處理了,對於這種情況,我們就稱為懸掛,即業務資源預留后沒法繼續處理
解決思路如果二階段執行完成,那一階段就不能再繼續執行。在執行一階段事務時判斷在該全局事務下,“分支事務記錄”表中是否已經有二階段事務記錄,如果有則不執行Try。

舉例:場景為 A 轉賬 30 元給 B,A和B賬戶在不同的服務

 1 賬戶Atry:
 2 檢查余額是否夠30元
 3 扣減30元
 4 confirm:
 5  6 cancel:
 7 增加30元
 8 賬戶B
 9 try10 增加30元
11 confirm:
12 13 cancel:
14 減少30元 

方案說明:
【1】賬戶A,這里的余額就是所謂的業務資源,按照前面提到的原則,在第一階段需要檢查並預留業務資源,因此,我們在扣錢 TCC 資源的 Try 接口里先檢查 A 賬戶余額是否足夠,如果足夠則扣除 30 元。 Confirm 接口表示正式提交,由於業務資源已經在 Try 接口里扣除掉了,那么在第二階段的 Confirm 接口里可以什么都不用做。Cancel接口的執行表示整個事務回滾,賬戶A回滾則需要把 Try 接口里扣除掉的 30 元還給賬戶。
【2】賬號B,在第一階段 Try 接口里實現給賬戶B加錢,Cancel 接口的執行表示整個事務回滾,賬戶B回滾則需要把Try 接口里加的 30 元再減去。

方案的問題分析:
【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

 1 try 2     try冪等校驗 
 3     try懸掛處理 
 4     檢查余額是否夠30元 
 5     扣減30元 
 6 
 7 confirm:
 8  9 
10 cancel:
11     cancel冪等校驗 
12     cancel空回滾處理 
13     增加可用余額30元

【賬戶B

1 try2 3 
4 confirm: 
5     confirm冪等校驗 
6     正式增加30元 
7     
8 cancel:
9

三、Hmily 實現 TCC事務


1】業務說明:本實例通過 Hmily實現 TCC分布式事務,模擬兩個賬戶的轉賬交易過程。兩個賬戶分別在不同的銀行(張三在bank1、李四在bank2),bank1、bank2是兩個微服務。交易過程是,張三給李四轉賬指定金額。上述交易步驟,要么一起成功,要么一起失敗,必須是一個整體性的事務。

2】數據庫:每個數據庫都創建 try、confirm、cancel三張日志表:用來記錄全局事務ID

 1 CREATE TABLE `local_try_log` ( 
 2                 `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) 
 3 ) ENGINE=InnoDB DEFAULT CHARSET=utf8
 4 
 5 CREATE TABLE `local_confirm_log` (
 6                  `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL 
 7 ) ENGINE=InnoDB DEFAULT CHARSET=utf8
 8 
 9 CREATE TABLE `local_cancel_log` ( 
10                 `tx_no` varchar(64) NOT NULL COMMENT '事務id', `create_time` datetime DEFAULT NULL, PRIMARY KEY (`tx_no`) 
11 ) ENGINE=InnoDB DEFAULT CHARSET=utf

3引入maven依賴

1 <dependency> 
2     <groupId>org.dromara</groupId> 
3     <artifactId>hmily‐springcloud</artifactId> 
4     <version>2.0.4‐RELEASE</version> 
5 </dependency>

4application.yml 中配置hmily:配置數據庫地址,因為會創建分支事務表

 1 org: 
 2     dromara: 
 3         hmily: 
 4         serializer : kryo 
 5         recoverDelayTime : 128 
 6         retryMax : 30 
 7         scheduledDelay : 128 
 8         scheduledThreadMax : 10 
 9         repositorySupport : db 
10         started: true 
11         hmilyDbConfig : 
12         driverClassName : com.mysql.jdbc.Driver 
13         url : jdbc:mysql://localhost:3306/bank?useUnicode=true 
14         username : root 
15         password : root

5創建配置類:配置類中接收 application.yml中的 Hmily配置信息,並創建 HmilyTransactionBootstrap Bean 和添加 @EnableAspectJAutoProxy(proxyTargetClass=true) 切面注解。

 1 @Configuration
 2 @EnableAspectJAutoProxy(proxyTargetClass=true)
 3 public class DatabaseConfiguration {
 4     @Bean
 5     public HmilyTransactionBootstrap hmilyTransactionBootstrap(HmilyInitService hmilyInitService){
 6         HmilyTransactionBootstrap hmilyTransactionBootstrap = new HmilyTransactionBootstrap(hmilyInitService);
 7         hmilyTransactionBootstrap.setSerializer(env.getProperty("org.dromara.hmily.serializer"));
 8         hmilyTransactionBootstrap.setRecoverDelayTime(Integer.parseInt(env.getProperty("org.dromara.hmily.recoverDelayTime")));
 9         hmilyTransactionBootstrap.setRetryMax(Integer.parseInt(env.getProperty("org.dromara.hmily.retryMax")));
10         hmilyTransactionBootstrap.setScheduledDelay(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledDelay")));
11         hmilyTransactionBootstrap.setScheduledThreadMax(Integer.parseInt(env.getProperty("org.dromara.hmily.scheduledThreadMax")));
12         hmilyTransactionBootstrap.setRepositorySupport(env.getProperty("org.dromara.hmily.repositorySupport"));
13         hmilyTransactionBootstrap.setStarted(Boolean.parseBoolean(env.getProperty("org.dromara.hmily.started")));
14         HmilyDbConfig hmilyDbConfig = new HmilyDbConfig();
15         hmilyDbConfig.setDriverClassName(env.getProperty("org.dromara.hmily.hmilyDbConfig.driverClassName"));
16         hmilyDbConfig.setUrl(env.getProperty("org.dromara.hmily.hmilyDbConfig.url"));
17         hmilyDbConfig.setUsername(env.getProperty("org.dromara.hmily.hmilyDbConfig.username"));
18         hmilyDbConfig.setPassword(env.getProperty("org.dromara.hmily.hmilyDbConfig.password"));
19         hmilyTransactionBootstrap.setHmilyDbConfig(hmilyDbConfig);
20         return hmilyTransactionBootstrap;
21     }
22 }

6啟動類修改:增加 org.dromara.hmily的掃描項:

 1 @SpringBootApplication 
 2 @EnableDiscoveryClient 
 3 @EnableHystrix 
 4 @EnableFeignClients(basePackages = {"cn.itcast.dtx.tccdemo.bank1.spring"}) 
 5 @ComponentScan({"cn.itcast.dtx.tccdemo.bank1","org.dromara.hmily"}) 
 6 public class Bank1HmilyServer { 
 7     public static void main(String[] args) { 
 8         SpringApplication.run(Bank1HmilyServer.class, args); 
 9     } 
10 }

7dtx-tcc-demo-bank1 (張三)實現 Try、Commit和 Cancel方法,如下:

 1 try 2     try冪等校驗 
 3     try懸掛處理 
 4     檢查余額是夠扣減金額 
 5     扣減金額 
 6     
 7 confirm:
 8  9     
10 cancel:
11     cancel冪等校驗 
12     cancel空回滾處理 
13     增加可用余額

8張三服務層:實現 Try、Commit 和 Cancel方法。Try 方法上添加@Hmily 注解表示開啟TCC,並配置 Commit提交方法和 Cancel回滾方法。注意:三個方法的入參和返回值必須相同

 1 @Service
 2 @Slf4j
 3 public class AccountInfoServiceImpl implements AccountInfoService {
 4 
 5     @Autowired
 6     AccountInfoDao accountInfoDao;
 7 
 8     @Autowired
 9     Bank2Client bank2Client;
10 
11     // 賬戶扣款,就是tcc的try方法
12 
13     /**
14      *     try冪等校驗
15      *     try懸掛處理
16      *     檢查余額是夠扣減金額
17      *     扣減金額
18      * @param accountNo
19      * @param amount
20      */
21     @Override
22     @Transactional
23     //只要標記@Hmily就是try方法,在注解中指定confirm、cancel兩個方法的名字
24     @Hmily(confirmMethod="commit",cancelMethod="rollback")
25     public void updateAccountBalance(String accountNo, Double amount) {
26         //獲取全局事務id
27         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
28         log.info("bank1 try begin 開始執行...xid:{}",transId);
29         //冪等判斷 判斷local_try_log表中是否有try日志記錄,如果有則不再執行
30         if(accountInfoDao.isExistTry(transId)>0){
31             log.info("bank1 try 已經執行,無需重復執行,xid:{}",transId);
32             return ;
33         }
34 
35         //try懸掛處理,如果cancel、confirm有一個已經執行了,try不再執行
36         if(accountInfoDao.isExistConfirm(transId)>0 || accountInfoDao.isExistCancel(transId)>0){
37             log.info("bank1 try懸掛處理  cancel或confirm已經執行,不允許執行try,xid:{}",transId);
38             return ;
39         }
40 
41         //扣減金額
42         if(accountInfoDao.subtractAccountBalance(accountNo, amount)<=0){
43             //扣減失敗
44             throw new RuntimeException("bank1 try 扣減金額失敗,xid:{}"+transId);
45         }
46         //插入try執行記錄,用於冪等判斷
47         accountInfoDao.addTry(transId);
48 
49         //遠程調用李四,轉賬
50         if(!bank2Client.transfer(amount)){
51             throw new RuntimeException("bank1 遠程調用李四微服務失敗,xid:{}"+transId);
52         }
53         if(amount == 2){
54             throw new RuntimeException("人為制造異常,xid:{}"+transId);
55         }
56         log.info("bank1 try end 結束執行...xid:{}",transId);
57     }
58 
59     //confirm方法
60     @Transactional
61     public void commit(String accountNo, Double amount){
62         //獲取全局事務id
63         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
64         log.info("bank1 confirm begin 開始執行...xid:{},accountNo:{},amount:{}",transId,accountNo,amount);
65     }
66 
67     /** cancel方法
68      *     cancel冪等校驗
69      *     cancel空回滾處理
70      *     增加可用余額
71      * @param accountNo
72      * @param amount
73      */
74     @Transactional
75     public void rollback(String accountNo, Double amount){
76         //獲取全局事務id
77         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
78         log.info("bank1 cancel begin 開始執行...xid:{}",transId);
79         //    cancel冪等校驗
80         if(accountInfoDao.isExistCancel(transId)>0){
81             log.info("bank1 cancel 已經執行,無需重復執行,xid:{}",transId);
82             return ;
83         }
84         //cancel空回滾處理,如果try沒有執行,cancel不允許執行
85         if(accountInfoDao.isExistTry(transId)<=0){
86             log.info("bank1 空回滾處理,try沒有執行,不允許cancel執行,xid:{}",transId);
87             return ;
88         }
89         //    增加可用余額
90         accountInfoDao.addAccountBalance(accountNo,amount);
91         //插入一條cancel的執行記錄
92         accountInfoDao.addCancel(transId);
93         log.info("bank1 cancel end 結束執行...xid:{}",transId);
94     }
95 }

9李四在張三項目中的接口定義:需要添加 @Hmily 注解,將全局事務ID傳輸給李四

1 @FeignClient(value="tcc-demo-bank2",fallback=Bank2ClientFallback.class)
2 public interface Bank2Client {
3     //遠程調用李四的微服務
4     @GetMapping("/bank2/transfer")
5     @Hmily
6     public  Boolean transfer(@RequestParam("amount") Double amount);
7 }

10dtx-tcc-demo-bank2 李四項目:實現如下功能

1 try2 3 confirm:
4     confirm冪等校驗 
5     正式增加金額 
6 cancel:
7

11李四服務層:與張三的服務格式相同

 1 @Service
 2 @Slf4j
 3 public class AccountInfoServiceImpl implements AccountInfoService {
 4 
 5     @Autowired
 6     AccountInfoDao accountInfoDao;
 7 
 8     @Override
 9     @Hmily(confirmMethod="confirmMethod", cancelMethod="cancelMethod")
10     public void updateAccountBalance(String accountNo, Double amount) {
11         //獲取全局事務id
12         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
13         log.info("bank2 try begin 開始執行...xid:{}",transId);
14     }
15 
16     /**
17      * confirm方法
18      *     confirm冪等校驗
19      *     正式增加金額
20      * @param accountNo
21      * @param amount
22      */
23     @Transactional
24     public void confirmMethod(String accountNo, Double amount){
25         //獲取全局事務id
26         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
27         log.info("bank2 confirm begin 開始執行...xid:{}",transId);
28         if(accountInfoDao.isExistConfirm(transId)>0){
29             log.info("bank2 confirm 已經執行,無需重復執行...xid:{}",transId);
30             return ;
31         }
32         //增加金額
33         accountInfoDao.addAccountBalance(accountNo,amount);
34         //增加一條confirm日志,用於冪等
35         accountInfoDao.addConfirm(transId);
36         log.info("bank2 confirm end 結束執行...xid:{}",transId);
37     }
38 
39     /**
40      * @param accountNo
41      * @param amount
42      */
43     public void cancelMethod(String accountNo, Double amount){
44         //獲取全局事務id
45         String transId = HmilyTransactionContextLocal.getInstance().get().getTransId();
46         log.info("bank2 cancel begin 開始執行...xid:{}",transId);
47 
48     }
49 }

四、小結


如果拿 TCC事務的處理流程與 2PC兩階段提交做比較,2PC通常都是在跨庫的 DB層面,而 TCC則在應用層面的處理,需要通過業務邏輯來實現。這種分布式事務的實現方式的優勢在於,可以讓應用自己定義數據操作的粒度,使得降低鎖沖突、提高吞吐量成為可能。而不足之處則在於對應用的侵入性非常強,業務邏輯的每個分支都需要實現Try、Confirm、Cancel三個操作。此外,其實現難度也比較大,需要按照網絡狀態、系統故障等不同的失敗原因實現不同的回滾策略。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM