今天這篇文章介紹一下Seata如何實現TCC事務模式,文章目錄如下:
什么是TCC模式?
TCC(Try Confirm Cancel)方案是一種應用層面侵入業務的兩階段提交。是目前最火的一種柔性事務方案,其核心思想是:針對每個操作,都要注冊一個與其對應的確認和補償(撤銷)操作。
TCC分為兩個階段,分別如下:
- 第一階段:Try(嘗試),主要是對業務系統做檢測及資源預留 (加鎖,鎖住資源)
- 第二階段:本階段根據第一階段的結果,決定是執行confirm還是cancel
Confirm(確認):執行真正的業務(執行業務,釋放鎖)
Cancle(取消):是預留資源的取消(出問題,釋放鎖)
TCC
為了方便理解,下面以電商下單為例進行方案解析,這里把整個過程簡單分為扣減庫存,訂單創建 2 個步驟,庫存服務和訂單服務分別在不同的服務器節點上。
假設商品庫存為 100,購買數量為 2,這里檢查和更新庫存的同時,凍結用戶購買數量的庫存,同時創建訂單,訂單狀態為待確認。
①Try 階段
TCC 機制中的 Try 僅是一個初步操作,它和后續的確認一起才能真正構成一個完整的業務邏輯,這個階段主要完成:
- 完成所有業務檢查( 一致性 ) 。
- 預留必須業務資源( 準隔離性 ) 。
- Try 嘗試執行業務。
Try階段
②Confirm / Cancel 階段
根據 Try 階段服務是否全部正常執行,繼續執行確認操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作滿足冪等性,如果 Confirm 或 Cancel 操作執行失敗,將會不斷重試直到執行完成。
Confirm:當 Try 階段服務全部正常執行, 執行確認業務邏輯操作,業務如下圖:
Try->Confirm
這里使用的資源一定是 Try 階段預留的業務資源。在 TCC 事務機制中認為,如果在 Try 階段能正常的預留資源,那 Confirm 一定能完整正確的提交。
Confirm 階段也可以看成是對 Try 階段的一個補充,Try+Confirm 一起組成了一個完整的業務邏輯。
Cancel:當 Try 階段存在服務執行失敗, 進入 Cancel 階段,業務如下圖:
Try-Cancel
Cancel 取消執行,釋放 Try 階段預留的業務資源,上面的例子中,Cancel 操作會把凍結的庫存釋放,并更新訂單狀態為取消。
以上便是TCC模式的全部概念,這部分內容在陳某之前的文章也是詳細的介紹過:對比7種分布式事務方案,還是偏愛阿里開源的Seata,真香!(原理+實戰)
TCC模式的三種類型?
業內實際生產中對TCC模式進行了擴展,總結出了如下三種類型,其實從官方的定義中無此說法,不過是企業生產中根據實際的需求衍生出來的三種方案。
1、通用型 TCC 解決方案
通用型TCC解決方案是最經典的TCC事務模型的實現,正如第一節介紹的模型,所有的從業務都參與到主業務的決策中。
通用型TCC
適用場景:
由于從業務服務是同步調用,其結果會影響到主業務服務的決策,因此通用型 TCC 分布式事務解決方案適用于執行時間確定且較短的業務,比如電商系統的三個核心服務:訂單服務、賬戶服務、庫存服務。
這個三個服務要么同時成功,要么同時失敗。
當庫存服務、賬戶服務的第二階段調用完成后,整個分布式事務完成。
2、異步確保型 TCC 解決方案
異步確保型 TCC 解決方案的直接從業務服務是可靠消息服務,而真正的從業務服務則通過消息服務解耦,作為消息服務的消費端,異步地執行。
異步確保型
可靠消息服務需要提供 Try,Confirm,Cancel 三個接口。Try 接口預發送,只負責持久化存儲消息數據;Confirm 接口確認發送,這時才開始真正的投遞消息;Cancel 接口取消發送,刪除消息數據。
消息服務的消息數據獨立存儲,獨立伸縮,降低從業務服務與消息系統間的耦合,在消息服務可靠的前提下,實現分布式事務的最終一致性。
此解決方案雖然增加了消息服務的維護成本,但由于消息服務代替從業務服務實現了 TCC 接口,從業務服務不需要任何改造,接入成本非常低。
適用場景:
由于從業務服務消費消息是一個異步的過程,執行時間不確定,可能會導致不一致時間窗口增加。因此,異步確保性 TCC 分布式事務解決方案只適用于對最終一致性時間敏感度較低的一些被動型業務(從業務服務的處理結果不影響主業務服務的決策,只被動的接收主業務服務的決策結果)。比如會員注冊服務和郵件發送服務:
3、補償型 TCC 解決方案
補償型 TCC 解決方案與通用型 TCC 解決方案的結構相似,其從業務服務也需要參與到主業務服務的活動決策當中。但不一樣的是,前者的從業務服務只需要提供 Do 和 Compensate 兩個接口,而后者需要提供三個接口。
Do 接口直接執行真正的完整業務邏輯,完成業務處理,業務執行結果外部可見;Compensate 操作用于業務補償,抵消或部分抵消正向業務操作的業務結果,Compensate操作需滿足冪等性。
與通用型解決方案相比,補償型解決方案的從業務服務不需要改造原有業務邏輯,只需要額外增加一個補償回滾邏輯即可,業務改造量較小。但要注意的是,業務在一階段就執行完整個業務邏輯,無法做到有效的事務隔離,當需要回滾時,可能存在補償失敗的情況,還需要額外的異常處理機制,比如人工介入。
適用場景:
由于存在回滾補償失敗的情況,補償型 TCC 分布式事務解決方案只適用于一些并發沖突較少或者需要與外部交互的業務,這些外部業務不屬于被動型業務,其執行結果會影響主業務服務的決策。
以上部分內容參考自:https://seata.io/zh-cn/blog/tcc-mode-applicable-scenario-analysis.html?utm_source=gold_browser_extension
TCC事務模式的落地實現
在前面文章中介紹了Seata的AT模式,有不清楚的可以看:對比7種分布式事務方案,還是偏愛阿里開源的Seata,真香!(原理+實戰)
當然Seata支持的事務模式不局限于AT模式,還有TCC模式、SAGA模式、XA模式,下面整合一下TCC模式。
1、演示場景
就以電商系統中下訂單為例,為了演示,直接去掉賬戶服務,以訂單服務、庫存服務為例介紹。
具體的邏輯如下:
- 客戶端調用下訂單接口
- 扣庫存
- 創建訂單
- 請求完成
根據上面的邏輯可知,訂單服務肯定是主業務服務,事務的發起方,庫存服務是從業務服務,參與事務的決策。
Seata的AT模式解決方案偽代碼如下:
- @GlobalTransactional
-
public Result
createOrder(Long productId,Long num,.....){ - //1、扣庫存
- reduceStorage();
- //2、創建訂單
- saveOrder();
- }
@GlobalTransactional這個注解用于發起一個全局事務。
但是AT模式有局限性,如下:
- 性能低,鎖定資源時間太長
- 無法解決跨應用的事務
因此對于要求性能的下單接口,可以考慮使用TCC模式進行拆分成兩階段執行,這樣整個流程鎖定資源的時間將會變短,性能也能提高。
此時的TCC模式的拆分如下:
1)、一階段的Try操作
TCC模式中的Try階段其實就是預留資源,在這個過程中可以將需要的商品數量的庫存凍結,這樣就要在庫存表中維護一個凍結的庫存這個字段。
偽代碼如下:
- @Transactional
- public boolean try(){
- //凍結庫存
- frozenStorage();
- //生成訂單,狀態為待確認
- saveOrder();
- }
- “
注意:@Transactional開啟了本地事務,只要出現了異常,本地事務將會回滾,同時執行第二階段的cancel操作。
2)、二階段的confirm操作
confirm操作在一階段try操作成功之后提交事務,涉及到的操作如下:
- 釋放try操作凍結的庫存(凍結庫存-購買數量)
- 生成訂單
偽代碼如下:
- @Transactional
- public boolean confirm(){
- //釋放掉try操作預留的庫存
- cleanFrozen();
- //修改訂單,狀態為已完成
- updateOrder();
- return true;
- }
注意:這里如果返回false,遵循TCC規范,應該要不斷重試,直到confirm完成。
3)、二階段的cancel操作
cancel操作在一階段try操作出現異常之后執行,用于回滾資源,涉及到的操作如下:
- 恢復凍結的庫存(凍結庫存-購買數量、庫存+購買數量)
- 刪除訂單
偽代碼如下:
- @Transactional
- public boolean cancel(){
- //釋放掉try操作預留的庫存
- rollbackFrozen();
- //修改訂單,狀態為已完成
- delOrder();
- return true;
- }
注意:這里如果返回false,遵循TCC規范,應該要不斷重試,直到cancel完成。
2、TCC事務模型的三個異常
實現TCC事務模型涉及到的三個異常是不可避免的,實際生產中必須要規避這三大異常。
1)、空回滾
定義:在未調用try方法或try方法未執行成功的情況下,就執行了cancel方法進行了回滾。
怎么理解呢?未調用try方法就執行了cancel方法,這個很容易理解,既然沒有預留資源,那么肯定是不能回滾。
try方法未執行成功是什么意思?
可以看上節中的第一階段try方法的偽代碼,由于try方法開啟了本地事務,一旦try方法執行過程中出現了異常,將會導致try方法的本地事務回滾(注意這里不是cancel方法回滾,而是try方法的本地事務回滾),這樣其實try方法中的所有操作都將會回滾,也就沒有必要調用cancel方法。
但是實際上一旦try方法拋出了異常,那么必定是要調用cancel方法進行回滾,這樣就導致了空回滾。
解決方案:
解決邏輯很簡單:在cancel方法執行操作之前,必須要知道try方法是否執行成功。
2)、冪等性
TCC模式定義中提到:如果confirm或者cancel方法執行失敗,要一直重試直到成功。
這里就涉及了冪等性,confirm和cancel方法必須保證同一個全局事務中的冪等性。
解決方案:
解決邏輯很簡單:對付冪等,自然是要利用冪等標識進行防重操作。
3)、懸掛
事務協調器在調用 TCC 服務的一階段 Try 操作時,可能會出現因網絡擁堵而導致的超時,此時事務管理器會觸發二階段回滾,調用 TCC 服務的 Cancel 操作,Cancel 調用未超時;
在此之后,擁堵在網絡上的一階段 Try 數據包被 TCC 服務收到,出現了二階段 Cancel 請求比一階段 Try 請求先執行的情況,此 TCC 服務在執行晚到的 Try 之后,將永遠不會再收到二階段的 Confirm 或者 Cancel ,造成 TCC 服務懸掛。
解決方案:
解決邏輯很簡單:在執行try方法操作資源之前判斷cancel方法是否已經執行;同樣的在cancel方法執行后要記錄執行的狀態。
4)、總結
針對以上三個異常,落地的解決方案很多,比如維護一個事務狀態表,每個事務的執行階段全部記錄下來。
- 冪等:在執行confirm或者cancel之前根據事務狀態表查詢當前全局事務是否已經執行過confirm或者cancel方法
- 空回滾:在執行cancel之前才能根據事務狀態表查詢當前全局事務是否已經執行成功try方法
- 懸掛:在執行try方法之前,根據事務狀態表查詢當前全局事務是否已經執行過cancel方法
Seata整合TCC實現
關于如何搭建項目、添加依賴這里就不再細說了,不熟悉的可以看我之前的文章:對比7種分布式事務方案,還是偏愛阿里開源的Seata,真香!(原理+實戰)
本節只介紹關鍵代碼,畢竟篇幅有限,其他部分請自行下載源碼。
源碼目錄如下:
源碼目錄
項目啟動所需要的相關文件如下圖:
nacos目錄中的SEATA_GROUP是Seata事務服務端和客戶端所需要的相關配置,直接導入nacos即可。
seata目錄中的conf是1.3.0版本服務端的配置
SQL目錄是相關的幾個數據庫。
1、TCC接口定義
在order-boot模塊創建OrderTccService,代碼如下:
代碼中注釋已經很完整了,下面挑幾個重點介紹一下:
- @LocalTCC:該注解開啟TCC事務
-
@TwoPhaseBusinessAction:該注解標注在try方法上,其中的三個屬性如下:
- name:TCC事務的名稱,必須是唯一的
- commitMethod:confirm方法的名稱,默認是commit
- rollbackMethod:cancel方法的名稱,,默認是rollback
- confirm和cancel的返回值尤為重要,返回false則會不斷的重試。
2、TCC接口實現
定義有了,總要實現,如下:
1)、try方法
try方法
①處的代碼是為了防止懸掛異常,從事務日志表中獲取全局事務ID的狀態,如果是cancel狀態則不執行。
②處的代碼凍結庫存
③處的代碼生成訂單,狀態為待確認
④處的代碼向冪等工具類中添加一個標記,key為當前類和全局事務ID,value為當前時間戳。
注意:必須要開啟本地事務,如上代碼使用@Transactional開啟本地事務
2)、confirm方法
confirm方法
①處的代碼從冪等工具類中根據當前類和全局事務ID獲取值,由于try階段執行成功會向其中添加值,confirm方法執行成功會移出這個值,因此在confirm開頭判斷這個值是否存在就起到了冪等效果,防止重試的效果。
⑥處的代碼從冪等工具類中移出try方法中添加的值。
②處的代碼是從BusinessActionContext中獲取try方法中的入參。
③處的代碼是釋放掉凍結的庫存
④處的代碼是修改訂單的狀態為已完成。
注意:1. 開啟本地事務 2. 注意返回值,返回false時將會重試
3)、cancel方法
cancel方法
①處的代碼是向事務日志記錄表中插入一條數據,標記當前事務進入cancel方法,用來防止懸掛,這個和try方法中的①處的代碼相呼應。
②處的代碼是為了防止冪等和空回滾,因為只有當try方法中執行成功冪等工具類中對應的當前類和全局事務ID才會存儲該值。這樣既防止了冪等,也防止了空回滾。
③處的代碼恢復凍結的庫存。
④處的代碼刪除這筆訂單
⑤處的代碼是移出冪等工具類當前類和全局事務ID對應的值。
3、如何防止TCC模型的三個異常?
實現方法有很多,有些案例是全部使用事務日志表記錄當前的狀態,這樣完美的解決了冪等、空回滾、懸掛的問題。
陳某這里為了方便,使用了兩種方案,如下:
1)、冪等、空回滾
使用了一個冪等工具類,其中是個Map,key為當前類和全局事務ID,value是時間戳。
代碼如下:
思路如下:
- 在try方法最后使用冪等工具類中的add方法添加值
- 在confirm、cancel方法中使用冪等工具類中的remove方法移出值
- 在confirm、cancel方法中使用冪等工具類中get方法獲取值,如果為空,則表示已經執行過了,直接返回true,這樣既防止了冪等,也防止了空回滾。
2、懸掛
懸掛的實現依靠的是事務日志表,表結構如下:
- CREATE TABLE `transactional_record` (
- `id` bigint(11) NOT NULL AUTO_INCREMENT,
- `xid` varchar(100) NOT NULL,
- `status` int(1) DEFAULT NULL COMMENT '1. try 2 commit 3 cancel ',
- PRIMARY KEY (`id`) USING BTREE
- ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
其中的xid是全局事務ID,status是事務的狀態。
其他的字段自己可以擴展
解決懸掛問題的邏輯如下:
- cancel方法中將當前全局事務ID記錄到事務日志表中,狀態為cancel
- try方法執行資源操作前檢查事務日志表中當前全局事務ID是否已經是cancel狀態
4、創建訂單的業務方法
上面只是完成了TCC的三個方法,主業務事務發起方還未提供,代碼如下:
@GlobalTransactional這個注解開啟了全局事務,是事務的發起方。
內部直接調用的TCC的try方法。
5、其他的配置
以上只是列出了關鍵的步驟,剩余其他的配置自己根據案例源碼完善,如下:
- 接口測試
- 整合nacos
- 整合feign
- 整合seata,TCC模式中的配置和AT模式的Seata配置相同
注意:一定要配置Seata的事務組tx-service-group,配置方法見之前的文章。
6、總結
TCC事務模型相對來說比較簡單的一種,有興趣的可以下載源碼試試。
原文鏈接:https://mp.weixin.qq.com/s/OyIRPNd2bJZlcin9VFO9hw