Seata-TCC 解決分佈式事務,真香!

大家好,我是不才陳某~

今天這篇文章介紹一下 Seata 如何實現 TCC 事務模式,文章目錄如下:

什麼是 TCC 模式?

TCC(Try Confirm Cancel)方案是一種應用層面侵入業務的兩階段提交。是目前最火的一種柔性事務方案,其核心思想是:針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作。

TCC 分爲兩個階段,分別如下:

  1. Confirm(確認):執行真正的業務(執行業務,釋放鎖)

  2. Cancle(取消):是預留資源的取消(出問題,釋放鎖)

TCC

爲了方便理解,下面以電商下單爲例進行方案解析,這裏把整個過程簡單分爲扣減庫存,訂單創建 2 個步驟,庫存服務和訂單服務分別在不同的服務器節點上。

假設商品庫存爲 100,購買數量爲 2,這裏檢查和更新庫存的同時,凍結用戶購買數量的庫存,同時創建訂單,訂單狀態爲待確認。

①Try 階段

TCC 機制中的 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、演示場景

就以電商系統中下訂單爲例,爲了演示,直接去掉賬戶服務,以訂單服務、庫存服務爲例介紹。

具體的邏輯如下:

  1. 客戶端調用下訂單接口

  2. 扣庫存

  3. 創建訂單

  4. 請求完成

根據上面的邏輯可知,訂單服務肯定是主業務服務,事務的發起方,庫存服務是從業務服務,參與事務的決策。

Seata 的 AT 模式解決方案僞代碼如下:

@GlobalTransactional
public Result<Void> 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 操作成功之後提交事務,涉及到的操作如下:

  1. 釋放 try 操作凍結的庫存(凍結庫存 - 購買數量)

  2. 生成訂單

僞代碼如下:

@Transactional
public boolean confirm(){
    //釋放掉try操作預留的庫存
    cleanFrozen();
    //修改訂單,狀態爲已完成
    updateOrder();
    return true;
}

注意:這裏如果返回 false,遵循 TCC 規範,應該要不斷重試,直到 confirm 完成。

3、二階段的 cancel 操作

cancel 操作在一階段 try 操作出現異常之後執行,用於回滾資源,涉及到的操作如下:

  1. 恢復凍結的庫存(凍結庫存 - 購買數量、庫存 + 購買數量)

  2. 刪除訂單

僞代碼如下:

@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、總結

針對以上三個異常,落地的解決方案很多,比如維護一個事務狀態表,每個事務的執行階段全部記錄下來。

Seata 整合 TCC 實現

關於如何搭建項目、添加依賴這裏就不再細說了,不熟悉的可以看我之前的文章:對比 7 種分佈式事務方案,還是偏愛阿里開源的 Seata,真香!(原理 + 實戰)

本節只介紹關鍵代碼,畢竟篇幅有限,其他部分請自行下載源碼。

案例源碼已上傳 GitHub,關注公衆號:碼猿技術專欄,回覆關鍵:9528 獲取!

源碼目錄如下:

源碼目錄

項目啓動所需要的相關文件如下圖:

nacos 目錄中的 SEATA_GROUP 是 Seata 事務服務端和客戶端所需要的相關配置,直接導入 nacos 即可。

seata 目錄中的 conf 是 1.3.0 版本服務端的配置

SQL 目錄是相關的幾個數據庫。

1、TCC 接口定義

在 order-boot 模塊創建 OrderTccService,代碼如下:

代碼中註釋已經很完整了,下面挑幾個重點介紹一下:

  1. @LocalTCC:該註解開啓 TCC 事務

  2. @TwoPhaseBusinessAction:該註解標註在 try 方法上,其中的三個屬性如下:

  3. name:TCC 事務的名稱,必須是唯一的

  4. commitMethod:confirm 方法的名稱,默認是 commit

  5. rollbackMethod:cancel 方法的名稱,,默認是 rollback

  6. 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 是時間戳。

代碼如下:

思路如下:

  1. 在 try 方法最後使用冪等工具類中的 add 方法添加值

  2. 在 confirm、cancel 方法中使用冪等工具類中的 remove 方法移出值

  3. 在 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 是事務的狀態。

其他的字段自己可以擴展

解決懸掛問題的邏輯如下:

  1. cancel 方法中將當前全局事務 ID 記錄到事務日誌表中,狀態爲 cancel

  2. try 方法執行資源操作前檢查事務日誌表中當前全局事務 ID 是否已經是 cancel 狀態

4、創建訂單的業務方法

上面只是完成了 TCC 的三個方法,主業務事務發起方還未提供,代碼如下:

@GlobalTransactional 這個註解開啓了全局事務,是事務的發起方。

內部直接調用的 TCC 的 try 方法。

5、其他的配置

以上只是列出了關鍵的步驟,剩餘其他的配置自己根據案例源碼完善,如下:

  1. 接口測試

  2. 整合 nacos

  3. 整合 feign

  4. 整合 seata,TCC 模式中的配置和 AT 模式的 Seata 配置相同

注意:一定要配置 Seata 的事務組 tx-service-group,配置方法見之前的文章。

6、總結

TCC 事務模型相對來說比較簡單的一種,有興趣的可以下載源碼試試。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/AkvRhiPZrHV97LgrZD_g5Q