分布式事務處理方案大 PK!
說好了寫 TienChin 項目的,最近這個分布式事務算是一個支線任務吧,今天是最后一篇,松哥再來一個短篇和小伙伴們總結一下分布式事務。
首先先說一個大原則:分布式事務能不用就不要用,畢竟這個用起來還是有一些麻煩的。當然,不用和不會用可是兩碼事。
1. 分布式事務基礎理論
學習分布式事務,有一些基礎理論需要我們先來了解下。
1.1 本地事務
本地事務是指將多條語句作為一個整體進行操作的功能,通過數據庫事務可以確保該事務范圍內的所有操作都可以全部成功或者全部失敗,如果事務失敗,那么效果就和沒有執行這些SQL一樣,不會對數據庫數據有任何改動。也就是事務具有原子性,一個事務中的一系列操作要么全部成功,要么全部失敗。一般來說,事務具有 4 個屬性:
- Atomic:原子性,將一個事務中的所有 SQL 作為原子工作單元執行,要么全部執行,要么全部不執行;
- Consistent:一致性,事務完成后,所有數據的狀態都是一致的,以銀行轉帳為例,如果 A 賬戶減去了 100,則 B 賬戶則必定加上了 100;
- Isolation:隔離性,如果有多個事務并發執行,每個事務作出的修改必須與其他事務隔離;
- Duration:持久性,即事務完成后,對數據庫數據的修改被持久化存儲。
這四個屬性通常稱為 ACID 特性。
這塊松哥之前專門錄過相關的視頻,這里就不再贅述了。
- https://www.bilibili.com/video/BV1Eq4y1R7Ds
1.2 分布式事務
當我們的項目上了微服務之后,分布式事務就是一個比較常見的問題了,我們也會遇到很多相關的場景。
就拿我們前兩天講的商品下單的分布式事務的案例來說,像下面這樣,一共有五個服務,架構如下圖:
- eureka:這是服務注冊中心。
- account:這是賬戶服務,可以查詢/修改用戶的賬戶信息(主要是賬戶余額)。
- order:這是訂單服務,可以下訂單。
- storage:這是一個倉儲服務,可以查詢/修改商品的庫存數量。
- bussiness:這是業務,用戶下單操作將在這里完成。
當用戶想要下單的時候,調用了 bussiness 中的接口,bussiness 中的接口又調用了它自己的 service,在 service 中,通過 feign 調用 storage 中的接口去扣庫存,然后再通過 feign 調用 order 中的接口去創建訂單(order 在創建訂單的時候,不僅會創建訂單,還會扣除用戶賬戶的余額)。
這三個操作,我們希望他們能夠同時成功或者同時失敗。然而如上圖所示,三個微服務都有自己的 DB,這是三個完全不同的 DB,相當于三個不同的本地事務,按照傳統的本地事務規則,我們顯然是無法實現三個操作同時成功或者同時失敗的。
想要實現 storage、order 以及 account 中的操作同時成功或者同時失敗,就得考慮分布式事務了。
最后,我們再來看看分布式事務的概念:分布式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于的不同節點之上,數據庫的操作執行成功與否,不僅取決于本地 DB 的執行結果,也取決于第三方系統的執行結果。而分布式事務就保證這些操作要么全部成功,要么全部失敗。本質上,分布式事務就是為了保證不同數據庫的數據一致性。
1.3 CAP
CAP 定理(CAP theorem),有時候又被稱作布魯爾定理(Brewer's theorem),它指出對于一個分布式計算系統來說,不可能同時滿足以下三點:
一致性(Consistency):在分布式系統中的所有數據備份,在同一時刻是否具備同樣的值。(等同于所有節點訪問同一份最新的數據副本)。
可用性(Availability):在集群中一部分節點故障后,集群整體是否還能響應客戶端的讀寫請求。(對數據更新具備高可用性)。
分區容錯性(Partition tolerance):這個我覺得可能對有的小伙伴來說有點難以理解,我就簡單說一下,先來說分區:因為我們是分布式系統,分布式系統中不同的微服務位于不同的網絡節點上,當發生網絡故障或者節點故障的時候,不同的服務之間就無法通信了,也就是說發生了分區;再來看分區容錯性:這是說,當我們的系統中出現分區的時候,系統還要能運行,不能罷工!一般來說,在一個分布式系統中,分區發生的概率還是比較大的,不會發生分區的系統,那就不是分布式系統了,而是單體應用了。
CAP 原則的精髓就是要么 AP,要么 CP,要么 AC,但是不存在 CAP。因為在分布式系統內,P 是必然的發生的,不選 P,一旦發生分區,整個分布式系統就完全無法使用了,這樣的系統就太脆弱了。所以對于分布式系統,我們只能能考慮當發生分區錯誤時,如何選擇一致性和可用性(選擇一致性,意味著服務在某段時間內不可用,選擇了可用性,意味著服務雖然一直可用但是返回的數據卻不一致)。
而根據一致性和可用性的選擇不同,開源的分布式系統往往又被分為 CP 系統和 AP 系統。
當一套系統在發生分區故障后,客戶端的任何請求都被卡死或者超時,但是系統的每個節點總是會返回一致的數據,則這套系統就是 CP 系統,經典的比如 Zookeeper。
如果一套系統發生分區故障后,客戶端依然可以訪問系統,但是獲取的數據有的是新的數據,有的還是老數據,那么這套系統就是 AP 系統,經典的比如 Eureka。
1.4 BASE
因為無法同時滿足 CAP,所以又有了 BASE 理論,BASE 理論指的是:
- 基本可用 Basically Available:分布式系統在出現故障的時候,允許損失部分可用性,即保證核心可用。
- 軟狀態 Soft State:允許系統存在中間狀態,而該中間狀態不會影響系統整體可用性。
- 終一致性 Eventual Consistency:系統中的所有數據副本經過一定時間后,最終能夠達到一致的狀態。
BASE 理論的核心思想是即便無法做到強一致性,但應該采用適合的方式保證最終一致性。
BASE 理論本質上是對 CAP 理論的延伸,是對 CAP 中 AP 方案的一個補充。
1.5 剛柔并濟
事務有剛性事務和柔性事務之分。
剛性事務(如單數據庫中的本地事務)完全遵循 ACID 規范,即數據庫事務正確執行的四個基本要素:
- 原子性(Atomicity)
- 一致性(Consistency)
- 隔離性(Isolation)
- 持久性(Durability)
柔性事務,主要就是只分布式事務了,柔性事務為了滿足可用性、性能與降級服務的需要,降低一致性(Consistency)與隔離性(Isolation)的要求,遵守 BASE 理論:
- 基本業務可用性(Basic Availability)
- 柔性狀態(Soft state)
- 最終一致性(Eventual consistency)
當然,柔性事務也部分遵循 ACID 規范:
- 原子性:嚴格遵循
- 一致性:事務完成后的一致性嚴格遵循;事務中的一致性可適當放寬
- 隔離性:并行事務間不可影響;事務中間結果可見性允許安全放寬
- 持久性:嚴格遵循
柔性事務有不同的分類,不過基本上都可以看作是分布式事務的解決方案:
- 兩階段型:分布式事務二階段提交,對應技術上的 XA、JTA/JTS,這是分布式環境下事務處理的典型模式。
- 補償型:我們之前文章介紹的 TCC,就算是一種補償型事務,在 Try 成功的情況下,如果事務要回滾,Cancel 將作為一個補償機制,回滾 Try 操作;TCC 各操作事務本地化,且盡早提交(沒有兩階段約束);當全局事務要求回滾時,通過另一個本地事務實現“補償”行為。TCC 是將資源層的二階段提交協議轉換到業務層,成為業務模型中的一部分。
- 異步確保型:將一些有同步沖突的事務操作變為異步操作,避免對數據庫事務的爭用,如消息事務機制。
- 最大努力通知型:通過通知服務器(消息通知)進行,允許失敗,有補充機制。
2. 分布式事務實踐
2.1 XA
先來說說 XA。
XA 是一種典型的兩階段提交(2PC,Two-phase commit protocol),而兩階段提交是一種強一致性設計,在兩階段提交中,一般會引入一個事務協調者的角色來協調管理各個事務參與者,例如我們之前文章中使用的 seata-server 其實是就是一個事務協調者。所謂的兩階段分別指的是準備和提交兩個階段。
XA 規范 是 X/Open 組織定義的分布式事務處理(DTP,Distributed Transaction Processing)標準。
XA 規范描述了全局的事務管理器與局部的資源管理器之間的接口。XA規范的目的是允許多個資源(如數據庫,應用服務器,消息隊列等)在同一事務中訪問,這樣可以使 ACID 屬性跨越應用程序而保持有效。
XA 規范使用兩階段提交來保證所有資源同時提交或回滾任何特定的事務。
XA 規范在上世紀 90 年代初就被提出。目前,幾乎所有主流的數據庫如 MySQL、Oracle、MSSQL 等都對 XA 規范提供了支持。
XA 事務的基礎是兩階段提交協議。需要有一個事務協調者來保證所有的事務參與者都完成了準備工作(第一階段)。如果協調者收到所有參與者都準備好的消息,就會通知所有的事務都可以提交了(第二階段)。MySQL 在這個 XA 事務中扮演的是參與者的角色,而不是協調者(事務管理器)。
MySQL 的 XA 事務分為內部 XA 和外部 XA。外部 XA 可以參與到外部的分布式事務中,需要應用層介入作為協調者;內部 XA 事務用于同一實例下跨多引擎事務,由 Binlog 作為協調者,比如在一個存儲引擎提交時,需要將提交信息寫入二進制日志,這就是一個分布式內部 XA 事務,只不過二進制日志的參與者是 MySQL 本身。MySQL 在 XA 事務中扮演的是一個參與者的角色,而不是協調者。
XA 事務的特點是:
- 簡單易理解,開發較容易。
- 對資源進行了長時間的鎖定,并發度低。
2.2 3PC
3PC 主要是為了彌補 2PC 的不足而產生的,2PC 有哪些不足呢?
- 同步阻塞:2PC 在執行過程中,所有參與節點(也就是一個分支事務)都是事務阻塞型的,當參與者占有公共資源時,其他第三方節點訪問公共資源不得不處于阻塞狀態,也就是在 2PC 執行的過程中,資源是被鎖住的。
- 單點故障:在 2PC 中,事務協調者扮演了舉足輕重的作用,由于事務協調者的重要性,一旦事務協調者發生故障,事務的參與者就會一直阻塞下去。尤其是在第二階段,如果協調者發生故障,那么所有的參與者還都處于鎖定事務資源的狀態中,而無法繼續完成事務操作。還有一個問題,就是當事務協調者發出 commit 指令之前,如果宕機了,此時雖然可以重新選舉一個新的協調者出來,但是還是無法解決因為事務協調者宕機導致的事務參與者處于阻塞狀態的問題。
3PC 則嘗試解決 2PC 的這些問題。3PC 主要是把 2PC 中的第一階段再次一分為二,這樣 3PC 就有 CanCommit、PreCommit 以及 DoCommit 三個不同的階段。不過 3PC 并不能解決 2PC 的所有問題,3PC 主要解決了單點故障問題,并且減少了阻塞。一旦事務參與者(分支事務)無法及時收到來自事務協調者的信息,那么分支事務會默認執行 commit,而不會一直持有事務資源并處于阻塞狀態,不過這種機制也帶來了新的問題,假設事務協調者發送了 abort 指令給各個分支事務,然而由于網絡問題導致分支事務沒有及時接收到該指令,那么分支事務在等待超時之后執行了 commit 操作,這樣就和其他接到 abort 命令并執行回滾的分支事務之間存在數據不一致的情況。
我們來看看 3PC 的流程:
- CanCommit 階段:這個階段所做的事很簡單,就是事務協調者詢問各個分支事務,你是否有能力完成此次事務?如果都返回 yes,則進入第二階段;有一個返回 no 或等待響應超時,則中斷事務,并向所有分支事務發送 abort 請求。
- PreCommit 階段:此時事務協調者會向所有的分支事務發送 PreCommit 請求,分支事務收到后開始執行事務操作,并將 Undo 和 Redo 信息記錄到事務日志中。分支執行完事務操作后(此時屬于未提交事務的狀態),就會向事務協調者反饋“Ack”表示我已經準備好提交了,并等待協調者的下一步指令。
- DoCommit 階段:在階段二中如果所有的分支事務節點都可以進行 PreCommit 提交,那么事務協調者就會從“預提交狀態”轉變為“提交狀態”,然后向所有的分支事務節點發送"doCommit"請求,分支事務節點在收到提交請求后就會各自執行事務提交操作,并向協調者節點反饋“Ack”消息,協調者收到所有參與者的 Ack 消息后完成事務。
相反,如果有一個分支事務節點未完成 PreCommit 的反饋或者反饋超時,那么協調者都會向所有的參與者節點發送 abort 請求,從而中斷事務。
2.3 TCC
關于 TCC(Try-Confirm-Cancel)的概念,最早是由 Pat Helland 于 2007 年發表的一篇名為《Life beyond Distributed Transactions:an Apostate’s Opinion》的論文提出。
TCC 模式主要有如下一些優缺點:
優點:
- 性能提升:通過具體業務來實現控制資源鎖的粒度變小,不會鎖定整個資源。
- 數據最終一致性:基于 Confirm 和 Cancel 的冪等性,保證事務最終完成確認或者取消,保證數據的一致性。
- 可靠性:解決了 XA 協議的協調者單點故障問題,由主業務方發起并控制整個業務活動,業務活動管理器也變成多點,引入集群。
缺點:
- 對微服務的侵入性強,微服務的每個事務都必須實現 try,confirm,cancel 等 3 個方法,開發成本高,今后維護改造的成本也高。
- 為了達到事務的一致性要求,try,confirm、cancel 接口必須實現等冪性操作,這在一定程度上增加了開發工作量。
TCC 主要是兩個階段,步驟如下:
- Try 階段(一階段):嘗試執行,完成所有業務檢查(一致性), 預留必須業務資源(準隔離性)。
- Confirm 階段(二階段):確認執行真正執行業務,不作任何業務檢查,只使用 Try 階段預留的業務資源,Confirm 操作滿足需要滿足冪等性,Confirm 執行失敗后需要進行重試。
- Cancel 階段:取消執行,釋放 Try 階段預留的業務資源,Cancel 操作也需要滿足冪等性。Cancel 階段的異常和 Confirm 階段異常處理方案基本上一致。
在我們之前的文章中,松哥也給大家舉了 TCC 的例子了,這里就不再贅述了。
2.4 SAGA
SAGA 最初出現在 1987 年 Hector Garcaa-Molrna & Kenneth Salem 發表的論文 SAGAS 里。這篇論文的核心思想是將長事務拆分為多個短事務,由 Saga 事務協調器協調,如果每個短事務都成功提交完成,那么全局事務就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。
Saga 事務的特點是:
- 并發度高,不用像 XA 事務那樣長期鎖定資源。
- 需要定義正常操作以及補償操作(回滾),開發量工作量比 XA 大。
- 一致性較弱,對于轉賬,可能發生 A 用戶已扣款,最后轉賬又失敗的情況
SAGA 適用的場景較多,適用于長事務或者對中間結果不敏感的業務場景。
2.5 本地消息表
本地消息表這個方案最初是 ebay 架構師 Dan Pritchett 在 2008 年發表給 ACM 的文章中提出。
顧名思義,本地消息表就是會有一張存放本地消息的表,一般都是放在數據庫中,然后在執行業務的時候將業務的執行和將消息放入消息表中的操作放在同一個事務中,這樣就能保證消息放入本地表以及業務肯定是一起執行成功的。
當一個操作執行成功之后,再去執行下一個操作,如果下一個操作調用成功了好說,消息表的消息狀態可以直接改為已成功;如果下一個任務調用失敗也沒關系,會有后臺任務定時去讀取本地消息表,篩選出還未成功的消息再調用對應的服務(重試),服務更新成功了再變更消息的狀態。
重試就得保證對應服務的方法是冪等的,而且一般重試會有最大次數,超過最大次數可以記錄下報警讓人工處理。
根據上面的描述,小伙伴們其實可以看到,本地消息表其實實現的是最終一致性,容忍了數據暫時不一致的情況。
本地消息表的特點:
- 長事務僅需要分拆成多個任務,使用簡單。
- 生產者需要額外的創建消息表。
- 每個本地消息表都需要進行輪詢(如果有失敗的要重試)。
- 消費者的邏輯如果無法通過重試成功,那么還需要更多的機制,來回滾操作。
根據本地消息表的特點我們可以發現,本地消息表適用于可異步執行且后續操作無需回滾的業務。
2.6 消息事務
這種方案的核心思路,其實就是通過消息中間件來將全局事務轉為本地事務,通過消息中間件來確保各個分支事務最終都能調用成功。
不過后來發現利用 Alibaba 的 RocketMQ(4.3之后)可以更好的實現分布式事務。
RocketMQ 是一種最終一致性的分布式事務,就是說它保證的是消息最終一致性,而不是像 2PC、3PC、TCC 那樣強一致分布式事務,在 RocketMQ 中有一種消息叫做 Half Message,Half Message 是指暫不能被 Consumer 消費的消息,雖然 Producer 已經把消息成功發送到了 Broker 端,但此消息被標記為暫不能投遞狀態,處于該種狀態下的消息稱為半消息,此時需要 Producer 對消息進行二次確認后,Consumer 才能去消費它。
RocketMQ 就是基于 Half Message 來實現的分布式事務,舉一個轉賬的例子:
- A 服務先發送個 Half Message 給 Brock 端,消息中攜帶 B 服務即將要 +100 元的信息。
- 當 A 服務知道 Half Message 發送成功后,那么開始本地事務。
- 執行本地事務(會有三種情況1、執行成功;2、執行失敗;3、網絡等原因導致沒有響應) 3.1 如果本地事務成功,那么 A 向 Broker 服務器發送 Commit,這樣 B 服務就可以消費該 message。3.2 如果本地事務失敗,那么 A 向 Broker 服務器發送 Rollback,那么就會直接刪除上面這條半消息。3.3 如果由于網絡或者生產者應用重啟等原因。導致 A 一直沒有對 Half Message 進行二次確認,此時 Broker 服務器會定時掃描長期處于半消息的消息,會主動詢問 A 端該消息的最終狀態(Commit 或者 Rollback),這個操作也就是所謂的消息回查。
可能有小伙伴會說,那要是 B 最終執行失敗怎么辦?對于這種情況,我們幾乎可以斷定就是代碼有問題所以才引起異常,因為消費端 RocketMQ 有重試機制,如果不是代碼問題一般重試幾次就能成功。
如果是代碼的原因引起多次重試失敗后,也沒有關系,將該異常記錄下來,由人工處理,人工兜底處理后,就可以讓事務達到最終的一致性。
2.7 最大努力通知
發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。具體包括:
- 有一定的消息重試機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對消息進行重試。
- 消息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費消息后要再次消費,此時可由接收方主動向通知方查詢消息信息來滿足需求。
在前面兩個小節介紹的的本地消息表和事務消息都屬于可靠消息,這與我們這里介紹的最大努力通知有什么不同?
- 可靠消息一致性:消息發起方需要保證將消息發出去,并且將消息發到接收方,消息的可靠性關鍵由發起方來保證。
- 最大努力通知:消息發起方盡最大努力將業務處理結果通知給接收方,但是可能消息接收不到,此時需要接收方主動調用發起方的接口查詢業務處理結果,此時消息的可靠性關鍵在接收方。
僅此而已。
在具體的解決方案上,最大努力通知需要消息發起方提供接口,讓被通知方能夠通過接口查詢業務處理結果。
最大努力通知適用于業務通知類型,最常見的場景就是支付回調,支付服務收到第三方服務支付成功通知后,先更新自己庫中訂單支付狀態,然后同步通知訂單服務支付成功。如果此次同步通知失敗,會通過異步腳步不斷重試地調用訂單服務的接口。
最大努力通知更多是業務上的設計,在基礎設施層,可以直接使用二階段消息,或者事務消息、本地消息表等來實現。
3. 小結
好啦,學習分布式事務解決方案,最大的感受就是:沒有銀彈!
參考資料:
https://help.aliyun.com/document_detail/132895.html
https://cloud.tencent.com/developer/article/1860632
https://zh.m.wikipedia.org/zh-hans/CAP%E5%AE%9A%E7%90%86
https://zhuanlan.zhihu.com/p/35616811