1. 程式人生 > >如何選擇分散式事務形態(TCC、SAGA、補償、基於訊息的最終一致等等)

如何選擇分散式事務形態(TCC、SAGA、補償、基於訊息的最終一致等等)

各種形態的分散式事務

分散式事務有多種主流形態,包括:

  • 基於訊息實現的分散式事務
  • 基於補償實現的分散式事務
  • 基於TCC實現的分散式事務
  • 基於SAGA實現的分散式事務
  • 基於2PC實現的分散式事務

這些形態的原理已經在很多文章中進行了剖析,用“分散式事務”關鍵字就能搜到對應的文章,本文不再贅述這些形態的原理,並將重點放在如何根據業務選擇對應的分散式事務形態上。

何時選擇單機事務?

這個相信大家都很清楚,在條件允許的情況下,我們應該儘可能地使用單機事務,因為單機事務裡,無需額外協調其他資料來源,減少了網路互動時間消耗以及協調時所需的儲存IO消耗,在修改等量業務資料的情況下,單機事務將會有更高的效能。

但單機資料庫由於 業務邏輯解耦等因素進行了資料庫垂直拆分、或者由於單機資料庫效能壓力等因素進行了資料庫水平拆分之後,資料分佈於多個數據庫,這時若需要對多個數據庫的資料進行協調變更,則需要引入分散式事務。

分散式事務的模式有很多種,那究竟要怎麼選擇適合業務的模式呢?以下我們將從使用場景、效能、開發成本這幾個方面進行分析。

何時選擇基於訊息實現的事務?

基於訊息實現的事務適用於分散式事務的提交或回滾只取決於事務發起方的業務需求,其他資料來源的資料變更跟隨發起方進行的業務場景。

舉個例子,假設存在業務規則:某筆訂單成功後,為使用者加一定的積分。

在這條規則裡,管理訂單資料來源的服務為事務發起方,管理積分資料來源的服務為事務跟隨者。

從這個過程可以看到,基於訊息佇列實現的事務存在以下操作:

  • 訂單服務建立訂單,提交本地事務
  • 訂單服務釋出一條訊息
  • 積分服務收到訊息後加積分

我們可以看到它的整體流程是比較簡單的,同時業務開發工作量也不大:

  • 編寫訂單服務裡訂單建立的邏輯
  • 編寫積分服務裡增加積分的邏輯

可以看到該事務形態過程簡單,效能消耗小,發起方與跟隨方之間的流量峰谷可以使用佇列填平,同時業務開發工作量也基本與單機事務沒有差別,都不需要編寫反向的業務邏輯過程。因此基於訊息佇列實現的事務是我們除了單機事務外最優先考慮使用的形態。

何時選擇利用補償實現的事務?

但是基於訊息實現的事務並不能解決所有的業務場景,例如以下場景:某筆訂單完成時,同時扣掉使用者的現金。

這裡事務發起方是管理訂單庫的服務,但對整個事務是否提交併不能只由訂單服務決定,因為還要確保使用者有足夠的錢,才能完成這筆交易,而這個資訊在管理現金的服務裡。這裡我們可以引入基於補償實現的事務,其流程如下:

  • 建立訂單資料,但暫不提交本地事務
  • 訂單服務傳送遠端呼叫到現金服務,以扣除對應的金額
  • 上述步驟成功後提交訂單庫的事務

以上這個是正常成功的流程,異常流程需要回滾的話,將額外發送遠端呼叫到現金服務以加上之前扣掉的金額。

以上流程比基於訊息佇列實現的事務的流程要複雜,同時開發的工作量也更多:

  • 編寫訂單服務裡建立訂單的邏輯
  • 編寫現金服務里扣錢的邏輯
  • 編寫現金服務裡補償返還的邏輯

可以看到,該事務流程相對於基於訊息實現的分散式事務更為複雜,需要額外開發相關的業務回滾方法,也失去了服務間流量削峰填谷的功能。但其僅僅只比基於訊息的事務複雜多一點,若不能使用基於訊息佇列的最終一致性事務,那麼可以優先考慮使用基於補償的事務形態。

(題外話:阿里GTS也是利用補償實現,只不過補償程式碼自動生成,無需業務干預,同時接管應用資料來源,禁止業務修改處於全域性事務狀態中的記錄。)

何時選擇利用TCC實現的事務

然而基於補償的事務形態也並非能實現所有的需求,如以下場景:某筆訂單完成時,同時扣掉使用者的現金,但交易未完成,也未被取消時,不能讓客戶看到錢變少了。

這時我們可以引入TCC,其流程如下:

  • 訂單服務建立訂單
  • 訂單服務傳送遠端呼叫到現金服務,凍結客戶的現金
  • 提交訂單服務資料
  • 訂單服務傳送遠端呼叫到現金服務,扣除客戶凍結的現金

以上是正常完成的流程,若為異常流程,則需要傳送遠端呼叫請求到現金服務,撤銷凍結的金額。

以上流程比基於補償實現的事務的流程要複雜,同時開發的工作量也更多:

  • 訂單服務編寫建立訂單的邏輯
  • 現金服務編寫凍結現金的邏輯
  • 現金服務編寫扣除現金的邏輯
  • 現金服務編寫解凍現金的邏輯

TCC實際上是最為複雜的一種情況,其能處理所有的業務場景,但無論出於效能上的考慮,還是開發複雜度上的考慮,都應該儘量避免該類事務。

何時選擇利用SAGA實現的事務?

SAGA可以看做一個非同步的、利用佇列實現的補償事務。

其適用於無需馬上返回業務發起方最終狀態的場景,例如:你的請求已提交,請稍後查詢或留意通知 之類。

將上述補償事務的場景用SAGA改寫,其流程如下:

  • 訂單服務建立最終狀態未知的訂單記錄,並提交事務
  • 現金服務扣除所需的金額,並提交事務
  • 訂單服務更新訂單狀態為成功,並提交事務

以上為成功的流程,若現金服務扣除金額失敗,那麼,最後一步訂單服務將會更新訂單狀態為失敗。

其業務編碼工作量比補償事務多一點,包括以下內容:

  • 訂單服務建立初始訂單的邏輯
  • 訂單服務確認訂單成功的邏輯
  • 訂單服務確認訂單失敗的邏輯
  • 現金服務扣除現金的邏輯
  • 現金服務補償返回現金的邏輯

但其相對於補償事務形態有效能上的優勢,所有的本地子事務執行過程中,都無需等待其呼叫的子事務執行,減少了加鎖的時間,這在事務流程較多較長的業務中效能優勢更為明顯。同時,其利用佇列進行進行通訊,具有削峰填谷的作用。

因此該形式適用於不需要同步返回發起方執行最終結果、可以進行補償、對效能要求較高、不介意額外編碼的業務場景。

但當然SAGA也可以進行稍微改造,變成與TCC類似、可以進行資源預留的形態。

2PC事務

其適用於參與者較少,單個本地事務執行時間較少,並且參與者自身可用性很高的場景,否則,其很可能導致效能下降嚴重。

並非一種事務形態就能打遍天下

通過分析我們可以發現,並不存在一種事務形態能解決所有的問題,我們需要根據特定的業務場景選擇合適的事務形態。甚至於有時需要混合多種事務形態才能更好的完成目標,如 上面提到的 訂單、積分、錢包混合的場景:訂單的成功與否需要依賴於錢包的餘額,但不依賴於積分的多少,因此可以混合基於訊息的事務形態以加積分 及 基於補償的事務形態以確保扣錢成功,從而得到一個性能更好,編碼量更少的形態。

然而目前很多框架都專注於某單一方面的事務形態,如TCC單獨一個框架,可靠訊息單獨一個框架,SAGA單獨一個框架,他們各自獨立,容易導致以下問題:

  • 由於前期只採用了其中一種型別事務的框架,因為工具目前只有錘子,引入其他工具又涉及測試、閱讀程式碼等過程,因此把所有問題都看做釘子,導致效能偏低或者實現不夠優雅
  • 由於不同框架管理事務的形態可能不一致,導致不能很好的協調工作,如某一個TCC框架和另一個基於訊息的事務框架無法很好融合。

解決方案

為了解決上面提到的問題,EasyTransaction這個基於Spring的分散式事務框架,實現了上述除2PC以外的所有事務形態,並提供了統一的使用介面,完美地解決了以上的問題。其主要特性如下:

  • 一個框架包含多種事務形態,一個框架搞定所有型別的事務
  • 多種事務形態可混合使用
  • 高效能,若不啟用框架的冪等功能,對業務資料庫的額外消耗僅為寫入25位元組的一行
  • 可選的框架冪等實現(包括呼叫次序錯亂處理),大幅減輕業務開發工作量
  • 業務程式碼可實現完全無入侵
  • 支援巢狀事務
  • 無需額外部署協調者,不同APP的服務協調自身發起的事務
  • 分散式事務ID可關聯業務ID,業務型別,APPID,便於監控各個業務的分散式事務執行情況

若各位對ET興趣,可以到 https://github.com/QNJR-GROUP/EasyTransaction 檢視詳細介紹及示例,本文不再深入介紹

總結

不同業務場景應按需引入不同的事務形態,在條件允許的情況下,個人建議按照如下次序選擇對應的事務形態:

單機事務》基於訊息的事務》基於補償的事務》TCC事務

因SAGA事務的形態需要配合較為明顯的前端業務互動變更,個人建議在單一事務執行過程較長、存在較多子事務,並且無法使用基於訊息的事務形態時使用。