分布式事務最經典的七種解決方案
轉載:后端 - 分布式事務最經典的七種解決方案 - 分布式事務 - SegmentFault 思否
隨著業務的快速發展、業務復雜度越來越高,幾乎每個公司的系統都會從單體走向分布式,特別是轉向微服務架構。隨之而來就必然遇到分布式事務這個難題。
這篇文章首先介紹了相關的基礎理論,然后總結了最經典的事務方案,最后給出了子事務亂序執行(冪等、空補償、懸掛問題)的解決方案,分享給大家。
基礎理論
在講解具體方案之前,我們先了解一下分布式事務所涉及到的基礎理論知識。
我們拿轉賬作為例子,A需要轉100元給B,那么需要給A的余額-100元,給B的余額+100元,整個轉賬要保證,A-100和B+100同時成功,或者同時失敗。看看在各種場景下,是如何解決這個問題的。
事務
把多條語句作為一個整體進行操作的功能,被稱為數據庫事務。數據庫事務可以確保該事務范圍內的所有操作都可以全部成功或者全部失敗。
事務具有 4 個屬性:原子性、一致性、隔離性、持久性。這四個屬性通常稱為 ACID 特性。
- Atomicity(原子性):一個事務中的所有操作,要么全部完成,要么全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被恢復到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
- Consistency(一致性):在事務開始前、進行中、結束后,數據庫的完整性沒有被破壞。完整性包括外鍵約束、應用定義的等約束不會被破壞。
- Isolation(隔離性):數據庫允許多個并發事務同時對其數據進行讀寫和修改的能力,隔離性可以防止多個事務并發執行時由于交叉執行而導致數據的不一致。
- Durability(持久性):事務處理結束后,對數據的修改就是永久的,即便系統故障也不會丟失。
假如我們的業務系統不復雜,可以在一個數據庫、一個服務內對數據進行修改,完成轉賬,那么,我們可以利用數據庫事務,保證轉賬業務的正確完成。
分布式事務
銀行跨行轉賬業務是一個典型分布式事務場景,假設A需要跨行轉賬給B,那么就涉及兩個銀行的數據,無法通過一個數據庫的本地事務保證轉賬的ACID,只能夠通過分布式事務來解決。
分布式事務就是指事務的發起者、資源及資源管理器和事務協調者分別位于分布式系統的不同節點之上。在上述轉賬的業務中,用戶A-100操作和用戶B+100操作不是位于同一個節點上。本質上來說,分布式事務就是為了保證在分布式場景下,數據操作的正確執行。
ACID
分布式事務會部分遵循 ACID 規范:
- 原子性:嚴格遵循
- 一致性:事務完成后的一致性嚴格遵循;事務中的一致性可適當放寬
- 隔離性:并行事務間不可影響;事務中間結果可見性允許安全放寬
- 持久性:嚴格遵循
因為事務過程中,不是一致的,但事務會最終完成,最終達到一致,所以我們把分布式事務稱為“最終一致”
最終一致性釋疑
特別強調一下,這里的最終一致性和CAP(C:一致性,A:可用性,P:分區容忍性)的最終一致性不同,目前大部分的書籍和資料,都將兩者混為一談,下面我們將重點進行一致性的解釋。
CAP的C指的是分布式系統中從多副本讀取數據時的一致性。簡單的說,如果我將一個數據從v1更新為v2,之后任意的數據讀取:
- 強一致:每次都能確保讀到v2,那么是強一致
- 弱一致:可能讀到v1,也可能讀到v2,那么是弱一致
- 最終一致:經過一定時間后,確保每次讀取能夠讀到v2,那么是最終一致
CAP理論提出,分布式系統無法同時滿足3個特性,最多只能選2,面對這樣的問題,有一類經典的方案是BASE理論,這類方案追求AP,然后放寬對C的要求。AWS的Dynamo就是一個這樣的系統,提供了最終一致性讀,詳情參見Dynamo 一致性讀
近些年分布式理論進一步發展,有很多系統不走BASE方案,而是CP+HA(Highly-Available)的方案。Paxos、Raft等分布式共識協議,完全滿足CP,而在A-可用性上面,雖然不是100%的可用,但是結合近些年硬件穩定性升級,可以做到了高可用。谷歌分布式鎖Chubby的公開數據顯示,集群能提供99.99958%的平均可用性,一年也就130s的運行中斷,已經能夠滿足非常嚴苛的應用要求。現在的SQL類數據庫軟件,都是走CP+HA,只是HA會比谷歌的這個極致數據更低一些,但一般都能夠達到4個9
CP+HA意味著不是BASE,意味著你只要寫入成功,那么接下來的讀,能夠讀取到最新的結果,開發人員不用擔心讀取到的不是最新數據,在多副本讀寫上面,與單機是一致的。
因為分布式事務研究解決的主要是涉及多數據庫的數據一致性,實際數據的存儲主要在數據庫,因此也是CP+HA。因此分布式事務滿足CAP的C,但是不滿足ACID的C,也被稱為最終一致
分布式事務的經典解決方案
由于分布式事務方案,無法做到完全的ACID的保證,沒有一種完美的方案,能夠解決掉所有業務問題。因此在實際應用中,會根據業務的不同特性,選擇最適合的分布式事務方案。
一、兩階段提交/XA
XA是由X/Open組織提出的分布式事務的規范,XA規范主要定義了(全局)事務管理器(TM)和(局部)資源管理器(RM)之間的接口。本地的數據庫如mysql在XA中扮演的是RM角色
XA一共分為兩階段:
第一階段(prepare):即所有的參與者RM準備執行事務并鎖住需要的資源。參與者ready時,向TM報告已準備就緒。
第二階段 (commit/rollback):當事務管理者(TM)確認所有參與者(RM)都ready后,向所有參與者發送commit命令。
目前主流的數據庫基本都支持XA事務,包括mysql、oracle、sqlserver、postgre
XA 事務由一個或多個資源管理器(RM)、一個事務管理器(TM)和一個應用程序(ApplicationProgram)組成。
這里的RM、TM、AP三個角色是經典的角色劃分,會貫穿后續Saga、Tcc等事務模式。
把上面的轉賬作為例子,一個成功完成的XA事務時序圖如下:
如果有任何一個參與者prepare失敗,那么TM會通知所有完成prepare的參與者進行回滾。
XA事務的特點是:
- 簡單易理解,開發較容易
- 對資源進行了長時間的鎖定,并發度低
如果讀者想要進一步研究XA,go語言以及PHP、Python、Java、C#、Node等都可參考DTM
二、SAGA
Saga是這一篇數據庫論文sagas提到的一個方案。其核心思想是將長事務拆分為多個本地短事務,由Saga事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。
把上面的轉賬作為例子,一個成功完成的SAGA事務時序圖如下:
Saga一旦到了Cancel階段,那么Cancel在業務邏輯上是不允許失敗了。如果因為網絡或者其他臨時故障,導致沒有返回成功,那么TM會不斷重試,直到Cancel返回成功。
Saga事務的特點:
- 并發度高,不用像XA事務那樣長期鎖定資源
- 需要定義正常操作以及補償操作,開發量比XA大
- 一致性較弱,對于轉賬,可能發生A用戶已扣款,最后轉賬又失敗的情況
論文里面的SAGA內容較多,包括兩種恢復策略,包括分支事務并發執行,我們這里的討論,僅包括最簡單的SAGA
SAGA適用的場景較多,長事務適用,對中間結果不敏感的業務場景適用
如果讀者想要進一步研究SAGA,可參考DTM,里面包括了SAGA成功、失敗回滾的例子,還包括各類網絡異常的處理。
三、TCC
關于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。
TCC分為3個階段
- Try 階段:嘗試執行,完成所有業務檢查(一致性), 預留必須業務資源(準隔離性)
- Confirm 階段:確認執行真正執行業務,不作任何業務檢查,只使用 Try 階段預留的業務資源,Confirm 操作要求具備冪等設計,Confirm 失敗后需要進行重試。
- Cancel 階段:取消執行,釋放 Try 階段預留的業務資源。Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致,要求滿足冪等設計。
把上面的轉賬作為例子,通常會在Try里面凍結金額,但不扣款,Confirm里面扣款,Cancel里面解凍金額,一個成功完成的TCC事務時序圖如下:
TCC的Confirm/Cancel階段在業務邏輯上是不允許返回失敗的,如果因為網絡或者其他臨時故障,導致不能返回成功,TM會不斷的重試,直到Confirm/Cancel返回成功。
TCC特點如下:
- 并發度較高,無長期資源鎖定。
- 開發量較大,需要提供Try/Confirm/Cancel接口。
- 一致性較好,不會發生SAGA已扣款最后又轉賬失敗的情況
- TCC適用于訂單類業務,對中間狀態有約束的業務
如果讀者想要進一步研究TCC,可參考DTM
四、本地消息表
本地消息表這個方案最初是 ebay 架構師 Dan Pritchett 在 2008 年發表給 ACM 的文章。設計核心是將需要分布式處理的任務通過消息的方式來異步確保執行。
大致流程如下:
寫本地消息和業務操作放在一個事務里,保證了業務和發消息的原子性,要么他們全都成功,要么全都失敗。
容錯機制:
- 扣減余額事務 失敗時,事務直接回滾,無后續步驟
- 輪序生產消息失敗, 增加余額事務失敗都會進行重試
本地消息表的特點:
- 不支持回滾
- 輪詢生產消息難實現,如果定時輪詢會延長事務總時長,如果訂閱binlog則開發維護困難
適用于可異步執行的業務,且后續操作無需回滾的業務
五、事務消息
在上述的本地消息表方案中,生產者需要額外創建消息表,還需要對本地消息表進行輪詢,業務負擔較重。阿里開源的RocketMQ 4.3之后的版本正式支持事務消息,該事務消息本質上是把本地消息表放到RocketMQ上,解決生產端的消息發送與本地事務執行的原子性問題。
事務消息發送及提交:
- 發送消息(half消息)
- 服務端存儲消息,并響應消息的寫入結果
- 根據發送結果執行本地事務(如果寫入失敗,此時half消息對業務不可見,本地邏輯不執行)
- 根據本地事務狀態執行Commit或者Rollback(Commit操作發布消息,消息對消費者可見)
正常發送的流程圖如下:
補償流程:
對沒有Commit/Rollback的事務消息(pending狀態的消息),從服務端發起一次“回查”
Producer收到回查消息,返回消息對應的本地事務的狀態,為Commit或者Rollback
事務消息方案與本地消息表機制非常類似,區別主要在于原先相關的本地表操作替換成了一個反查接口
事務消息特點如下:
- 長事務僅需要分拆成多個任務,并提供一個反查接口,使用簡單
- 事務消息的回查沒有好的方案,極端情況可能出現數據錯誤
適用于可異步執行的業務,且后續操作無需回滾的業務
如果讀者想要進一步研究事務消息,可參考DTM,也可以參考Rocketmq
六、最大努力通知
發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。具體包括:
有一定的消息重復通知機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對消息重復通知。
消息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費消息后要再次消費,此時可由接收方主動向通知方查詢消息信息來滿足需求。
前面介紹的的本地消息表和事務消息都屬于可靠消息,與這里介紹的最大努力通知有什么不同?
可靠消息一致性,發起通知方需要保證將消息發出去,并且將消息發到接收通知方,消息的可靠性關鍵由發起通知方來保證。
最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能消息接收不到,此時需要接收通知方主動調用發起通知方的接口查詢業務處理結果,通知的可靠性關鍵在接收通知方。
解決方案上,最大努力通知需要:
- 提供接口,讓接受通知放能夠通過接口查詢業務處理結果
- 消息隊列ACK機制,消息隊列按照間隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知間隔 ,直到達到通知要求的時間窗口上限。之后不再通知
最大努力通知適用于業務通知類型,例如微信交易的結果,就是通過最大努力通知方式通知各個商戶,既有回調通知,也有交易查詢接口
七、AT事務模式
這是阿里開源項目seata中的一種事務模式,在螞蟻金服也被稱為FMT。優點是該事務模式使用方式,類似XA模式,業務無需編寫各類補償操作,回滾由框架自動完成,該模式缺點也較多,一方面類似XA,存在較長時間的鎖,不滿足高并發的場景;另一方面存在臟回滾之類的問題,容易引發數據不一致。關于AT與XA之間的對比研究,可以參考: XA vs AT
分布式事務的新方案
https://github.com/dtm-labs/dtm在研究了經典的各種解決方案之后,根據許多公司使用dtm的經驗,提出了新的更加方便易用的新方案,幫助大家更好更快的解決跨庫跨服務的數據一致性問題。
二階段消息
dtm首創了一種二階段消息架構,該架構大大優于本地消息表和事務消息,可以完美替代本地消息表和事務消息。
二階段消息的工作時序圖如下:
對比本地消息表和事務消息,二階段消息有以下優點:
- 不需要隊列,因此不需要消費者,用戶就是簡單的調用API
- 二階段消息也有回查,但是回查是框架自動處理,且保證數據正確
關于二階段消息的詳情可以參考這里 二階段消息
工作流模式
前面介紹了XA、Saga、Tcc等模式,每種模式都有相關的優點和缺點,適合不同的業務。有沒有辦法結合他們的優點,對不同的業務使用不同的模式,然后將他們融合組成一個全局事務?
dtm首創的Workflow模式能夠支持上述三種模式的混合使用,同時也允許HTTP/gRPC/本地事務混合使用,具備非常大的靈活性,可以解決各式各樣的業務場景。
關于Workflow的詳情可以參考這里 Workflow
異常處理
在分布式事務的各個環節都有可能出現網絡以及業務故障等問題,這些問題需要分布式事務的業務方做到防空回滾,冪等,防懸掛三個特性。
異常情況
下面以TCC事務說明這些異常情況:
空回滾:
在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然后直接返回成功。
出現原因是當一個分支事務所在服務宕機或網絡異常,分支事務調用記錄為失敗,這個時候其實是沒有執行Try階段,當故障恢復后,分布式事務進行回滾則會調用二階段的Cancel方法,從而形成空回滾。
冪等:
由于任何一個請求都可能出現網絡異常,出現重復請求,所以所有的分布式事務分支,都需要保證冪等性
懸掛:
懸掛就是對于一個分布式事務,其二階段 Cancel 接口比 Try 接口先執行。
出現原因是在 RPC 調用分支事務try時,先注冊分支事務,再執行RPC調用,如果此時 RPC 調用的網絡發生擁堵,RPC 超時以后,TM就會通知RM回滾該分布式事務,可能回滾完成后,Try 的 RPC 請求才到達參與者真正執行。
下面看一個網絡異常的時序圖,更好的理解上述幾種問題
- 業務處理請求4的時候,Cancel在Try之前執行,需要處理空回滾
- 業務處理請求6的時候,Cancel重復執行,需要冪等
- 業務處理請求8的時候,Try在Cancel后執行,需要處理懸掛
面對上述復雜的網絡異常情況,目前看到各家建議的方案都是業務方通過唯一鍵,去查詢相關聯的操作是否已完成,如果已完成則直接返回成功。相關的判斷邏輯較復雜,易出錯,業務負擔重。
子事務屏障
在項目https://github.com/dtm-labs/dtm中,出現了一種子事務屏障技術,使用該技術,能夠達到這個效果,看示意圖:
所有這些請求,到了子事務屏障后:不正常的請求,會被過濾;正常請求,通過屏障。開發者使用子事務屏障之后,前面所說的各種異常全部被妥善處理,業務開發人員只需要關注實際的業務邏輯,負擔大大降低。
子事務屏障提供了方法CallWithDB,方法的原型為:
func (bb *BranchBarrier) CallWithDB(db *sql.DB, busiCall BusiFunc) error
業務開發人員,在busiCall里面編寫自己的相關邏輯,調用該函數。CallWithDB保證,在空回滾、懸掛等場景下,busiCall不會被調用;在業務被重復調用時,有冪等控制,保證只被提交一次。
子事務屏障會管理TCC、SAGA等,也可以擴展到其他領域
子事務屏障原理
子事務屏障技術的原理是,在本地數據庫,建立分支事務狀態表sub_trans_barrier,唯一鍵為全局事務id-子事務id-子事務分支名稱(try|confirm|cancel)
- 開啟本地事務
- 對于當前操作op(try|confirm|cancel),insert ignore一條數據gid-branchid-op,如果插入不成功,提交事務返回成功(常見的冪等控制方法)
- 如果當前操作是cancel,那么在insert ignore一條數據gid-branchid-try,如果插入成功(注意是成功),則提交事務返回成功
- 調用屏障內的業務邏輯,如果業務返回成功,則提交事務返回成功;如果業務返回失敗,則回滾事務返回失敗
在此機制下,解決了網絡異常相關的問題
- 空補償控制--如果Try沒有執行,直接執行了Cancel,那么Cancel插入gid-branchid-try會成功,不走屏障內的邏輯,保證了空補償控制
- 冪等控制--任何一個分支都無法重復插入唯一鍵,保證了不會重復執行
- 防懸掛控制--Try在Cancel之后執行,那么插入的gid-branchid-try不成功,就不執行,保證了防懸掛控制
對于SAGA等,也是類似的機制。
子事務屏障小結
子事務屏障技術,為https://github.com/dtm-labs/dtm首創,它的意義在于設計簡單易實現的算法,提供了簡單易用的接口,在首創,它的意義在于設計簡單易實現的算法,提供了簡單易用的接口,在這兩項的幫助下,開發人員徹底的從網絡異常的處理中解放出來。
該技術目前需要搭配dtm-labs/dtm事務管理器,目前SDK已經提供給Go、Python、C#、Java語言的開發者。其他語言的sdk正在規劃中。對于其他的分布式事務框架,只要提供了合適的分布式事務信息,能夠按照上述原理,快速實現該技術。
dtm不僅實現了基于SQL數據庫的子事務屏障,還實現了基于Redis、Mongo的子事務屏障,因而能夠組合Redis、Mongo、SQL數據庫,以及其他支持事務的存儲引擎,形成一個全局事務,提供非常大的靈活性。
分布式事務實踐
我們還有很多文章,通過一個個實際的例子,帶您快速上手開發一個分布式事務,其中包括各種語言的版本,如果您有興趣,可以訪問:dtm 教程

浙公網安備 33010602011771號