萬字長文漫談分佈式事務實現原理
1 分佈式事務場景
1.1 事務核心特性
在聊分佈式事務之前,我們先理清楚有關於 “事務” 的定義.
事務 Transaction,是一段特殊的執行程序,其需要具備如下四項核心性質:
上面四項核心要素被稱爲事務的 acid 四大特性. 當事務的影響範圍侷限在一個關係型數據庫範圍內時,很多時候上述四項性質是能夠水到渠成地得到實現的,但是倘若事務涉及修改的對象是跨數據庫甚至跨服務跨存儲組件時,這個問題就開始變得複雜且有趣了,也就正式邁入我們今天所重點探討的 “分佈式事務” 領域的問題.
1.2 分佈式事務場景問題
下面我們通過一個常見的場景問題引出有關於分佈式事務的話題.
假設我們在維護一個電商後臺系統,每當在處理一筆來自用戶創建訂單的請求時,需要執行兩步操作:
-
• 從賬戶系統中,扣減用戶的賬戶餘額
-
• 從庫存系統中,扣減商品的剩餘庫存
從業務流程上來說,這個流程需要保證具備事務的原子性,即兩個操作需要能夠一氣呵成地完成執行,要麼同時成功,要麼同時失敗,不能夠出現數據狀態不一致的問題,比如發生從用戶賬戶扣除了金額但商品庫存卻扣減失敗的問題.
然而從技術流程上來講,兩個步驟是相對獨立的兩個操作,底層涉及到的存儲介質也是相互獨立的,因此無法基於本地事務的實現方式.
因此,這本質上就是我們今天所要談及的分佈式事務問題.
分佈式事務的實現難度是很高的,但是不用慌,辦法總比困難多,在業界針對於分佈式事務早已提出一套被廣泛認可應用的解決方案,這部分內容將在本文第 2、3 章內容中詳細展開介紹. 在這裏,我們需要明確所謂分佈式事務的實現中,其中所謂的數據狀態一致性是需要做出妥協的:
-
• 在分佈式事務中,我們談到的 “數據狀態一致性”,指的是數據的最終一致性,而非數據的即時一致性,因爲即時一致性通常是不切實際的
-
• 沒有分佈式事務能夠保證數據狀態具備百分之百的一致性,根本原因就在於網絡環境和第三方系統的不穩定性
2 事務消息方案
首先,一類偏狹義的分佈式事務解決方案是基於消息隊列 MessageQueue(後續簡稱 MQ)實現的事務消息 Transaction Message.
2.1 RocketMQ 簡介
RocketMQ 是阿里基於 java 實現並託管於 apache 基金會的頂級開源消息隊列組件,其中事務消息 TX Msg 也是 RocketMQ 現有的一項能力. 本章將主要基於 RocketMQ 針對事務消息的實現思路展開介紹.
RocketMQ github 地址:https://github.com/apache/rocketmq
有關於 RocketMQ 中 TX Msg 的有關介紹,可以參見官方文檔:https://rocketmq.apache.org/docs/4.x/producer/06message5/
在本章中,我亦會根據自己的個人理解,對這個流程加以描述和潤色.
2.2 基於 MQ 實現分佈式事務
我們知道在 MQ 組件中,通常能夠爲我們保證的一項能力是:投遞到 MQ 中的消息能至少被下游消費者 consumer 消費到一次,即所謂的 at least once 語義.
基於此,MQ 組件能夠保證消息不會在消費環節丟失,但是無法解決消息的重複性問題. 因此,倘若我們需要追求精確消費一次的目標,則下游的 consumer 還需要基於消息的唯一鍵執行冪等去重操作,在 at least once 的基礎上過濾掉重複消息,最終達到 exactly once 的語義.
依賴於 MQ 中 at least once 的性質,我們簡單認爲,只要把一條消息成功投遞到 MQ 組件中,它就一定被下游 consumer 端消費端,至少不會發生消息丟失的問題.
倘若我們需要執行一個分佈式事務,事務流程中包含需要在服務 A 中執行的動作 I 以及需要在服務 B 中執行的動作 II,此時我們可以基於如下思路串聯流程:
-
• 以服務 A 作爲 MQ 生產方 producer,服務 B 作爲 MQ 消費方 consumer
-
• 服務 A 首先在執行動作 I,執行成功後往 MQ 中投遞消息,驅動服務 B 執行動作 II
-
• 服務 B 消費到消息後,完成動作 II 的執行
對上述流程進行總結,其具備如下優勢:
-
• 服務 A 和服務 B 通過 MQ 組件實現異步解耦,從而提高系統處理整個事務流程的吞吐量
-
• 當服務 A 執行 動作 I 失敗後,可以選擇不投遞消息從而熔斷流程,因此保證不會出現動作 II 執行成功,而動作 I 執行失敗的不一致問題
-
• 基於 MQ at least once 的語義,服務 A 只要成功消息的投遞,就可以相信服務 B 一定能消費到該消息,至少服務 B 能感知到動作 II 需要執行的這一項情報
-
• 依賴於 MQ 消費側的 ack 機制,可以實現服務 B 有限輪次的重試能力. 即當服務 B 執行動作 II 失敗後,可以給予 MQ bad ack,從而通過消息重發的機制實現動作 II 的重試,提高動作 II 的執行成功率
與之相對的,上述流程也具備如下幾項侷限性:
-
• 問題 1:服務 B 消費到消息執行動作 II 可能發生失敗,即便依賴於 MQ 重試也無法保證動作一定能執行成功,此時缺乏令服務 A 回滾動作 I 的機制. 因此很可能出現動作 I 執行成功,而動作 II 執行失敗的不一致問題
-
• 問題 2:在這個流程中,服務 A 需要執行的操作有兩步:(1)執行動作 I;(2)投遞消息. 這兩個步驟本質上也無法保證原子性,即可能出現服務 A 執行動作 I 成功,而投遞消息失敗的問題.
在本章談及的事務消息實現方案中:
-
• 針對問題 1 是無能爲力的,因爲這個問題本身就脫離於事務消息的領域範疇之外,需要放到第 3 章中通過另一類分佈式事務的實現方案加以解決.
-
• 而針對於問題 2 的解決思路,則正是本章中所需要重點探討的話題.
2.3 本地事務 + 消息投遞
2.2 小節中,聊到的服務 A 所要執行的操作分爲兩步:本地事務 + 消息投遞. 這裏我們需要如何保證這兩個步驟的執行能夠步調統一呢,下面不妨一起來推演一下我們的流程設計思路:
首先,這兩個步驟在流程中一定會存在一個執行的先後順序,我們首先來思考看看不同的組織順序可能會分別衍生出怎樣的問題:
- • 組合 I:先執行本地事務,後執行消息投遞
組合 I 的優勢:不會出現消息投遞成功而本地事務執行失敗的情況. 這是因爲在本地事務執行失敗時,可以主動熔斷消息投遞的動作.
組合 I 的劣勢:可能出現本地事務執行成功而消息投遞失敗的問題. 比如本地事務成功後,想要嘗試執行消息投遞操作時一直出現失敗,最終消息無法發出. 此時由於本地事務已經提交,要執行回滾操作會存在着很高的成本.
- • 組合 II:先執行消息投遞,後執行本地事務
組合 II 的優勢:不會出現本地事務執行成功而消息投遞失敗的問題. 因爲在消息投遞失敗時,可以不開啓本地事務的執行操作.
組合 II 的劣勢:可能出現消息投遞成功而本地事務執行失敗問題. 比如消息投遞成功後,本地事務始終無法成功執行,而消息一經發出,就已經覆水難收了.
捋完上述兩種流程中存在的問題後,一種比較容易想到的實現思路是:基於本地事務包裹消息投遞操作的實現方式,對應執行步驟如下:
-
• 首先 begin transaction,開啓本地事務
-
• 在事務中,執行本地狀態數據的更新
-
• 完成數據更新後,不立即 commit transaction
-
• 執行消息投遞操作
-
• 倘若消息投遞成功,則 commit transaction
-
• 倘若消息投遞失敗,則 rollback transaction
這個流程乍一看沒啥毛病,重複利用了本地事務回滾的能力,解決了本地修改操作成功、消息投遞失敗後本地數據修正成本高的問題.
然而,這僅僅是表現. 上述流程實際上是經不住推敲的,其中存在三個致命問題:
-
• 在和數據庫交互的本地事務中,夾雜了和第三方組件的 IO 操作,可能存在引發長事務的風險
-
• 執行消息投遞時,可能因爲超時或其他意外原因,導致出現消息在事實上已投遞成功,但 producer 獲得的投遞響應發生異常的問題,這樣就會導致本地事務被誤回滾的問題
-
• 在執行事務提交操作時,可能發生失敗. 此時事務內的數據庫修改操作自然能夠回滾,然而 MQ 消息一經發出,就已經無法回收了.
2.4 事務消息原理
2.3 小節聊完後,我猜測大家心頭多少產生一點點窒息感,同時也對我們今天所探討的分佈式事務這個話題產生了更充分的敬畏之心.
下面,我們就正式開始介紹解決這個問題的正解——事務消息 Transaction Message.
我們以 RocketMQ 中 TX Msg 的實現方案爲例展開介紹. 首先拋出結論,TX Msg 能保證我們做到在本地事務執行成功的情況下,後置的投遞消息操作能以接近百分之百的概率被髮出. 其實現的核心流程爲:
-
• 生產方 producer 首先向 RocketMQ 生產一條半事務消息,此消息處於中間態,會暫存於 RocketMQ 不會被立即發出
-
• producer 執行本地事務
-
• 如果本地事務執行成功,producer 直接提交本地事務,並且向 RocketMQ 發出一條確認消息
-
• 如果本地事務執行失敗,producer 向 RocketMQ 發出一條回滾指令
-
• 倘若 RocketMQ 接收到確認消息,則會執行消息的發送操作,供下游消費者 consumer 消費
-
• 倘若 RocketMQ 接收到回滾指令,則會刪除對應的半事務消息,不會執行實際的消息發送操作
-
• 此外,在 RocketMQ 側,針對半事務消息會有一個輪詢任務,倘若半事務消息一直未收到來自 producer 側的二次確認,則 RocketMQ 會持續主動詢問 producer 側本地事務的執行狀態,從而引導半事務消息走向終態
在 TX Msg 的實現流程中,能夠保證 2.3 小節中談及的各種 badcase 都能被很好地消化:
-
• 倘若本地事務執行失敗,則 producer 會向 RocketMQ 發出刪除半事務消息的回滾指令,因此保證消息不會被髮出
-
• 倘若本地事務執行成功, 則 producer 會向 RocketMQ 發出事務成功的確認指令,因此消息能夠被正常發出
-
• 倘若 producer 端在發出第二輪的確認或回滾指令前發生意外狀況,導致第二輪結果指令確實. 則 RocketMQ 會基於自身的輪詢機制主動詢問本地事務的執行狀況,最終幫助半事務消息推進進度.
RocketMQ 中半事務消息輪詢流程示意如下:
最後,我們再回過頭把 RocketMQ TX Msg 的使用交互流程總結梳理如下:
2.5 事務消息侷限性
在本章一開始時,我就有提到,事務消息 TX Msg 只是一類偏狹義的分佈式事務解決方案. 因此我們在使用這項能力時,需要在心中需要有一個明確的認知和預期.
現在我們就來總結梳理一下,TX Msg 中存在的幾項侷限性:
-
• 流程高度抽象: TX Msg 把流程抽象成本地事務 + 投遞消息兩個步驟. 然而在實際業務場景中,分佈式事務內包含的步驟數量可能很多,因此就需要把更多的內容更重的內容糅合在所謂的 “本地事務” 環節中,上游 producer 側可能會存在比較大的壓力
-
• 不具備逆向回滾能力: 倘若接收消息的下游 consumer 側執行操作失敗,此時至多隻能依賴於 MQ 的重發機制通過重試動作的方式提高執行成功率,但是無法從根本上解決下游 consumer 操作失敗後回滾上游 producer 的問題. 這一點正是 TX Msg 中存在的最大的侷限性.
關於上面第二點,我們再展開談幾句. 我們知道,並非所有動作都能通過簡單的重試機制加以解決.
打個比方,倘若下游是一個庫存管理系統,而對應商品的庫存在事實上已經被扣減爲 0,此時無論重試多少次請求都是徒然之舉,這就是一個客觀意義上的失敗動作.
而遵循正常的事務流程,後置操作失敗時,我們應該連帶前置操作一起執行回滾,然而這部分能力在 TX Msg 的主流程中並沒有予以體現.
要實現這種事務的逆向回滾能力,就必然需要構築打通一條由下游逆流而上回調上游的通道,這一點並不屬於 TX Msg 探討的範疇.
我相信,能跟着我的思路一起走到這裏的同學,一定都是對技術抱着主動求知、精益求精的積極態度,針對於 TX Msg 中存在的問題,心中一定是迫切地希望能尋求到一個合適的解決方案. 那麼請大家不用着急,有關於這部分內容,我們就在第 3 章內容中見分曉.
3 TCC 實現方案
3.1 TCC 概念簡述
TCC,全稱 Try-Confirm-Cancel,指的是將一筆狀態數據的修改操作拆分成兩個階段:
-
• 第一個階段是 Try,指的是先對資源進行鎖定,資源處於中間態但不處於最終態
-
• 第二個階段分爲 Confirm 和 Cancel,指的是在 Try 操作的基礎上,真正提交這次修改操作還是回滾這次變更操作
更多有關於 TCC 的概念介紹,可以參見這篇文檔:https://www.bytesoft.org/tcc-intro/ . 個人覺得內容還是比較優質.
3.2 TCC 宏觀架構
下面是 TCC 分佈式事務實現方案的整體架構,大家可以先整體瀏覽一下存個印象,下面我們會逐一展開介紹:
在 TCC 分佈式事務架構中,包含三類角色:
-
• 應用方 Application:指的是需要使用到分佈式事務能力的應用方,即這套 TCC 框架服務的甲方
-
• TCC 組件 TCC Component:指的是需要完成分佈式事務中某個特定步驟的子模塊. 這個模塊通常負責一些狀態數據的維護和更新操作,需要對外暴露出 Try、Confirm 和 Cancel 三個 API:
-
• Try:鎖定資源,通常以類似【凍結】的語義對資源的狀態進行描述,保留後續變化的可能性
-
• Confirm:對 Try 操作進行二次確認,將記錄中的【凍結】態改爲【成功】態
-
• Cancel:對 Try 操作進行回滾,將記錄中的【凍結】狀消除或者改爲【失敗】態. 其底層對應的狀態數據會進行回滾
-
• 事務協調器 TX Manager:負責統籌分佈式事務的執行:
-
• 實現 TCC Component 的註冊管理功能
-
• 負責和 Application 交互,提供分佈式事務的創建入口,給予 Application 事務執行結果的響應
-
• 串聯 Try -> Confirm/Cancel 的兩階段流程. 在第一階段中批量調用 TCC Component 的 Try 接口,根據其結果,決定第二階段是批量調用 TCC Component 的 Confirm 接口還是 Cancel 接口
3.3 TCC 案例分析
前面我們大致捋了一遍 TCC 的整體架構,但幹聊概念還是過於抽象,理解認知上不夠形象和立體,下面我們引入一個具體的分佈式事務場景問題,並通過 TCC 架構加以實現,幫助大家進一步提高對 TCC 分佈式事務方案的感性認識.
現在假設我們需要維護一個電商後臺系統,需要處理來自用戶的支付請求. 每當有一筆支付請求到達,我們需要執行下述三步操作,並要求其前後狀態保持一致性:
-
• 在訂單模塊中,創建出這筆訂單流水記錄
-
• 在賬戶模塊中,對用戶的賬戶進行相應金額的扣減
-
• 在庫存模塊中,對商品的庫存數量進行扣減
上面這三步操作分別需要對接訂單、賬戶、庫存三個不同的子模塊,底層的狀態數據是基於不同的數據庫和存儲組件實現的,並且我們這套後臺系統是基於當前流行的微服務架構實現的,這三子個模塊本身對應的就是三個相互獨立的微服務,因此如何實現在一筆支付請求處理流程中,使得這三筆操作對應的狀態數據始終保持高度一致性,就成了一個非常具有技術挑戰性的問題.
下面我們就擼起袖子開整.
首先,我們基於 TCC 的設計理念,將訂單模塊、賬戶模塊、庫存模塊分別改造成三個 TCC Component,每個 Component 對應需要暴露出 Try、Confirm、Cancel 三個 API,對應於凍結資源、確認更新資源、回滾解凍資源三個行爲.
同時,爲了能夠簡化後續 TX Manager 和 Application 之間的交互協議,每個 TCC Component 會以插件的形式提前註冊到 TX Manager 維護的組件市場 Component Market 中,並提前聲明好一個全局唯一鍵與之進行映射關聯.
由於每個 TCC Component 需要支持 Try 接口的鎖定操作,因此其中維護的數據需要在明細記錄中拆出一個用於標識 “凍結” 狀態的標籤,或者在狀態機中拆出一個 “凍結” 狀態.
最終在第二階段的 Confirm 或者 Cancel 請求到達時,再把 ”凍結 “狀態調整爲” 成功 “ 或者 ” 失敗“ 的終態.
下面描述一下,基於 TCC 架構實現後,對應於一次支付請求的分佈式事務處理流程:
-
• Application 調用 TX Manager 的接口,創建一輪分佈式事務:
-
• Application 需要向 TX Manager 聲明,這次操作涉及到的 TCC Component 範圍,包括 訂單組件、賬戶組件和庫存組件
-
• Application 需要向 TX Manager 提前傳遞好,用於和每個 TCC Component 交互的請求參數( TX Manager 調用 Component Try 接口時需要傳遞)
-
• TX Manager 需要爲這筆新開啓的分佈式事務分配一個全局唯一的事務主鍵 Transaction ID
-
• TX Manager 將這筆分佈式事務的明細記錄添加到事務日誌表中
-
• TX Manager 分別調用訂單、賬戶、庫存組件的 Try 接口,試探各個子模塊的響應狀況,比並嘗試鎖定對應的資源
-
• TX Manager 收集每個 TCC Component Try 接口的響應結果,根據結果決定下一輪的動作是 Confirm 還是 Cancel
-
• 倘若三筆 Try 請求中,有任意一筆未請求成功:
-
• TX Manager 給予 Application 事務執行失敗的 Response
-
• TX Manager 批量調用訂單、賬戶、庫存 Component 的 Cancel 接口,回滾釋放對應的資源
-
• 在三筆 Cancel 請求都響應成功後,TX Manager 在事務日誌表中將這筆事務記錄置爲【失敗】狀態
-
• 倘若三筆 Try 請求均響應成功了:
-
• TX Manager 給予 Application 事務執行成功的 ACK
-
• TX Manager 批量調用訂單、賬戶、庫存 Component 的 Confirm 接口,使得對應的變更記錄實際生效
-
• 在三筆 Confirm 請求都響應成功後,TX Manager 將這筆事務日誌置爲【成功】狀態
在上述流程中,有一個很重要的環節需要補充說明:
首先,TCC 本質上是一個兩階段提交(Two Phase Commitment Protocol,2PC)的實現方案,分爲 Try 和 Confirm/Cancel 的兩個階段:
-
• Try 操作的容錯率是比較高的,原因在於有人幫它兜底. Try 只是一個試探性的操作,不論成功或失敗,後續可以通過第二輪的 Confirm 或 Cancel 操作對最終結果進行修正
-
• Confirm/Cancel 操作是沒有容錯的,倘若在第二階段出現問題,可能會導致 Component 中的狀態數據被長時間” 凍結 “或者數據狀態不一致的問題
針對於這個場景,TCC 架構中採用的解決方案是:在第二階段中,TX Manager 輪詢重試 + TCC Component 冪等去重. 通過這兩套動作形成的組合拳,保證 Confirm/ Cancel 操作至少會被 TCC Component 執行一次.
首先,針對於 TX Manager 而言:
-
• 需要啓動一個定時輪詢任務
-
• 對於事務日誌表中,所有未被更新爲【成功 / 失敗】對應終態的事務,需要摘出進行檢查
-
• 檢查時查看其涉及的每個組件的 Try 接口的響應狀態以及這筆事務的持續時長
-
• 倘若事務應該被置爲【失敗】(存在某個 TCC Component Try 接口請求失敗),但狀態卻並未更新,說明之前批量執行 Cancel 操作時可能發生了錯誤. 此時需要補償性地批量調用事務所涉及的所有 Component 的 Cancel 操作,待所有 Cancel 操作都成功後,將事務置爲【失敗】狀態
-
• 倘若事務應該被置爲【成功】(所有 TCC Component Try 接口均請求成功),但狀態卻並未更新,說明之前批量執行 Confirm 操作時可能發生了錯誤. 此時需要補償性地批量調用事務所涉及的所有 Component 的 Confirm 操作,待所有 Confirm 操作都成功後,將事務置爲【成功】狀態
-
• 倘若事務仍處於【進行中】狀態(TCC Component Try 接口請求未出現失敗,但並非所有 Component Try 接口都請求成功),則檢查事務的創建時間,倘若其耗時過長,同樣需要按照事務失敗的方式進行處理
需要注意,在 TX Manager 輪詢重試的流程中,針對下游 TCC Component 的 Confirm 和 Cancel 請求只能保證 at least once 的語義,換句話說,這部分請求是可能出現重複的.
因此,在下游 TCC Component 中,需要在接收到 Confirm/Cancel 請求時,執行冪等去重操作. 冪等去重操作需要有一個唯一鍵作爲去重的標識,這個標識鍵就是 TX Manager 在開啓事務時爲其分配的全局唯一的 Transaction ID,它既要作爲這項事務在事務日誌表中的唯一鍵,同時在 TX Manager 每次向 TCC Component 發起請求時,都要攜帶上這筆 Transaction ID.
3.4 TX Manager 職責
通過上面這個實際案例的流程剖析後,相信大家已經對這套 TCC 架構有了一定的理解. 接下來我們再從 TX Manager 和 TCC Component 職責領域劃分的視角出發,進行一輪梳理.
首先針對於事務協調器 TX Manager,其核心要點包括:
-
• 暴露出註冊 TCC Component 的接口,進行 Component 的註冊和管理
-
• 暴露出啓動分佈式事務的接口,作爲和 Application 交互的唯一入口,並基於 Application 事務執行結果的反饋
-
• 爲每個事務維護全局唯一的 Transaction ID,基於事務日誌表記錄每項分佈式事務的進展明細
-
• 串聯 Try——Confirm/Cancel 的兩階段流程,根據 Try 的結果,推進執行 Confirm 或 Cancel 流程
-
• 持續運行輪詢檢查任務,推進每個處於中間態的分佈式事務流轉到終態
3.5 TCC Component 職責
對於 TCC Component 而言,其需要關心和處理的工作包括:
-
• 暴露出 Try、Confirm、Cancel 三個入口,對應於 TCC 的語義
-
• 針對數據記錄,新增出一個對應於 Try 操作的中間狀態枚舉值
-
• 針對於同一筆事務的重複請求,需要執行冪等性校驗
-
• 需要支持空回滾操作. 即針對於一筆新的 Transaction ID,在沒收到 Try 的前提下,若提前收到了 Cancel 操作,也需要將這個信息記錄下來,但不需要對真實的狀態數據發生變更
下面針對最後一點提到的空回滾操作,進一步加以說明:
這個空回滾機制本質上是爲了解決 TCC 流程中出現的懸掛問題,下面我們舉個具體例子加以說明:
-
• TX Manager 在向 Component A 發起 Try 請求時,由於出現網絡擁堵,導致請求超時
-
• TX Manager 發現存在 Try 請求超時,將其判定爲失敗,因此批量執行 Component 的 Cancel 操作
-
• Component A 率先收到了後發先至的 Cancel 請求
-
• 過了一會兒,之前阻塞在網絡鏈路中的 Try 請求也到達了 Component A
從執行邏輯上,Try 應該先於 Cancel 到達和處理,然而在事實上,由於網絡環境的不穩定性,請求到達的先後次序可能顛倒. 在這個場景中,Component A 需要保證的是,針對於同一筆事務,只要接受過對應的 Cancel 請求,之後到來的 Try 請求需要被忽略. 這就是 TCC Component 需要支持空回滾操作的原因所在.
3.6 TCC 優劣勢分析
最後我們針對 TCC 分佈式事務實現方案的優劣勢進行分析:
優勢:
-
• TCC 可以稱得上是真正意義上的分佈式事務:任意一個 Component 的 Try 操作發生問題,都能支持事務的整體回滾操作
-
• TCC 流程中,分佈式事務中數據的狀態一致性能夠趨近於 100%,這是因爲第二階段 Confirm/Cancel 的成功率是很高的,原因在於如下三個方面:
-
• TX Manager 在此前剛和 Component 經歷過一輪 Try 請求的交互並獲得了成功的 ACK,因此短時間內,Component 出現網絡問題或者自身節點狀態問題的概率是比較小的
-
• TX Manager 已經通過 Try 操作,讓 Component 提前鎖定了對應的資源,因此確保了資源是充分的,且由於執行了狀態鎖定,出現併發問題的概率也會比較小
-
• TX Manager 中通過輪詢重試機制,保證了在 Confirm 和 Cancel 操作執行失敗時,也能夠通過重試機制得到補償
劣勢:
-
• TCC 分佈式事務中,涉及的狀態數據變更只能趨近於最終一致性,無法做到即時一致性
-
• 事務的原子性只能做到趨近於 100%,而無法做到真正意義上的 100%,原因就在於第二階段的 Confirm 和 Cancel 仍然存在極小概率發生失敗,即便通過重試機制也無法挽救. 這部分小概率事件,就需要通過人爲介入進行兜底處理
-
• TCC 架構的實現成本是很高的,需要所有子模塊改造成 TCC 組件的格式,且整個事務的處理流程是相對繁重且複雜的. 因此在針對數據一致性要求不那麼高的場景中,通常不會使用到這套架構.
事實上,上面提到的第二點劣勢也並非是 TCC 方案的缺陷,而是所有分佈式事務都存在的問題,由於網絡請求以及第三方系統的不穩定性,分佈式事務永遠無法達到 100% 的原子性.
4 總結
本期和大家一起分享了事務消息和 TCC 事務兩種分佈式事務實現方案的技術原理:
-
• Transaction Message:能夠支持狹義的分佈式事務. 基於消息隊列組件中半事務消息以及輪詢檢查機制,保證了本地事務和消息生產兩個動作的原子性,但不具備事務的逆向回滾能力
-
• TCC Transaction:能夠支持廣義的分佈式事務. 架構中每個模塊需要改造成實現 Try/Confirm/Cancel 能力的 TCC 組件,通過事務協調器進行全局 Try——Confirm/Cancel 兩階段流程的串聯,保證數據的最終一致性趨近於 100%
我個人一直秉持的觀點是——理論需要得到實踐的檢驗. 今天咱們看似聊了很多,但都是偏抽象和空洞的原理性內容. 這裏咱們提前做個預告,接下來我會基於今天聊到的這套 TCC 分佈式事務的技術方案進行落地實踐,基於 Golang 從零到一搭建出一個 TCC 分佈式事務框架,敬請期待!
目前基於 Golang 實現的 TCC 框架已於 github 開源:https://github.com/xiaoxuxiansheng/gotcc 大家走過路過,留個 star,不勝感激!
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/yXl-LpuwwIbGMcHJDgx0Gw