關於分布式事務,XA協議的學習筆記
XA分布式事務協議,包含二階段提交(2PC),三階段提交(3PC)兩種實現。
1、二階段提交方案:強一致性
事務的發起者稱協調者,事務的執行者稱參與者。
處理流程:
1、準備階段
事務協調者,向所有事務參與者發送事務內容,詢問是否可以提交事務,並等待參與者回復。
事務參與者收到事務內容,開始執行事務操作,講 undo 和 redo 信息記入事務日誌中(但此時並不提交事務)。
如果參與者執行成功,給協調者回復yes,表示可以進行事務提交。如果執行失敗,給協調者回復no,表示不可提交。
2、提交階段
如果協調者收到了參與者的失敗信息或超時信息,直接給所有參與者發送回滾(rollback)信息進行事務回滾,否則,發送提交(commit)信息。
參與者根據協調者的指令執行提交或者回滾操作,釋放所有事務處理過程中使用的鎖資源。(註意:必須在最後階段釋放鎖資源) 接下來分兩種情況分別討論提交階段的過程。
簡單一點理解,可以把協調者節點比喻為帶頭大哥,參與者理解比喻為跟班小弟,帶頭大哥統一協調跟班小弟的任務執行。
階段 1:準備階段
準備階段有如下三個步驟:
- 協調者向所有參與者發送事務內容,詢問是否可以提交事務,並等待所有參與者答復。
- 各參與者執行事務操作,將 undo 和 redo 信息記入事務日誌中(但不提交事務)。
- 如參與者執行成功,給協調者反饋 yes,即可以提交;如執行失敗,給協調者反饋 no,即不可提交。
階段 2:提交階段
如果協調者收到了參與者的失敗消息或者超時,直接給每個參與者發送回滾(rollback)消息;否則,發送提交(commit)消息。
參與者根據協調者的指令執行提交或者回滾操作,釋放所有事務處理過程中使用的鎖資源。(註意:必須在最後階段釋放鎖資源) 接下來分兩種情況分別討論提交階段的過程。
情況 1,當所有參與者均反饋 yes,提交事務,如上圖:
- 協調者向所有參與者發出正式提交事務的請求(即 commit 請求)。
- 參與者執行 commit 請求,並釋放整個事務期間占用的資源。
- 各參與者向協調者反饋 ack(應答)完成的消息。
- 協調者收到所有參與者反饋的 ack 消息後,即完成事務提交。
情況 2,當任何階段 1 一個參與者反饋 no,中斷事務,如上圖:
- 協調者向所有參與者發出回滾請求(即 rollback 請求)。
- 參與者使用階段 1 中的 undo 信息執行回滾操作,並釋放整個事務期間占用的資源。
- 各參與者向協調者反饋 ack 完成的消息。
- 協調者收到所有參與者反饋的 ack 消息後,即完成事務中斷。
方案總結
2PC 方案實現起來簡單,實際項目中使用比較少,主要因為以下問題:
- 性能問題:所有參與者在事務提交階段處於同步阻塞狀態,占用系統資源,容易導致性能瓶頸。
- 可靠性問題:如果協調者存在單點故障問題,如果協調者出現故障,參與者將一直處於鎖定狀態。
- 數據一致性問題:在階段 2 中,如果發生局部網絡問題,一部分事務參與者收到了提交消息,另一部分事務參與者沒收到提交消息,那麽就導致了節點之間數據的不一致。
2、3階段提交
三階段提交是在二階段提交上的改進版本,主要是加入了超時機制。同時在協調者和參與者中都引入超時機制。
三階段將二階段的準備階段拆分為2個階段,插入了一個preCommit階段,以此來處理原先二階段,參與者準備後,參與者發生崩潰或錯誤,導致參與者無法知曉是否提交或回滾的不確定狀態所引起的延時問題。
處理流程
階段 1:canCommit
協調者向參與者發送 commit 請求,參與者如果可以提交就返回 yes 響應(參與者不執行事務操作),否則返回 no 響應:
- 協調者向所有參與者發出包含事務內容的 canCommit 請求,詢問是否可以提交事務,並等待所有參與者答復。
- 參與者收到 canCommit 請求後,如果認為可以執行事務操作,則反饋 yes 並進入預備狀態,否則反饋 no。
階段 2:preCommit
協調者根據階段 1 canCommit 參與者的反應情況來決定是否可以進行基於事務的 preCommit 操作。根據響應情況,有以下兩種可能。
情況 1:階段 1 所有參與者均反饋 yes,參與者預執行事務,如上圖:
- 協調者向所有參與者發出 preCommit 請求,進入準備階段。
- 參與者收到 preCommit 請求後,執行事務操作,將 undo 和 redo 信息記入事務日誌中(但不提交事務)。
- 各參與者向協調者反饋 ack 響應或 no 響應,並等待最終指令。
情況 2:階段 1 任何一個參與者反饋 no,或者等待超時後協調者尚無法收到所有參與者的反饋,即中斷事務,如上圖:
- 協調者向所有參與者發出 abort 請求。
- 無論收到協調者發出的 abort 請求,或者在等待協調者請求過程中出現超時,參與者均會中斷事務。
階段 3:do Commit
該階段進行真正的事務提交,也可以分為以下兩種情況。
情況 1:階段 2 所有參與者均反饋 ack 響應,執行真正的事務提交,如上圖:
- 如果協調者處於工作狀態,則向所有參與者發出 do Commit 請求。
- 參與者收到 do Commit 請求後,會正式執行事務提交,並釋放整個事務期間占用的資源。
- 各參與者向協調者反饋 ack 完成的消息。
- 協調者收到所有參與者反饋的 ack 消息後,即完成事務提交。
情況 2:階段 2 任何一個參與者反饋 no,或者等待超時後協調者尚無法收到所有參與者的反饋,即中斷事務,如上圖:
- 如果協調者處於工作狀態,向所有參與者發出 abort 請求。
- 參與者使用階段 1 中的 undo 信息執行回滾操作,並釋放整個事務期間占用的資源。
- 各參與者向協調者反饋 ack 完成的消息。
- 協調者收到所有參與者反饋的 ack 消息後,即完成事務中斷。
註意:進入階段 3 後,無論協調者出現問題,或者協調者與參與者網絡出現問題,都會導致參與者無法接收到協調者發出的 do Commit 請求或 abort 請求。此時,參與者都會在等待超時之後,繼續執行事務提交。
方案總結
優點:相比二階段提交,三階段提交降低了阻塞範圍,在等待超時後協調者或參與者會中斷事務。避免了協調者單點問題,階段 3 中協調者出現問題時,參與者會繼續提交事務。
缺點:數據不一致問題依然存在,當在參與者收到 preCommit 請求後等待 do commite 指令時,此時如果協調者請求中斷事務,而協調者無法與參與者正常通信,會導致參與者繼續提交事務,造成數據不一致。
TCC 事務:最終一致性
方案簡介
TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 於 2007 年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。
TCC 是服務化的二階段編程模型,其 Try、Confirm、Cancel 3 個方法均由業務編碼實現:
- Try 操作作為一階段,負責資源的檢查和預留。
- Confirm 操作作為二階段提交操作,執行真正的業務。
- Cancel 是預留資源的取消。
TCC 事務的 Try、Confirm、Cancel 可以理解為 SQL 事務中的 Lock、Commit、Rollback。
處理流程
為了方便理解,下面以電商下單為例進行方案解析,這裏把整個過程簡單分為扣減庫存,訂單創建 2 個步驟,庫存服務和訂單服務分別在不同的服務器節點上。
①Try 階段
從執行階段來看,與傳統事務機制中業務邏輯相同。但從業務角度來看,卻不一樣。
TCC 機制中的 Try 僅是一個初步操作,它和後續的確認一起才能真正構成一個完整的業務邏輯,這個階段主要完成:
- 完成所有業務檢查( 一致性 ) 。
- 預留必須業務資源( 準隔離性 ) 。
- Try 嘗試執行業務。
TCC 事務機制以初步操作(Try)為中心的,確認操作(Confirm)和取消操作(Cancel)都是圍繞初步操作(Try)而展開。
因此,Try 階段中的操作,其保障性是最好的,即使失敗,仍然有取消操作(Cancel)可以將其執行結果撤銷。
假設商品庫存為 100,購買數量為 2,這裏檢查和更新庫存的同時,凍結用戶購買數量的庫存,同時創建訂單,訂單狀態為待確認。
②Confirm / Cancel 階段
根據 Try 階段服務是否全部正常執行,繼續執行確認操作(Confirm)或取消操作(Cancel)。
Confirm 和 Cancel 操作滿足冪等性,如果 Confirm 或 Cancel 操作執行失敗,將會不斷重試直到執行完成。
Confirm:當 Try 階段服務全部正常執行, 執行確認業務邏輯操作
這裏使用的資源一定是 Try 階段預留的業務資源。在 TCC 事務機制中認為,如果在 Try 階段能正常的預留資源,那 Confirm 一定能完整正確的提交。
Confirm 階段也可以看成是對 Try 階段的一個補充,Try+Confirm 一起組成了一個完整的業務邏輯。
Cancel:當 Try 階段存在服務執行失敗, 進入 Cancel 階段
Cancel 取消執行,釋放 Try 階段預留的業務資源,上面的例子中,Cancel 操作會把凍結的庫存釋放,並更新訂單狀態為取消。
方案總結
TCC 事務機制相對於傳統事務機制(X/Open XA),TCC 事務機制相比於上面介紹的 XA 事務機制,有以下優點:
- 性能提升:具體業務來實現控制資源鎖的粒度變小,不會鎖定整個資源。
- 數據最終一致性:基於 Confirm 和 Cancel 的冪等性,保證事務最終完成確認或者取消,保證數據的一致性。
- 可靠性:解決了 XA 協議的協調者單點故障問題,由主業務方發起並控制整個業務活動,業務活動管理器也變成多點,引入集群。
缺點: TCC 的 Try、Confirm 和 Cancel 操作功能要按具體業務來實現,業務耦合度較高,提高了開發成本。
本地消息表:最終一致性
方案簡介
本地消息表的方案最初是由 eBay 提出,核心思路是將分布式事務拆分成本地事務進行處理。
方案通過在事務主動發起方額外新建事務消息表,事務發起方處理業務和記錄事務消息在本地事務中完成,輪詢事務消息表的數據發送事務消息,事務被動方基於消息中間件消費事務消息表中的事務。
這樣設計可以避免”業務處理成功 + 事務消息發送失敗",或"業務處理失敗 + 事務消息發送成功"的棘手情況出現,保證 2 個系統事務的數據一致性。
處理流程
下面把分布式事務最先開始處理的事務方稱為事務主動方,在事務主動方之後處理的業務內的其他事務稱為事務被動方。
為了方便理解,下面繼續以電商下單為例進行方案解析,這裏把整個過程簡單分為扣減庫存,訂單創建 2 個步驟。
庫存服務和訂單服務分別在不同的服務器節點上,其中庫存服務是事務主動方,訂單服務是事務被動方。
事務的主動方需要額外新建事務消息表,用於記錄分布式事務的消息的發生、處理狀態。
整個業務處理流程如下:
步驟1:事務主動方處理本地事務。
事務主動方在本地事務中處理業務更新操作和寫消息表操作。上面例子中庫存服務階段在本地事務中完成扣減庫存和寫消息表(圖中 1、2)。
步驟 2:事務主動方通過消息中間件,通知事務被動方處理事務通知事務待消息。
消息中間件可以基於 Kafka、RocketMQ 消息隊列,事務主動方主動寫消息到消息隊列,事務消費方消費並處理消息隊列中的消息。
上面例子中,庫存服務把事務待處理消息寫到消息中間件,訂單服務消費消息中間件的消息,完成新增訂單(圖中 3 - 5)。
步驟 3:事務被動方通過消息中間件,通知事務主動方事務已處理的消息。
上面例子中,訂單服務把事務已處理消息寫到消息中間件,庫存服務消費中間件的消息,並將事務消息的狀態更新為已完成(圖中 6 - 8)。
為了數據的一致性,當處理錯誤需要重試,事務發送方和事務接收方相關業務處理需要支持冪等。
具體保存一致性的容錯處理如下:
- 當步驟 1 處理出錯,事務回滾,相當於什麽都沒發生。
- 當步驟 2、步驟 3 處理出錯,由於未處理的事務消息還是保存在事務發送方,事務發送方可以定時輪詢為超時消息數據,再次發送到消息中間件進行處理。事務被動方消費事務消息重試處理。
- 如果是業務上的失敗,事務被動方可以發消息給事務主動方進行回滾。
- 如果多個事務被動方已經消費消息,事務主動方需要回滾事務時需要通知事務被動方回滾。
方案總結
方案的優點如下:
- 從應用設計開發的角度實現了消息數據的可靠性,消息數據的可靠性不依賴於消息中間件,弱化了對 MQ 中間件特性的依賴。
- 方案輕量,容易實現。
缺點如下:
- 與具體的業務場景綁定,耦合性強,不可公用。
- 消息數據與業務數據同庫,占用業務系統資源。
- 業務系統在使用關系型數據庫的情況下,消息服務性能會受到關系型數據庫並發性能的局限。
MQ 事務:最終一致性
方案簡介
基於 MQ 的分布式事務方案其實是對本地消息表的封裝,將本地消息表基於 MQ 內部,其他方面的協議基本與本地消息表一致。
處理流程
下面主要基於 RocketMQ 4.3 之後的版本介紹 MQ 的分布式事務方案。
在本地消息表方案中,保證事務主動方發寫業務表數據和寫消息表數據的一致性是基於數據庫事務,RocketMQ 的事務消息相對於普通 MQ,相對於提供了 2PC 的提交接口,方案如下:
正常情況:事務主動方發消息
這種情況下,事務主動方服務正常,沒有發生故障,發消息流程如下:
- 圖中 1:發送方向 MQ 服務端(MQ Server)發送 half 消息。
- 圖中 2:MQ Server 將消息持久化成功之後,向發送方 ack 確認消息已經發送成功。
- 圖中 3:發送方開始執行本地事務邏輯。
- 圖中 4:發送方根據本地事務執行結果向 MQ Server 提交二次確認(commit 或是 rollback)。
- 圖中 5:MQ Server 收到 commit 狀態則將半消息標記為可投遞,訂閱方最終將收到該消息;MQ Server 收到 rollback 狀態則刪除半消息,訂閱方將不會接受該消息。
異常情況:事務主動方消息恢復
在斷網或者應用重啟等異常情況下,圖中 4 提交的二次確認超時未到達 MQ Server,此時處理邏輯如下:
- 圖中 5:MQ Server 對該消息發起消息回查。
- 圖中 6:發送方收到消息回查後,需要檢查對應消息的本地事務執行的最終結果。
- 圖中 7:發送方根據檢查得到的本地事務的最終狀態再次提交二次確認。
- 圖中 8:MQ Server基於 commit/rollback 對消息進行投遞或者刪除。
介紹完 RocketMQ 的事務消息方案後,由於前面已經介紹過本地消息表方案,這裏就簡單介紹 RocketMQ 分布式事務:
事務主動方基於 MQ 通信通知事務被動方處理事務,事務被動方基於 MQ 返回處理結果。
如果事務被動方消費消息異常,需要不斷重試,業務處理邏輯需要保證冪等。
如果是事務被動方業務上的處理失敗,可以通過 MQ 通知事務主動方進行補償或者事務回滾。
方案總結
相比本地消息表方案,MQ 事務方案優點是:
- 消息數據獨立存儲 ,降低業務系統與消息系統之間的耦合。
- 吞吐量由於使用本地消息表方案。
缺點是:
- 一次消息發送需要兩次網絡請求(half 消息 + commit/rollback 消息) 。
- 業務處理服務需要實現消息狀態回查接口。
Saga 事務:最終一致性
方案簡介
Saga 事務源於 1987 年普林斯頓大學的 Hecto 和 Kenneth 發表的如何處理 long lived transaction(長活事務)論文。
Saga 事務核心思想是將長事務拆分為多個本地短事務,由 Saga 事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。
處理流程
Saga 事務基本協議如下:
- 每個 Saga 事務由一系列冪等的有序子事務(sub-transaction) Ti 組成。
- 每個 Ti 都有對應的冪等補償動作 Ci,補償動作用於撤銷 Ti 造成的結果。
可以看到,和 TCC 相比,Saga 沒有“預留”動作,它的 Ti 就是直接提交到庫。
下面以下單流程為例,整個操作包括:創建訂單、扣減庫存、支付、增加積分。
Saga 的執行順序有兩種,如上圖:
- 事務正常執行完成:T1, T2, T3, ..., Tn,例如:扣減庫存(T1),創建訂單(T2),支付(T3),依次有序完成整個事務。
- 事務回滾:T1, T2, ..., Tj, Cj,..., C2, C1,其中 0 < j < n,例如:扣減庫存(T1),創建訂單(T2),支付(T3,支付失敗),支付回滾(C3),訂單回滾(C2),恢復庫存(C1)。
Saga 定義了兩種恢復策略:
向前恢復(forward recovery):對應於上面第一種執行順序,適用於必須要成功的場景,發生失敗進行重試,執行順序是類似於這樣的:T1, T2, ..., Tj(失敗), Tj(重試),..., Tn,其中j是發生錯誤的子事務(sub-transaction)。該情況下不需要Ci。
向後恢復(backward recovery):對應於上面提到的第二種執行順序,其中 j 是發生錯誤的子事務(sub-transaction),這種做法的效果是撤銷掉之前所有成功的子事務,使得整個 Saga 的執行結果撤銷。
Saga 事務常見的有兩種不同的實現方式:
①命令協調(Order Orchestrator):中央協調器負責集中處理事件的決策和業務邏輯排序。
中央協調器(Orchestrator,簡稱 OSO)以命令/回復的方式與每項服務進行通信,全權負責告訴每個參與者該做什麽以及什麽時候該做什麽。
以電商訂單的例子為例:
- 事務發起方的主業務邏輯請求 OSO 服務開啟訂單事務
- OSO 向庫存服務請求扣減庫存,庫存服務回復處理結果。
- OSO 向訂單服務請求創建訂單,訂單服務回復創建結果。
- OSO 向支付服務請求支付,支付服務回復處理結果。
- 主業務邏輯接收並處理 OSO 事務處理結果回復。
中央協調器必須事先知道執行整個訂單事務所需的流程(例如通過讀取配置)。如果有任何失敗,它還負責通過向每個參與者發送命令來撤銷之前的操作來協調分布式的回滾。
基於中央協調器協調一切時,回滾要容易得多,因為協調器默認是執行正向流程,回滾時只要執行反向流程即可。
②事件編排(Event Choreography0):沒有中央協調器(沒有單點風險)時,每個服務產生並觀察其他服務的事件,並決定是否應采取行動。
在事件編排方法中,第一個服務執行一個事務,然後發布一個事件。該事件被一個或多個服務進行監聽,這些服務再執行本地事務並發布(或不發布)新的事件。
當最後一個服務執行本地事務並且不發布任何事件時,意味著分布式事務結束,或者它發布的事件沒有被任何 Saga 參與者聽到都意味著事務結束。
以電商訂單的例子為例:
- 事務發起方的主業務邏輯發布開始訂單事件。
- 庫存服務監聽開始訂單事件,扣減庫存,並發布庫存已扣減事件。
- 訂單服務監聽庫存已扣減事件,創建訂單,並發布訂單已創建事件。
- 支付服務監聽訂單已創建事件,進行支付,並發布訂單已支付事件。
- 主業務邏輯監聽訂單已支付事件並處理。
事件/編排是實現 Saga 模式的自然方式,它很簡單,容易理解,不需要太多的代碼來構建。如果事務涉及 2 至 4 個步驟,則可能是非常合適的。
方案總結
命令協調設計的優點如下:
- 服務之間關系簡單,避免服務之間的循環依賴關系,因為 Saga 協調器會調用 Saga 參與者,但參與者不會調用協調器。
- 程序開發簡單,只需要執行命令/回復(其實回復消息也是一種事件消息),降低參與者的復雜性。
- 易維護擴展,在添加新步驟時,事務復雜性保持線性,回滾更容易管理,更容易實施和測試。
命令協調設計缺點如下:
- 中央協調器容易處理邏輯容易過於復雜,導致難以維護。
- 存在協調器單點故障風險。
事件/編排設計優點如下:
- 避免中央協調器單點故障風險。
- 當涉及的步驟較少服務開發簡單,容易實現。
事件/編排設計缺點如下:
- 服務之間存在循環依賴的風險。
- 當涉及的步驟較多,服務間關系混亂,難以追蹤調測。
值得補充的是,由於 Saga 模型中沒有 Prepare 階段,因此事務間不能保證隔離性。
當多個 Saga 事務操作同一資源時,就會產生更新丟失、臟數據讀取等問題,這時需要在業務層控制並發,例如:在應用層面加鎖,或者應用層面預先凍結資源。
總結
各方案使用場景
介紹完分布式事務相關理論和常見解決方案後,最終的目的在實際項目中運用,因此,總結一下各個方案的常見的使用場景:
- 2PC/3PC:依賴於數據庫,能夠很好的提供強一致性和強事務性,但相對來說延遲比較高,比較適合傳統的單體應用,在同一個方法中存在跨庫操作的情況,不適合高並發和高性能要求的場景。
- TCC:適用於執行時間確定且較短,實時性要求高,對數據一致性要求高,比如互聯網金融企業最核心的三個服務:交易、支付、賬務。
- 本地消息表/MQ 事務:都適用於事務中參與方支持操作冪等,對一致性要求不高,業務上能容忍數據不一致到一個人工檢查周期,事務涉及的參與方、參與環節較少,業務上有對賬/校驗系統兜底。
- Saga 事務:由於 Saga 事務不能保證隔離性,需要在業務層控制並發,適合於業務場景事務並發操作同一資源較少的情況。 Saga 相比缺少預提交動作,導致補償動作的實現比較麻煩,例如業務是發送短信,補償動作則得再發送一次短信說明撤銷,用戶體驗比較差。Saga 事務較適用於補償動作容易處理的場景。
分布式事務方案設計
本文介紹的偏向於原理,業界已經有不少開源的或者收費的解決方案,篇幅所限,就不再展開介紹。
實際運用理論時進行架構設計時,許多人容易犯“手裏有了錘子,看什麽都覺得像釘子”的錯誤,設計方案時考慮的問題場景過多,各種重試,各種補償機制引入系統,導致系統過於復雜,落地遙遙無期。
世界上解決一個計算機問題最簡單的方法:“恰好”不需要解決它!
—— 阿裏中間件技術專家沈詢
有些問題,看起來很重要,但實際上我們可以通過合理的設計或者將問題分解來規避。
設計分布式事務系統也不是需要考慮所有異常情況,不必過度設計各種回滾,補償機制。
如果硬要把時間花在解決問題本身,實際上不僅效率低下,而且也是一種浪費。
如果系統要實現回滾流程的話,有可能系統復雜度將大大提升,且很容易出現 Bug,估計出現 Bug 的概率會比需要事務回滾的概率大很多。
在設計系統時,我們需要衡量是否值得花這麽大的代價來解決這樣一個出現概率非常小的問題,可以考慮當出現這個概率很小的問題,能否采用人工解決的方式,這也是大家在解決疑難問題時需要多多思考的地方。
參考資料:
- technology-talk —— 事務
- MySQL 中事務的實現
- 分布式一致性算法 2PC 和 3PC
- 分布式開放消息系統(RocketMQ)的原理與實踐
- RocketMQ 事務消息入門介紹
- Saga 分布式事務解決方案與實踐 —— 姜寧
- 分布式事務 Saga 模式
- 從一筆金幣充值去思考分布式事務
原文參考地址:
https://maimai.cn/article/detail?fid=1122653559&efid=jewbG8cL4Wikr2CJ3r_fAw
關於分布式事務,XA協議的學習筆記