分佈式事務的概念論述和方案總結

1 分佈式事務的概念

事務在分佈式計算領域也得到了廣泛的應用。在單機數據庫中,我們很容易能夠實現一套滿足ACID特性的事務處理系統,但是在分佈式數據庫中,數據分散在各臺不同的機器上,如何對這些數據進行分佈式事務處理具有非常大的挑戰。

分佈式事務的分佈式,是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位於分佈式系統的不同節點之上。通常一個分佈式事務會涉及對多個數據源或業務系統的操作。

一個最典型的分佈式事務場景是跨行的轉賬操作。該操作涉及調用兩個異地的銀行服務。其中一個是本地銀行提供的取款服務,另一個是目標銀行提供的存款服務,這兩個服務本身是無狀態且相互獨立的,共同構成了一個完整的分佈式事務。取款和存款兩個步驟要麼都執行,要麼都不執行。否則,如果從本地銀行取款成功,但是因爲某種原因存款服務失敗了,那麼必須回滾到取款之前的狀態,否則就會導致數據不一致。

從上面的例子可以看出,一個分佈式事務可以看作是由多個分佈式操作序列組成的,例如上面例子中的取款服務和存款服務,通常可以把這一系列分佈式的操作序列稱爲子事務。由於分佈式事務中,各個子事務的執行是分佈式的,因此要實現一種能夠保證ACID特性的分佈式事務處理系統就顯得格外複雜。

分佈式事務=分佈式+事務,這是分佈式事務本身最直觀,也最重要的標籤。我們要想理解分佈式事務的理論基礎,就要首先從這兩個角度來解讀:

1.1 分佈式事務是個事務

首先,分佈式事務是個事務,既然是事務,那麼我們會希望它能夠滿足傳統事務的ACID四個特性:

1.1.1 傳統事務要擁有ACID特性

  • Atomic(原子性)

    • 事務的原子性是指事務必須是一個原子的操作序列單元。事務中包含的各項操作在一次執行過程中,要麼全部執行,要麼全部不執行。
    • 任何一項操作失敗都將導致整個事務失敗,同時其他已經被執行的操作都將被撤銷並回滾。只有所有的操作全部成功,整個事務纔算是成功完成。
  • Consistency(一致性)

    • 事務的一致性是指事務的執行不能破壞數據庫數據的完整性和一致性,一個事務在執行前後,數據庫都必須處於一致性狀態。換句話說,事務的執行結果必須是使數據庫從一個一致性狀態轉變到另一個一致性狀態。
    • 假設銀行的轉賬操作就是一個事務。假設A和B原來賬戶都有100元。此時A轉賬給B50元,轉賬結束後,應該是A賬戶減去50元變成50元,B賬戶增加50元變成150元。A、B的賬戶總和還是200元。轉賬前後,數據庫就是從一個一致性狀態(A100元,B100元,A、B共200元)轉變到另一個一致性狀態(A50元,B150元,A、B共200元)。假設轉賬結束後只扣了A賬戶,沒有增加B賬戶,這時數據庫就處於不一致的狀態。
  • Isolation(隔離性)

    • 事務的隔離性是指在併發環境中,併發的事務是相互隔離的,事務之間互不干擾。
    • 在標準的SQL規範中,定義的4個事務隔離級別,不同隔離級別對事務的處理不同。4個隔離級別分別是:讀未提交、讀已提交、可重複讀和串行化。

    • 事務隔離級別越高,就越能保證數據的完整性和一致性,但同時對併發性能的影響也越大。
    • 通常,對於絕大多數的應用來說,可以優先考慮將數據庫系統的隔離級別設置爲授權讀取,這能夠在避免髒讀的同時保證較好的併發性能。儘管這種事務隔離級別會導致不可重複讀、幻讀和第二類丟失更新等併發問題,但較爲科學的做法是在可能出現這類問題的個別場合中,由應用程序主動採用悲觀鎖或樂觀鎖來進行事務控制。
  • Durability(持久性)

    • 事務的持久性又稱爲永久性,是指一個事務一旦提交,對數據庫中對應數據的狀態變更就應該是永久性的。即使發生系統崩潰或機器宕機等故障,只要數據庫能夠重新啓動,那麼一定能夠將其恢復到事務成功結束時的狀態。

1.2 分佈式事務是分佈式的

其次,分佈式事務是分佈式的,既然是分佈式的系統,那麼它必然無可避免的要收到CAP理論的約束:

1.2.1 分佈式系統要受CAP理論約束

CAP理論:一個分佈式系統不可能同時滿足一致性(C:Consistency)、可用性(A:Availability)和分區容錯性(P:Partition tolerance)這三個基本要求,最多隻能滿足其中的兩項。

  • 一致性
    • 在分佈式環境中,一致性是指數據在多個副本之間是否能夠保持一致的特性(這點跟ACID中的一致性含義不同)。
    • 對於一個將數據副本分佈在不同節點上的分佈式系統來說,如果對第一個節點的數據進行了更新操作並且更新成功後,卻沒有使得第二個節點上的數據得到相應的更新,於是在對第二個節點的數據進行讀取操作時,獲取的依然是更新前的數據(稱爲髒數據),這就是典型的分佈式數據不一致情況。
    • 在分佈式系統中,如果能夠做到針對一個數據項的更新操作執行成功後,所有的用戶都能讀取到最新的值,那麼這樣的系統就被認爲具有強一致性(或嚴格的一致性)。
  • 可用性
    • 可用性是指系統提供的服務必須一直處於可用的狀態,對於用戶的每一個操作請求總是能夠在有限的時間內返回結果,如果超過了這個時間範圍,那麼系統就被認爲是不可用的。
    • 『有限的時間內』是一個在系統設計之初就設定好的運行指標,不同的系統會有很大的差別。比如對於一個在線搜索引擎來說,通常在0.5秒內需要給出用戶搜索關鍵詞對應的檢索結果。而對應Hive來說,一次正常的查詢時間可能在20秒到30秒之間。
    • 『返回結果』是可用性的另一個非常重要的指標,它要求系統在完成對用戶請求的處理後,返回一個正常的響應結果。正常的響應結果通常能夠明確地反映出對請求的處理結果,及成功或失敗,而不是一個讓用戶感到困惑的返回結果。
    • 讓我們再來看看上面提到的在線搜索引擎的例子,如果用戶輸入指定的搜索關鍵詞後,返回的結果是一個系統錯誤,比如"OutOfMemoryErroe"或"System Has Crashed"等提示語,那麼我們認爲此時系統是不可用的。
  • 分區容錯性
    • 分區容錯性要求一個分佈式系統需要具備如下特性:分佈式系統在遇到任何網絡分區故障的時候,仍然能夠保證對外提供滿足一致性和可用性的服務,除非是整個網絡環境都發生了故障。
    • 網絡分區是指在分佈式系統中,不同的節點分佈在不同的子網絡(機房或異地網絡等)中,由於一些特殊的原因導致這些子網絡之間出現網絡不連通的狀況,但各個子網絡的內部網絡是正常的,從而導致整個系統的網絡環境被切分成了若干個孤立的區域。

需要明確的一點是:對於一個分佈式系統而言,分區容錯性可以說是一個最基本的要求。因爲既然是一個分佈式系統,那麼分佈式系統中的組件必然需要被部署到不同的節點,否則也就無所謂的分佈式系統了,因此必然出現子網絡。

而對於分佈式系統而言,網絡問題又是一個必定會出現的異常情況,因此分區容錯性也就成爲了一個分佈式系統必然需要面對和解決的問題。因此係統架構師往往需要把精力花在如何根據業務特點在C(一致性)和A(可用性)之間尋求平衡

比如Cassandra、Dynamo等中間件,他們的實現默認優先選擇AP,弱化C;

而HBase、MongoDB等中間件,他們的實現默認優先選擇CP,弱化A。

1.2.2 一致性和可用性權衡的總結——BASE理論

BASE是Basically Available(基本可用)、Soft state(軟狀態)和Eventually consistent(最終一致性)三個短語的簡寫,由eBay架構師Dan Pritchett提出的,是對CAP中一致性和可用性權衡的結果,其來源於對大規模互聯網分佈式系統實踐的總結,是基於CAP定律逐步演化而來。

BASE理論核心思想是:即使無法做到強一致性,但每個應用都可以根據自身業務特點,採用適當的方式來使系統達到最終一致性

  • 基本可用
    • 基本可用是指分佈式系統在出現不可預知故障的時候,允許損失部分可用性——但請注意,這絕不等價於系統不可用。比如
      • 響應時間上的損失:正常情況下,一個在線搜索引擎需要在0.5秒之內返回給用戶相應的查詢結果,但由於出現故障(比如系統部分機房發生斷電或斷網故障),查詢結果的響應時間增加到了1~2秒。
      • 功能上的損失:正常情況下,在一個電子商務網站(比如淘寶)上購物,消費者幾乎能夠順利地完成每一筆訂單。但在一些節日大促購物高峯的時候(比如雙十一、雙十二),由於消費者的購物行爲激增,爲了保護系統的穩定性(或者保證一致性),部分消費者可能會被引導到一個降級頁面
  • 弱狀態
    • 弱狀態是指允許系統中的數據存在中間狀態,並認爲該中間狀態的存在不會影響系統的整體可用性,即允許系統在不同的數據副本之間進行數據同步的過程存在延時。
  • 最終一致性
    • 最終一致性強調的是系統中所有的數據副本,在經過一段時間的同步後,最終能夠達到一個一致的狀態。因此,最終一致性的本質是需要系統保證最終數據能夠達到一致,而不需要實時保證系統數據的強一致性。
    • 最終一致性是一種特殊的弱一致性:系統能夠保證在沒有其他新的更新操作的情況下,數據最終一定能夠達到一致的狀態,因此所有客戶端對系統的數據訪問都能夠獲取到最新的值。同時,在沒有發生故障的前提下,數據到達一致狀態的時間延遲,取決於網絡延遲、系統負載和數據複製方案設計等因素。
    • 在實際工程實踐中,最終一致性存在以下五類主要的變種:
      • 因果一致性(Causal consistency)
        • 如果進程A通知進程B它已更新了一個數據項,那麼進程B的後續訪問將返回更新後的值,且一次寫入將保證取代前一次寫入。與進程A無因果關係的進程C的訪問遵守一般的最終一致性規則。
      • 讀己之所寫(Read your writes)
        • 當進程A自己更新一個數據項之後,它總是訪問到更新過的值,絕不會看到舊值。這是因果一致性模型的一個特例。
      • 會話一致性(Session consistency)
        • 這是上一個模型的實用版本,它把訪問存儲系統的進程放到會話的上下文中。只要會話還存在,系統就保證“讀己之所寫”一致性。如果由於某些失敗情形令會話終止,就要建立新的會話,而且系統的保證不會延續到新的會話。
      • 單調讀一致性(Monotonic read consistency)
        • 如果某個進程已經看到過數據對象的某個值,那麼該進程任何後續訪問都不會返回在那個值之前的值。
      • 單調寫一致性(Monotonic write consistency)
        • 系統保證來自同一個進程的寫操作順序執行。要是系統不能保證這種程度的一致性,就非常難以編程了。

    以上就是最終一致性的五種常見的變種,在實際系統實踐中,可以將其中的若干個變種互相結合起來,以構建一個具有最終一致性特性的分佈式系統。 事實上,最終一致性並不是只有那些大型分佈式系統才涉及的特性,許多現代的關係型數據庫都採用了最終一致性模型。在現代關係型數據庫中(比如MySQL和PostgreSQL),大多都會採用同步或異步方式來實現主備數據複製技術。在同步方式中,數據的複製過程通常是更新事務的一部分,因此在事務完成後,主備數據庫的數據就會達到一致。而在異步方式中,備庫的更新往往會存在延時,這取決於事務日誌在主備數據庫之間傳輸的時間長短。如果傳輸時間過長或者甚至在日誌傳輸過程中出現異常導致無法及時將事務應用到備庫上,那麼很顯然,從備庫中讀取的數據將是舊的,因此就出現了數據不一致的情況。
    當然,無論是採用多次重試還是人爲數據訂正,關係型數據庫還是能夠保證最終數據達到一致,這就是系統提供最終一致性保證的經典案例。

1.3 ACID和CAP妥協下的柔性事務

可以看到,ACID特性和CAP理論,在關於一致性問題上都有論述,只不過

  • ACID中的C論述的是:一個事務在執行前後,數據庫的數據都必須處於一致性狀態,如轉賬過程,金錢總量應該保持不變。
  • CAP中的C論述的是:同一個數據在多個分佈式副本之間是否能夠保持一致,如某個用戶的餘額,在各個副本之間值應該一致。

我們需要注意到他們論述的點其實是不同的。

同時,我們還要注意到,雖然分佈式系統受限於CAP理論而時常要在A和C中做取捨,但對於分佈式事務系統來說,C的重要性是高於A的,故而市面上成熟的分佈式事務解決方案,都是在努力事務ACID特性的基礎上,儘量在分佈式的情況下(也就是滿足分區容錯性的情況下)達到較好的數據一致性。

我們一般來說,根據數據一致性的實效,以及ACID/CAP取捨的類型,可將事務分爲:

  1. 剛性事務:遵循ACID原則,強一致性。本地事務,基本都是剛性事務。
  2. 柔性事務:遵循BASE理論,最終一致性;與剛性事務不同,柔性事務允許一定時間內,不同節點的數據不一致,但要求最終一致。

受限於分佈式的侷限,分佈式事務的實現目前都是柔性事務,換句話說,我們還無法實現完全滿足ACID強一致性的分佈式事務

2 分佈式事務的解決方案

經過上文的論述,我們有了一定的理論基礎,明確了我們希望的分佈式事務應該是什麼樣的。我們往往爲了可用性和分區容錯性,忍痛放棄強一致支持的剛性事務,轉而追求最終一致性的柔性事務。

那麼如何實現能夠基本滿足ACID特性和CAP理論的分佈式事務呢?我們接下來介紹幾種成熟的柔性事務實現。

  1. XA協議:更偏向於在數據庫層面解決數據庫之間的分佈式事務
    • 1.1 2PC(兩段式提交)
    • 1.2 3PC(三段式提交)
  2. TCC兩階段補償型事務:更偏向於在應用層面解決分佈式系統中的補償形分佈式事務
  3. 最大努力通知:最簡單的一種柔性事務,適用於一些最終一致性時間敏感度低,且被動方處理結果不影響主動方的處理結果的業務。
  4. 本地消息表:將分佈式事務拆分成本地事務進行處理的一種思路
  5. 半消息/最終一致性(RocketMQ)

TCC、Saga、事務消息、最大努力事務

2.1 XA協議

在分佈式系統中,每個節點都能明確知道自身事務操作結果,但無法直接獲取到其他分佈式節點的操作結果。所以當一個事務要橫跨多個節點時,爲了保證事務處理的ACID特性而引入了協調者組件來統一調度所有分佈式節點(參與者)的執行邏輯,協調者調度參與者的行爲並最終決定是否把參與者的事務進行真正的提交。

XA協議是體現和貫徹協調者角色的一種很經典分佈式事務協議,由Tuxedo提出,XA的目的是保證分佈式事務的ACID特性,就像本地事務一樣。

XA大致分爲兩部分:事務管理器(協調者角色)和本地資源管理器。其中本地資源管理器往往由數據庫實現,比如Oracle、DB2這些商業數據庫都實現了XA接口,而事務管理器作爲全局的調度者,負責各個本地資源的提交和回滾。

XA協議爲了保證分佈式事務能夠在保持ACID特性的同時保證分佈式系統之間的數據一致性,提供了兩種分佈式事務的實現:2PC和3PC協議。

2.1.1 2PC

2.1.1.1 簡介

  • 2PC(Two-Phase Commit 兩階段提交):完成參與者的協調,統一決定事務的提交或回滾,使基於分佈式系統架構下的所有節點在進行事務處理過程中能夠保持原子性和數據一致性。
  • 目前絕大部分的關係型數據庫都是採用二階段提交協議來完成分佈式事務處理的。

2.1.1.2 協議內容

  1. 投票,嘗試讓協調者們提交事務

    • 事務詢問:協調者向所有參與者發送事務內容,詢問是否可以執行事務提交操作,等待響應
    • 執行事務:參與者節點執行事務操作,並記錄Undo和Redo信息到事務日誌
    • 參與者響應:若參與者成功執行事務,則向協調者反饋Yes響應,否則反饋No響應
  2. 根據協調者反饋決定事務執行結果 21. 如果所有參與者的反饋都是Yes響應,那麼執行事務提交 - 發送提交請求:協調者向所有參與者發送Commit請求 - 事務提交:參與者接受到Commit請求後執行事務提交操作並釋放佔用的事務資源 - 反饋事務提交結果:參與者完成事務提交後向協調者發送Ack消息 - 完成事務:協調者收到所有參與者的Ack響應後,完成事務提交

    1. 如果任何一個參與者返回了N響應或者協調者等待超時後就會中斷事務
      • 發送回滾請求:協調者向所有參與者發送Rollback請求
      • 事務回滾:參與者受到請求後通過Undo信息執行事務回滾操作並釋放佔用的事務資源
      • 反饋事務回滾結果:參與者回滾事務後向協調者發送Ack消息
      • 中斷事務:協調者接收到所有參與者的Ack響應後,完成事務中斷

2.1.1.3 優缺點

  • 優點
    • 原理簡單,實現方便,有許多現成的實現框架
  • 缺點
    • 同步阻塞:在階段二事務提交過程中,所有參與者的操作邏輯都處於阻塞狀態,等待其他參與者響應,協調者請求
    • 單點問題:一旦協調者出現問題,階段二提交流程無法運轉,並且參與者會一直處於鎖定事務資源的狀態,無法繼續事務操作
    • 太過保守:任何一個參與節點的失敗使得協調者無法獲取所有參與者的響應信息都會導致整個事務的失敗

2.1.2 3PC

2.1.2.1 簡介

  • 3PC(Three-Phase Commit 三階段提交)將二階段提交的提交事務請求過程一分爲二,形成CanCommit、PreCommit、doCommit三個階段

2.1.2.2 內容

  1. CanCommit
    • 事務詢問:協調者向所有參與者發送包含事務內容的CanCommit請求,詢問是否可以執行事務提交操作,等待響應
    • 參與者響應:參與者接收到CanCommit請求後判斷自身能夠順利執行事務,能則返回Yes響應並進入預備狀態,否則返回No響應
  2. PreCommit 21. 如果所有參與者反饋都爲Yes響應,則執行事務預提交 - 發送預提交請求:協調者向所有參與者節點發出PreCommit請求,並進入Prepared階段 - 事務預提交:參與者接收到PreCommit請求後預執行事務操作(還未提交),並記錄Undo和Redo信息到事務日誌中 - 參與者響應事務執行結果:若參與者成功執行事務後則返回Ack響應給協調者,等待最終命令,提交(commit)或者中斷(abort) 22. 如果任何一個參與者反饋了No響應或者協調者等待所有協調者的響應超時則中斷事務 - 發送中斷請求:協調者向所有參與者節點發出Abort請求 - 中斷事務:無論收到Abort請求或者等待協調者請求超時,參與者都會中斷事務
  3. DoCommit 31. 執行提交 - 發送提交請求:當協調者收到所有參與者反饋的Ack響應,向所有參與者發送DoCommit請求,從預提交狀態轉到提交狀態 - 事務提交:參與者接收到DoCommit請求後,正式執行事務提交操作,並釋放佔用的事務資源 - 反饋事務提交結果:參與者完成事務提交後向協調者發送Ack消息 - 完成事務:協調者接受到所有參與者反饋的Ack響應後,完成事務 32. 中斷事務 - 發送中斷請求:協調者向所有參與者節點發出Abort請求 - 事務回滾:參與者接收到Abort請求後,利用Undo信息執行事務回滾操作,並釋放佔用的事務資源 - 反饋事務回滾結果:參與者完成事務回滾後向協調者發送Ack消息 - 中斷事務:協調者接收到所有參與者反饋的Ack響應後,中斷事務

    ps1.需要注意的是,在這一階段,可能發生兩種故障,協調者工作異常,或者協調者與參與者之間網絡異常。無論出現何種情況,都會導致參與者無法及時接收到協調者發送的doCommit或者Abort請求,針對這樣的異常,參與者在等待超時後,繼續進行事務提交。

2.1.2.3 優缺點

  • 優點
    • 降低參與者的阻塞範圍,能夠在出現單點故障後繼續達成一致
  • 缺點
    • 接受者接收到PreCommit消息後,如果出現網絡分區導致協調者和參與者無法正常通信,這時參與者仍會進行事務提交,造成數據的不一致

2.1.3 2PC和3PC的區別總結

  • 2PC圖示

    • 提交成功
    • 中斷事務
  • 3PC 圖示

  • 與兩階段提交不同的是,三階段提交有如下改動點。

    • 引入超時機制。同時在協調者和參與者中都引入超時機制。
    • 在第一階段和第二階段中插入一個準備階段。保證了在最後提交階段之前各參與節點的狀態是一致的。
    • 3PC的第三階段,參與者等待協調者反饋超時時,會默認執行。
  • 總結

    • 相對於2PC,3PC主要解決的單點故障問題,並減少阻塞,因爲一旦參與者無法及時收到來自協調者的信息之後,他會默認執行commit。而不會一直持有事務資源並處於阻塞狀態。但是這種機制也會導致數據一致性問題,因爲,由於網絡原因,協調者發送的abort響應沒有及時被參與者接收到,那麼參與者在等待超時之後執行了commit操作。這樣就和其他接到abort命令並執行回滾的參與者之間存在數據不一致的情況。
    • 默認執行其實是基於概率來決定的,當進入第三階段時,說明參與者在第二階段已經收到了PreCommit請求,那麼協調者產生PreCommit請求的前提條件是他在第二階段開始之前,收到所有參與者的CanCommit響應都是Yes。(一旦參與者收到了PreCommit,意味他知道大家其實都同意修改了)所以,一句話概括就是,當進入第三階段時,由於網絡超時等原因,雖然參與者沒有收到commit或者abort響應,但是他有理由相信:成功提交的機率很大。

2.2 TCC兩階段補償型事務

2.2.1 簡介

TCC方案是可能是目前最火的一種柔性事務方案了。關於TCC(Try-Confirm-Cancel)的概念,最早是由Pat Helland於2007年發表的一篇名爲《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。在該論文中,TCC還是以Tentative-Confirmation-Cancellation命名。正式以Try-Confirm-Cancel作爲名稱的是Atomikos公司,其註冊了TCC商標。

國內最早關於TCC的報道,應該是InfoQ上對阿里程立博士的一篇採訪。經過程博士的這一次傳道之後,TCC在國內逐漸被大家廣爲了解並接受。

Atomikos公司在商業版本事務管理器ExtremeTransactions中提供了TCC方案的實現,但是由於其是收費的,因此相應的很多的開源實現方案也就湧現出來,如:TCC-transaction、ByteTCC、spring-cloud-rest-tcc、ByteTCC、Himly。

2.2.2 內容

TCC是三個英文單詞的首字母縮寫而來。沒錯,TCC分別對應Try、Confirm和Cancel三種操作,這三種操作的業務含義如下:

  1. Try:預留業務資源
  2. Confirm:確認執行業務操作
  3. Cancel:取消執行業務操作

我們以一個經典電商系統下的支付訂單場景爲例:

那對一個訂單支付之後,我們需要做下面的步驟:

  1. 更改訂單的狀態爲“已支付”
  2. 扣減商品庫存
  3. 給會員增加積分
  4. 創建銷售出庫單通知倉庫發貨

上述這幾個步驟,要麼一起成功,要麼一起失敗,必須是一個整體性的事務。

那麼TCC如何實現呢?

2.2.2.1 Try

Try操作的核心是預留業務資源,比如

  1. 別直接把訂單狀態修改爲已支付,可以先把訂單狀態修改爲 UPDATING,也就是修改中的意思。
  2. 庫存服務也別直接扣減庫存啊,而改爲凍結掉庫存。你可以把可銷售的庫存:100-2=98,設置爲98沒問題,然後在一個單獨的凍結庫存的字段裏,設置一個2,也就是說,有2個庫存是給凍結了。
  3. 同理,別直接給用戶增加會員積分,可以先在積分表裏的一個預增加積分字段加入積分。
  4. 銷售出庫單可以創建,但是也設置一箇中間狀態“UNKNOWN”表示未確認。

2.2.2.2 Confirm

完成了Try操作後,接下來就分成兩種情況了,第一種情況是比較理想的,那就是各個服務執行自己的Try操作都成功了,那麼緊接着進入Confirm階段。

訂單,庫存,積分,出庫四個模塊都感知到了try操作的成功,這是confirm操作執行:

  1. 正式把訂單的狀態設置爲“已支付”。
  2. 凍結庫存字段的2個庫存扣掉變爲0。
  3. 將預增加字段的10個積分扣掉,然後加入實際的會員積分字段中。
  4. 將銷售出庫單的狀態正式修改爲“已創建”,可以供倉儲管理人員查看和使用,而不是停留在之前的中間狀態“UNKNOWN”了。

這裏簡單提一句,如果你要玩TCC分佈式事務,必須引入一款TCC分佈式事務框架,比如國內開源的 ByteTCC、Himly、TCC-transaction。否則的話,感知各個階段的執行情況以及推進執行下一個階段的這些事情,不太可能自己手寫實現,太複雜了。

2.2.2.3 Cancel

Confirm是try都成功後的操作,那麼cancel就是try操作異常後纔會進入的階段。如積分服務吧,它執行出錯了,訂單服務內的TCC事務框架是可以感知到的,然後它會決定對整個TCC分佈式事務進行回滾。

  1. 將訂單的狀態設置爲“CANCELED”,也就是這個訂單的狀態是已取消。
  2. 將凍結庫存扣減掉2,加回到可銷售庫存裏去,98 + 2 = 100。
  3. 將預增加積分字段的10個積分扣減掉。
  4. 將銷售出庫單的狀態修改爲“CANCELED”,即已取消。

2.2.3 TCC是補償形事務

TCC中的兩階段提交(try+confirm或者try+cancel)並沒有對開發者完全屏蔽,也就是說從代碼層面,開發者是可以感受到兩階段提交的存在。如上述案例:在第一階段,相關模塊需要提供try接口,爲積分庫存等預留字段分配資源。在第二階段,各模塊需要提供confirm/cancel接口(確認/取消預留)。開發者明顯的感知到了兩階段提交過程的存在。try、confirm/cancel在執行過程中,一般都會開啓各自的本地事務,來保證方法內部業務邏輯的ACID特性。其中:

  1. try過程的本地事務,是保證資源預留的業務邏輯的正確性。

  2. confirm/cancel執行的本地事務邏輯確認/取消預留資源,以保證最終一致性,也就是所謂的補償型事務(Compensation-Based Transactions)。

由於是多個獨立的本地事務,因此不會對資源一直加鎖。

另外,這裏提到confirm/cancel執行的本地事務是補償性事務,關於什麼事補償性事務,atomikos 官網上有以下描述:

紅色框中的內容,是對補償性事務的解釋。大致含義是,"補償是一個獨立的支持ACID特性的本地事務,用於在邏輯上取消服務提供者上一個ACID事務造成的影響,對於一個長事務(long-running transaction),與其實現一個巨大的分佈式ACID事務,不如使用基於補償性的方案,把每一次服務調用當做一個較短的本地ACID事務來處理,執行完就立即提交”。

在這裏,筆者理解爲confirm和cancel就是補償事務,用於取消try階段本地事務造成的影響。因爲第一階段try只是預留資源,之後必須要明確的告訴服務提供者,這個資源你到底要不要,對應第二階段的confirm/cancel。

現在應該明白爲什麼把TCC叫做兩階段補償性事務了,提交過程分爲2個階段,第二階段的confirm/cancel執行的事務屬於補償事務。

2.2.4 優缺點

  • 優點
    • 解決了跨應用業務操作的原子性問題,在諸如組合支付、賬務拆分場景非常實用。
    • TCC實際上把數據庫層的二階段提交上提到了應用層來實現,對於數據庫來說是一階段提交,規避了數據庫層的2PC性能低下問題。
  • 缺點
    • TCC的Try、Confirm和Cancel操作功能需業務提供,開發成本高。

2.3 最大努力通知

2.3.1 簡介

最大努力通知型( Best-effort delivery)是最簡單的一種柔性事務,適用於一些最終一致性時間敏感度低的業務,且被動方處理結果不影響主動方的處理結果。典型的使用場景如銀行通知、商戶通知等。

最大努力通知型的實現方案,一般符合以下特點:

  1. 不可靠消息:業務活動執行方,在完成業務處理之後,向業務活動的觸發方發送消息,直到通知N次後不再通知,允許消息丟失(不可靠消息)。
  2. 定期校對:業務活動的觸發方,根據定時策略,向業務活動執行方查詢(執行方提供查詢接口),恢復丟失的業務消息。

2.3.2 內容

舉例來說:設計一個短信發送平臺,背景是公司內部有多個業務都有發送短信的需求,如果每個業務獨立實現短信發送功能,存在功能實現上的重複。因此專門做了一個短信平臺項目,所有的業務方都接入這個短信平臺,來實現發送短信的功能。簡化後的架構如下所示:

短信發送流程如下:

  1. 業務方將短信發送請求提交給短信平臺
  2. 短信平臺接收到要發送的短信,記錄到數據庫中,並標記其狀態爲”已接收"
  3. 短信平臺調用外部短信發送供應商的接口,發送短信。外部供應商的接口也是異步將短信發送到用戶手機上,因此這個接口調用後,立即返回,進入第4步。
  4. 更新短信發送狀態爲"已發送"
  5. 短信發送供應商異步通知短信平臺短信發送結果。而通知可能失敗,因此最多隻會通知N次。
  6. 短信平臺接收到短信發送結果後,更新短信發送狀態,可能是成功,也可能失敗(如手機欠費)。到底是成功還是失敗並不重要,重要的是我們知道了這調短信發送的最終結果
  7. 如果最多隻通知N次,如果都失敗了的話,那麼短信平臺將不知道短信到底有沒有成功發送。因此短信發送供應商需要提供一個查詢接口,以方便短信平臺驅動的去查詢,進行定期校對。

在這個案例中,短信發送供應商通知短信平臺短信發送結果的過程中,就是最典型的最大努力通知型方案,盡最大的努力通知了N次就不再通知。通過提供一個短信結果查詢接口,讓短信平臺可以進行定期的校對。而由於短信發送業務的時間敏感度並不高,比較適合採用這個方案。

需要注意的是,定期校對的步驟很重要,短信結果查詢接口很重要,必須要進行定期校對。因爲後期要進行對賬,比如一個月的短信發送總量在高峯期可以達到1億條左右,即使一條短信只要5分錢,一個月就有500W。

2.3.3 優缺點

  • 優點
    • 原理簡單,實現方便,目前也有現成的實現框架
  • 缺點
    • 即便柔性事務都只能保證數據的最終一致性,最大努力通知模型的最終時間也可能是最長的,因爲消息發送的不確定性,可能會導致通知遲遲無法被消費,只適用於最終一致性時間敏感度低的業務。
    • 回滾邏輯需要業務編寫補償邏輯來實現,比較費力。

2.4 本地消息表

在描述本地消息表之前,我們要先了解一個概念:

消息發送一致性:是指產生消息的業務動作與消息發送的一致,本地業務邏輯執行與消息發送是原子性的。也就是說,如果業務操作成功,那麼由這個業務操作所產生的消息一定要成功投遞出去(一般是發送到kafka、rocketmq、rabbitmq等消息中間件中),否則就丟消息。

以購物場景爲例,張三購買物品,賬戶扣款100元的同時,需要保證在下游的會員服務中給該賬戶增加100積分。如果扣款100元的業務邏輯執行失敗了,但是通知增加積分的消息卻沒有回滾,而是發送出去了,那就會導致積分無故增加。同樣的,如果扣款成功了,但是消息通知失敗了,扣款卻沒有回滾的話,也會導致該增加的積分沒有增加。

2.4.1 簡介

本地消息表這種實現方式應該是業界使用最多的,這種實現方式的思路,其實是源於 ebay,後來通過支付寶等公司的佈道,在業內廣泛使用。其基本的設計思想是將遠程分佈式事務拆分成一系列的本地事務。如果不考慮性能及設計優雅,藉助關係型數據庫中的表即可實現。

2.4.2 內容

我們可以從下面的流程圖中看出其中的一些細節:

舉例說明:下單購買商品

  1. 支付服務器:前提是有個本地消息表A

    • 1.1 當你支付的時候,你需要把你支付的金額扣減,並且把消息落到本地消息表A,這兩個操作要放入同一個事務(依靠數據庫本地事務保證一致性)。
    • 1.2 消息落表後,發送MQ通知到商品庫存服務器,發送成功後,更新表A中的狀態。
    • 1.3 除此之外,支付服務器還有一個定時任務去輪詢這個本地事務表A,把沒有發送的消息,重試發送給商品庫存服務器。
  2. 商品庫存服務器:前提是有個本地消息表B

    • 2.1 MQ到達商品服務器之後,將接收的消息寫入這個服務器的本地消息表B,然後進行扣減庫存這兩個操作要放入同一個事務(依靠數據庫本地事務保證一致性)。扣減成功後,更新事務表B中的狀態。
    • 2.2 發送反饋消息給支付服務器,如果執行成功了,就反饋成功消息。如果執行失敗,則反饋失敗消息。
    • 2.3 除此之外,商品庫存服務器還有一個定時任務去輪詢這個本地事務表B,把沒有發送的消息,重試發送給支付服務器。

如果支付服務器接收到成功的回饋,那麼事務成功。如果接收到失敗的反饋,則執行回滾操作,即調用補償接口進行反向操作。

本地消息表模型,通過將業務和消息落表的操作放入同一個本地事務,利用本地事務的ACID特性,來確保發送方/接收方的自身業務邏輯的連貫性和緊密型

換句話說,只有發送方的業務邏輯執行成功,發送方纔會將消息落表,以及發出通知,因爲這些步驟在一個本地事務裏面,要麼都失敗,要麼都成功。

同理,接收方的業務邏輯執行,接收消息的落表,以及消息表狀態的翻轉,也都在一個本地事務裏面,所以如果接收方發出了通知,那證明接收方的業務邏輯肯定已經執行了。

當兩端自身的邏輯都具有連貫性和緊密型,那剩下的只要確保消息可靠就行了。mq的重試機制,以及兩方的定時校驗機制,都是這種可靠性的保障。

2.4.3 優缺點

  • 優點
    • 一種非常經典的實現,將整個分佈式事務分割成多個端的本地事務,利用本地事務的可靠性來保證分佈式事務在各個端的可靠性,從而使我們的精力只要集中要消息通知和校檢上。
  • 缺點
    • 消息表會耦合到業務系統中,如果沒有封裝好的解決方案,會有很多雜活需要處理。
    • 回滾邏輯需要業務編寫補償邏輯來實現,比較費力。

2.5 事務消息機制

2.5.1 簡介

前文討論本地消息表的時候,我們提到了消息發送一致性,使用本地消息表,將業務邏輯和本地消息表的讀寫用本地事務來保證,這確實是一個辦法。但這種辦法需要額外建消息表,還需要手動編寫落表邏輯和業務邏輯綁定的代碼,耦合較重。有什麼更優雅的,但同樣能保證消息發送一致性的實現嗎?答案就是本章討論的事務消息機制。

從Apache RocketMQ發佈的4.3版本開始,RocketMQ開源了社區最爲關心的分佈式事務消息,而且實現了對外部組件的零依賴。

RocketMQ事務消息設計則主要是爲了解決Producer端的消息發送與本地事務執行的原子性問題,RocketMQ的設計中broker與producer端的雙向通信能力,使得broker天生可以作爲一個事務協調者存在;而RocketMQ本身提供的存儲機制,則爲事務消息提供了持久化能力;RocketMQ的高可用機制以及可靠消息設計,則爲事務消息在系統在發生異常時,依然能夠保證事務的最終一致性達成。

2.5.2 內容

事務消息的邏輯,是由發送端Producer進行保證(消費端無需考慮)

  1. 首先,發送一個事務消息,這個時候,RocketMQ將消息狀態標記爲Prepared,注意此時這條消息消費者是無法消費到的。
  2. 接着,執行業務代碼邏輯,可能是一個本地數據庫事務操作
  3. 最後,確認發送消息,根據本地業務執行結果返回commit或者是rollback。
    • 3.1 如果本地業務執行成功,消息是commit,這個時候,RocketMQ將消息狀態標記爲可消費,這個時候消費者,才能真正的保證消費到這條數據。
    • 3.2 如果消息是rollback,RocketMQ將刪除該prepare消息不進行下發。

如果發送端發送的確認消息發送失敗了怎麼辦?RocketMQ會定期掃描消息集羣中的事務消息,如果發現了Prepared消息,它會向消息發送端(生產者)確認。RocketMQ會根據發送端設置的策略來決定是回滾還是繼續發送確認消息。這樣就保證了消息發送與本地事務同時成功或同時失敗。

消費端的消費成功機制由RocketMQ保證。如果發送的消息消費超時了就一直重試。

但值得注意的是,如果消費端接到通知,然後執行消費端業務邏輯失敗了的話,阿里提供給我們的解決方法是:人工解決。也就是說,兩端之間的原子性,需要人工做補償邏輯,該機制無法保證。

2.5.3 優缺點

  • 優點
    • 依靠成熟的消息中間件的事務消息機制,不用耦合太多其他邏輯在業務邏輯中,就可以保證消息發送一致性,實現簡單。
  • 缺點
    • 發送端和消費端之間的原子性無法保證,如果發送回滾,需要人工介入。

2.6 Saga事務模型

2.6.1 簡介

Saga事務模型又叫做長時間運行的事務(Long-running-transaction), 它是由普林斯頓大學的H.Garcia-Molina等人於1987年提出,是一種異步的分佈式事務解決方案,其理論基礎在於,其假設所有事件按照順序推進,總能達到系統的最終一致性,因此saga需要服務分別定義提交接口以及補償接口,當某個事務分支失敗時,調用其它的分支的補償接口來進行回滾。

2.6.2 內容

saga的具體實現分爲兩種:Choreography以及Orchestration:

Choreography:更接近Saga模型的初衷的一種實現:所有事件按照順序推進,總能達到系統的最終一致性

這種模式下不存在協調器的概念,每個節點均對自己的上下游負責,在監聽處理上游節點事件的同時,對下游節點發布事件。

Orchestration:存在中心節點的模式

該中心節點,即協調器知道整個事務的分佈狀態,相比於無中心節點方式,該方式有着許多優點:

  1. 能夠避免事務之間的循環依賴關係,由協調器來管理整個事務鏈條。
  2. 參與者只需要執行命令/回覆(其實回覆消息也是一種事件消息),無需關心和維護自己的上下游是誰,降低參與者的複雜性。
  3. 開發測試門檻低。
  4. 擴展性好,在添加新步驟時,事務複雜性保持線性,回滾更容易管理。

基於上述優勢,因此大多數saga模型實現均採用了這種思路。

2.6.3 優缺點

  • 優點
    • 降低了事務粒度,使得事務擴展更加容易,同時採用了異步化方式提升性能。
  • 缺點
    • 很多時候很難定義補償接口,回滾代價高,而且由於在執行過程中採用了先提交後補償的思路進行操作,所以單個子事務在併發提交時的隔離性很難保證。

3 分佈式事務解決方案總結

3.1 XA協議和TCC的區別

作爲最熱門的兩種解決方案,XA協議和TCC的區別我們需要重點知曉。

TCC與XA兩階段提交有着異曲同工之妙,下圖列出了二者之間的對比:

  1. 在階段1:

    • 在XA中,各個RM準備提交各自的事務分支,事實上就是準備提交資源的更新操作(insert、delete、update等);而在TCC中,是主業務活動請求(try)各個從業務服務預留資源。
  2. 在階段2:

    • XA根據第一階段每個RM是否都prepare成功,判斷是要提交還是回滾。如果都prepare成功,那麼就commit每個事務分支,反之則rollback每個事務分支。

TCC中,如果在第一階段所有業務資源都預留成功,那麼confirm各個從業務服務,否則取消(cancel)所有從業務服務的資源預留請求。

TCC兩階段提交與XA兩階段提交的區別是:

  1. XA是資源層面的分佈式事務,強一致性,在兩階段提交的整個過程中,一直會持有資源的鎖

    • XA事務中的兩階段提交內部過程是對開發者屏蔽的,回顧我們之前講解JTA規範時,通過UserTransaction的commit方法來提交全局事務,這只是一次方法調用,其內部會委派給TransactionManager進行真正的兩階段提交,因此開發者從代碼層面是感知不到這個過程的。
    • 而事務管理器在兩階段提交過程中,從prepare到commit/rollback過程中,資源實際上一直都是被加鎖的。如果有其他人需要更新這兩條記錄,那麼就必須等待鎖釋放。
  2. TCC是業務層面的分佈式事務,最終一致性,在TCC整個過程中,不會一直持有資源的鎖

    • TCC中的兩階段提交併沒有對開發者完全屏蔽,也就是說從代碼層面,開發者是可以感受到兩階段提交的存在。如上述航班預定案例:在第一階段,航空公司需要提供try接口(機票資源預留)。
    • 在第二階段,航空公司提需要提供confirm/cancel接口(確認購買機票/取消預留)。開發者明顯的感知到了兩階段提交過程的存在。try、confirm/cancel在執行過程中,一般都會開啓各自的本地事務,來保證方法內部業務邏輯的ACID特性。其中:
      1. try過程的本地事務,是保證資源預留的業務邏輯的正確性。
      2. confirm/cancel執行的本地事務邏輯確認/取消預留資源,以保證最終一致性,也就是所謂的補償型事務(Compensation-Based Transactions)。

3.2 最大努力通知和本地消息表的區別

雖然都是利用mq,但是本地消息表利用本地事務來綁定業務邏輯和消息發送,使得mq兩端的操作(發送前和接收後)是絕對可靠的,原子的。保證了消息發送一致性。

而最大努力通知模型,業務邏輯和發送消息之間沒有這種緊密的可靠性保證,一切只能在業務上自己去實現代碼來保證可靠。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章