前言
這是《Spring Cloud 進階》專欄的第六篇文章,往期文章如下:
- 五十五張圖告訴你微服務的靈魂擺渡者Nacos究竟有多強?
- openFeign奪命連環9問,這誰受得了?
- 阿里面試這樣問:Nacos、Apollo、Config配置中心如何選型?這10個維度告訴你!
- 阿里面試敗北:5種微服務注冊中心如何選型?這幾個維度告訴你!
- 阿里限流神器Sentinel奪命連環 17 問?
這篇文章主要介紹一些目前主流的幾種分布式解決方案以及阿里開源的一站式分布式解決方案Seata。
文章有點長,耐心看完,看完你還不懂分布式事務,歡迎來捶我...............
文章目錄如下:
什么是分布式事務?
分布式對應的是單體架構,互聯網早起單體架構是非常流行的,好像是一個家族企業,大家在一個家里勞作,單體架構如下圖:

但是隨着業務的復雜度提高,大家族人手不夠,此時不得不招人,這樣逐漸演變出了分布式服務,互相協作,每個服務負責不同的業務,架構如下圖:
因此需要服務與服務之間的遠程協作才能完成事務,這種分布式系統環境下由不同的服務之間通過網絡遠程協作完成事務稱之為分布式事務,例如用戶注冊送積分 事務、創建訂單減庫存事務,銀行轉賬事務等都是分布式事務。
典型的場景就是微服務架構 微服務之間通過遠程調用完成事務操作。 比如:訂單微服務和庫存微服務,下單的同時訂單微服務請求庫存微服務減庫存。 簡言之:跨JVM進程產生分布式事務。
什么是CAP原則?
CAP原則又叫CAP定理,同時又被稱作布魯爾定理(Brewer's theorem),指的是在一個分布式系統中,不可能同時滿足以下三點。

一致性(Consistency)
指強一致性,在寫操作完成后開始的任何讀操作都必須返回該值,或者后續寫操作的結果。
也就是說,在一致性系統中,一旦客戶端將值寫入任何一台服務器並獲得響應,那么之后client從其他任何服務器讀取的都是剛寫入的數據
一致性保證了不管向哪台服務器寫入數據,其他的服務器能實時同步數據
可用性(Availability)
可用性(高可用)是指:每次向未崩潰的節點發送請求,總能保證收到響應數據(允許不是最新數據)
分區容忍性(Partition tolerance)
分布式系統在遇到任何網絡分區故障的時候,仍然能夠對外提供滿足一致性和可用性的服務,也就是說,服務器A和B發送給對方的任何消息都是可以放棄的,也就是說A和B可能因為各種意外情況,導致無法成功進行同步,分布式系統要能容忍這種情況。除非整個網絡環境都發生了故障。
為什么只能在A和C之間做出取舍?
分布式系統中,必須滿足 CAP 中的 P,此時只能在 C/A 之間作出取舍。
如果選擇了CA,舍棄了P,說白了就是一個單體架構。
一致性有幾種分類?
CAP理論告訴我們只能在C、A之間選擇,在分布式事務的最終解決方案中一般選擇犧牲一致性來獲取可用性和分區容錯性。
這里的 “犧牲一致性” 並不是完全放棄數據的一致性,而是放棄強一致性而換取弱一致性。
一致性可以分為以下三種:
- 強一致性
- 弱一致性
- 最終一致性
強一致性
系統中的某個數據被成功更新后,后續任何對該數據的讀取操作都將得到更新后的值。
也稱為:原子一致性(Atomic Consistency)、線性一致性(Linearizable Consistency)
簡言之,在任意時刻,所有節點中的數據是一樣的。例如,對於關系型數據庫,要求更新過的數據能被后續的訪問都能看到,這是強一致性。
總結:
- 一個集群需要對外部提供強一致性,所以只要集群內部某一台服務器的數據發生了改變,那么就需要等待集群內其他服務器的數據同步完成后,才能正常的對外提供服務。
- 保證了強一致性,務必會損耗可用性。
弱一致性
系統中的某個數據被更新后,后續對該數據的讀取操作可能得到更新后的值,也可能是更改前的值。
但即使過了不一致時間窗口這段時間后,后續對該數據的讀取也不一定是最新值。
所以說,可以理解為數據更新后,如果能容忍后續的訪問只能訪問到部分或者全部訪問不到,則是弱一致性。
例如12306買火車票,雖然最后看到還剩下幾張余票,但是只要選擇購買就會提示沒票了,這就是弱一致性。
最終一致性
是弱一致性的特殊形式,存儲系統保證在沒有新的更新的條件下,最終所有的訪問都是最后更新的值。
不保證在任意時刻任意節點上的同一份數據都是相同的,但是隨着時間的遷移,不同節點上的同一份數據總是在向趨同的方向變化。
簡單說,就是在一段時間后,節點間的數據會最終達到一致狀態。
總結
弱一致性即使過了不一致時間窗口,后續的讀取也不一定能保證一致,而最終一致過了不一致窗口后,后續的讀取一定一致。
什么是Base理論?
BASE理論是對CAP中的一致性和可用性進行一個權衡的結果,理論的核心思想就是:我們無法做到強一致,但每個應用都可以根據自身的業務特點,采用適當的方式來使系統達到最終一致性。
BA(Basic Available)基本可用
整個系統在某些不可抗力的情況下,仍然能夠保證“可用性”,即一定時間內仍然能夠返回一個明確的結果。這里是屬於基本可用。
基本可用和高可用的區別:
- “一定時間”可以適當延長 當舉行大促(比如秒殺)時,響應時間可以適當延長
- 給部分用戶返回一個降級頁面 給部分用戶直接返回一個降級頁面,從而緩解服務器壓力。但要注意,返回降級頁面仍然是返回明確結果。
S(Soft State)柔性狀態
稱為柔性狀態,是指允許系統中的數據存在中間狀態,並認為該中間狀態的存在不會影響系統的整體可用性,即允許系統不同節點的數據副本之間進行數據同步的過程存在延時。
E(Eventual Consisstency)最終一致性
同一數據的不同副本的狀態,可以不需要實時一致,但一定要保證經過一定時間后仍然是一致的。
分布式事務有哪幾種解決方案?
在分布式架構下,每個節點只知曉自己操作的失敗或者成功,無法得知其他節點的狀態。當一個事務跨多個節點時,為了保持事務的原子性與一致性,而引入一個協調者來統一掌控所有參與者的操作結果,並指示它們是否要把操作結果進行真正的提交或者回滾(rollback)。
2階段提交(2PC)
二階段提交協議(Two-phase Commit,即 2PC)是常用的分布式事務解決方案,即將事務的提交過程分為兩個階段來進行處理。
兩個階段分別為:
- 准備階段
- 提交階段
參與的角色:
- 事務協調者(事務管理器):事務的發起者
- 事務參與者(資源管理器):事務的執行者
准備階段(投票階段)
這是兩階段的第一段,這一階段只是准備階段,由事務的協調者發起詢問參與者是否可以提交事務,但是這一階段並未提交事務,流程圖如下圖:
- 協調者向所有參與者發送事務內容,詢問是否可以提交事務,並等待答復
- 各參與者執行事務操作,將 undo 和 redo 信息記入事務日志中(但不提交事務)
- 如參與者執行成功,給協調者反饋同意,否則反饋中止
提交階段
這一段階段屬於2PC的第二階段(提交 執行階段),協調者發起正式提交事務的請求,當所有參與者都回復同意時,則意味着完成事務,流程圖如下:
- 協調者節點向所有參與者節點發出正式提交(
commit
)的請求。 - 參與者節點正式完成操作,並釋放在整個事務期間內占用的資源。
- 參與者節點向協調者節點發送ack完成消息。
- 協調者節點收到所有參與者節點反饋的ack完成消息后,完成事務。
但是如果任意一個參與者節點在第一階段返回的消息為終止,或者協調者節點在第一階段的詢問超時之前無法獲取所有參與者節點的響應消息時,那么這個事務將會被回滾,回滾的流程圖如下:
- 協調者節點向所有參與者節點發出回滾操作(
rollback
)的請求。 - 參與者節點利用階段1寫入的undo信息執行回滾,並釋放在整個事務期間內占用的資源。
- 參與者節點向協調者節點發送ack回滾完成消息。
- 協調者節點受到所有參與者節點反饋的ack回滾完成消息后,取消事務。
不管最后結果如何,第二階段都會結束當前事務。
二階段提交的事務正常提交的完整流程如下圖:
二階段提交事務回滾的完整流程如下圖:
舉個百米賽跑的例子來具體描述下2PC的流程:學校運動會,有三個同學,分別是A,B,C,2PC流程如下:
- 裁判:A同學准備好了嗎?准備進入第一賽道....
- 裁判:B同學准備好了嗎?准備進入第一賽道....
- 裁判:C同學准備好了嗎?准備進入第一賽道....
- 如果有任意一個同學沒准備好,則裁判下達回滾指令
- 如果裁判收到了所有同學的OK回復,則再次下令跑......
- 裁判:1,2,3 跑............
- A同學沖刺到終點,匯報給裁判
- B,C同學沖刺失敗,匯報給裁判
2PC的缺點
二階段提交看起來確實能夠提供原子性的操作,但是不幸的是,二階段提交還是有幾個缺點的:
- 性能問題:執行過程中,所有參與節點都是事務阻塞型的。當參與者占有公共資源時,其他第三方節點訪問公共資源不得不處於阻塞狀態。
- 可靠性問題:參與者發生故障。協調者需要給每個參與者額外指定超時機制,超時后整個事務失敗。協調者發生故障。參與者會一直阻塞下去。需要額外的備機進行容錯。
- 數據一致性問題:二階段無法解決的問題:協調者在發出
commit
消息之后宕機,而唯一接收到這條消息的參與者同時也宕機了。那么即使協調者通過選舉協議產生了新的協調者,這條事務的狀態也是不確定的,沒人知道事務是否被已經提交。 - 實現復雜:犧牲了可用性,對性能影響較大,不適合高並發高性能場景。
2PC的優點
- 盡量保證了數據的強一致,適合對數據強一致要求很高的關鍵領域。(其實也不能100%保證強一致)
3階段提交(3PC)
三階段提交協議,是二階段提交協議的改進版本,三階段提交有兩個改動點。
- 在協調者和參與者中都引入超時機制
- 在第一階段和第二階段中插入一個准備階段。保證了在最后提交階段之前各參與節點的狀態是一致的。
也就是說,除了引入超時機制之外,3PC把2PC的准備階段再次一分為二,這樣三階段提交就有CanCommit
、PreCommit
、DoCommit
三個階段。處理流程如下:
階段一:CanCommit階段
3PC的CanCommit
階段其實和2PC的准備階段很像。協調者向參與者發送commit
請求,參與者如果可以提交就返回Yes響應,否則返回No響應。
- 事務詢問:協調者向所有參與者發出包含事務內容的
canCommit
請求,詢問是否可以提交事務,並等待所有參與者答復。 - 響應反饋:參與者收到
canCommit
請求后,如果認為可以執行事務操作,則反饋 yes 並進入預備狀態,否則反饋 no。
CanCommit階段流程如下圖:
階段二:PreCommit階段
協調者根據參與者的反應情況來決定是否可以進行事務的PreCommit
操作。根據響應情況,有以下兩種可能。
- 假如所有參與者均反饋 yes,協調者預執行事務。
- 發送預提交請求 :協調者向參與者發送
PreCommit
請求,並進入准備階段 - 事務預提交 :參與者接收到
PreCommit
請求后,會執行事務操作,並將undo
和redo
信息記錄到事務日志中(但不提交事務) - 響應反饋 :如果參與者成功的執行了事務操作,則返回ACK響應,同時開始等待最終指令。
- 發送預提交請求 :協調者向參與者發送
- 假如有任何一個參與者向協調者發送了No響應,或者等待超時之后,協調者都沒有接到參與者的響應,那么就執行事務的中斷。
- 發送中斷請求 :協調者向所有參與者發送
abort
請求。 - 中斷事務 :參與者收到來自協調者的
abort
請求之后(或超時之后,仍未收到協調者的請求),執行事務的中斷。
- 發送中斷請求 :協調者向所有參與者發送
階段三:doCommit階段
該階段進行真正的事務提交,也可以分為以下兩種情況。
進入階段 3 后,無論協調者出現問題,或者協調者與參與者網絡出現問題,都會導致參與者無法接收到協調者發出的 do Commit 請求或 abort 請求。此時,參與者都會在等待超時之后,繼續執行事務提交。
- 執行提交
- 發送提交請求 協調接收到參與者發送的ACK響應,那么他將從預提交狀態進入到提交狀態。並向所有參與者發送
doCommit
請求。 - 事務提交 參與者接收到
doCommit
請求之后,執行正式的事務提交。並在完成事務提交之后釋放所有事務資源。 - 響應反饋 事務提交完之后,向協調者發送ack響應。
- 完成事務 協調者接收到所有參與者的ack響應之后,完成事務。
- 發送提交請求 協調接收到參與者發送的ACK響應,那么他將從預提交狀態進入到提交狀態。並向所有參與者發送
- 中斷事務:任何一個參與者反饋 no,或者等待超時后協調者尚無法收到所有參與者的反饋,即中斷事務
- 發送中斷請求 如果協調者處於工作狀態,向所有參與者發出 abort 請求
- 事務回滾 參與者接收到abort請求之后,利用其在階段二記錄的undo信息來執行事務的回滾操作,並在完成回滾之后釋放所有的事務資源。
- 反饋結果 參與者完成事務回滾之后,向協調者反饋ACK消息
- 中斷事務 協調者接收到參與者反饋的ACK消息之后,執行事務的中斷。
優點
相比二階段提交,三階段提交降低了阻塞范圍,在等待超時后協調者或參與者會中斷事務。避免了協調者單點問題,階段 3 中協調者出現問題時,參與者會繼續提交事務。
缺點
數據不一致問題依然存在,當在參與者收到 preCommit
請求后等待 doCommit
指令時,此時如果協調者請求中斷事務,而協調者無法與參與者正常通信,會導致參與者繼續提交事務,造成數據不一致。
TCC(事務補償)
TCC(Try Confirm Cancel)方案是一種應用層面侵入業務的兩階段提交。是目前最火的一種柔性事務方案,其核心思想是:針對每個操作,都要注冊一個與其對應的確認和補償(撤銷)操作。
TCC分為兩個階段,分別如下:
- 第一階段:Try(嘗試),主要是對業務系統做檢測及資源預留 (加鎖,鎖住資源)
- 第二階段:本階段根據第一階段的結果,決定是執行confirm還是cancel
- Confirm(確認):執行真正的業務(執行業務,釋放鎖)
- Cancle(取消):是預留資源的取消(出問題,釋放鎖)
為了方便理解,下面以電商下單為例進行方案解析,這里把整個過程簡單分為扣減庫存,訂單創建 2 個步驟,庫存服務和訂單服務分別在不同的服務器節點上。
假設商品庫存為 100,購買數量為 2,這里檢查和更新庫存的同時,凍結用戶購買數量的庫存,同時創建訂單,訂單狀態為待確認。
①Try 階段
TCC 機制中的 Try 僅是一個初步操作,它和后續的確認一起才能真正構成一個完整的業務邏輯,這個階段主要完成:
- 完成所有業務檢查( 一致性 ) 。
- 預留必須業務資源( 准隔離性 ) 。
- Try 嘗試執行業務。
②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 事務機制以初步操作(Try)為中心的,確認操作(Confirm)和取消操作(Cancel)都是圍繞初步操作(Try)而展開。因此,Try 階段中的操作,其保障性是最好的,即使失敗,仍然有取消操作(Cancel)可以將其執行結果撤銷。
- Try階段執行成功並開始執行
Confirm
階段時,默認Confirm
階段是不會出錯的。也就是說只要Try
成功,Confirm
一定成功(TCC設計之初的定義) 。 - Confirm與Cancel如果失敗,由TCC框架進行重試補償
- 存在極低概率在CC環節徹底失敗,則需要定時任務或人工介入
方案總結
TCC 事務機制相對於傳統事務機制(X/Open XA),TCC 事務機制相比於上面介紹的 XA 事務機制,有以下優點:
- 性能提升:具體業務來實現控制資源鎖的粒度變小,不會鎖定整個資源。
- 數據最終一致性:基於 Confirm 和 Cancel 的冪等性,保證事務最終完成確認或者取消,保證數據的一致性。
- 可靠性:解決了 XA 協議的協調者單點故障問題,由主業務方發起並控制整個業務活動,業務活動管理器也變成多點,引入集群。
缺點:
- TCC 的 Try、Confirm 和 Cancel 操作功能要按具體業務來實現,業務耦合度較高,提高了開發成本。
本地消息表
本地消息表的方案最初是由 eBay 提出,核心思路是將分布式事務拆分成本地事務進行處理。
角色:
- 事務主動方
- 事務被動方
通過在事務主動發起方額外新建事務消息表,事務發起方處理業務和記錄事務消息在本地事務中完成,輪詢事務消息表的數據發送事務消息,事務被動方基於消息中間件消費事務消息表中的事務。
這樣可以避免以下兩種情況導致的數據不一致性:
- 業務處理成功、事務消息發送失敗
- 業務處理失敗、事務消息發送成功
整體的流程如下圖:
上圖中整體的處理步驟如下:
- ①:事務主動方在同一個本地事務中處理業務和寫消息表操作
- ②:事務主動方通過消息中間件,通知事務被動方處理事務通知事務待消息。消息中間件可以基於 Kafka、RocketMQ 消息隊列,事務主動方主動寫消息到消息隊列,事務消費方消費並處理消息隊列中的消息。
- ③:事務被動方通過消息中間件,通知事務主動方事務已處理的消息。
- ④:事務主動方接收中間件的消息,更新消息表的狀態為已處理。
一些必要的容錯處理如下:
- 當①處理出錯,由於還在事務主動方的本地事務中,直接回滾即可
- 當②、③處理出錯,由於事務主動方本地保存了消息,只需要輪詢消息重新通過消息中間件發送,事務被動方重新讀取消息處理業務即可。
- 如果是業務上處理失敗,事務被動方可以發消息給事務主動方回滾事務
- 如果事務被動方已經消費了消息,事務主動方需要回滾事務的話,需要發消息通知事務主動方進行回滾事務。
優點
- 從應用設計開發的角度實現了消息數據的可靠性,消息數據的可靠性不依賴於消息中間件,弱化了對 MQ 中間件特性的依賴。
- 方案輕量,容易實現。
缺點
- 與具體的業務場景綁定,耦合性強,不可公用。
- 消息數據與業務數據同庫,占用業務系統資源。
- 業務系統在使用關系型數據庫的情況下,消息服務性能會受到關系型數據庫並發性能的局限。
MQ事務方案(可靠消息事務)
基於 MQ 的分布式事務方案其實是對本地消息表的封裝,將本地消息表基於 MQ 內部,其他方面的協議基本與本地消息表一致。
MQ事務方案整體流程和本地消息表的流程很相似,如下圖:
從上圖可以看出和本地消息表方案唯一不同就是將本地消息表存在了MQ內部,而不是業務數據庫中。
那么MQ內部的處理尤為重要,下面主要基於 RocketMQ 4.3 之后的版本介紹 MQ 的分布式事務方案。
在本地消息表方案中,保證事務主動方發寫業務表數據和寫消息表數據的一致性是基於數據庫事務,RocketMQ 的事務消息相對於普通 MQ提供了 2PC 的提交接口,方案如下:
正常情況:事務主動方發消息
這種情況下,事務主動方服務正常,沒有發生故障,發消息流程如下:
- 步驟①:發送方向 MQ 服務端(MQ Server)發送 half 消息。
- 步驟②:MQ Server 將消息持久化成功之后,向發送方 ack 確認消息已經發送成功。
- 步驟③:發送方開始執行本地事務邏輯。
- 步驟④:發送方根據本地事務執行結果向 MQ Server 提交二次確認(commit 或是 rollback)。
- 步驟⑤:MQ Server 收到 commit 狀態則將半消息標記為可投遞,訂閱方最終將收到該消息;MQ Server 收到 rollback 狀態則刪除半消息,訂閱方將不會接受該消息。
異常情況:事務主動方消息恢復
在斷網或者應用重啟等異常情況下,圖中 4 提交的二次確認超時未到達 MQ Server,此時處理邏輯如下:
- 步驟⑤:MQ Server 對該消息發起消息回查。
- 步驟⑥:發送方收到消息回查后,需要檢查對應消息的本地事務執行的最終結果。
- 步驟⑦:發送方根據檢查得到的本地事務的最終狀態再次提交二次確認。
- 步驟⑧:MQ Server基於 commit/rollback 對消息進行投遞或者刪除。
優點
相比本地消息表方案,MQ 事務方案優點是:
- 消息數據獨立存儲 ,降低業務系統與消息系統之間的耦合。
- 吞吐量大於使用本地消息表方案。
缺點
- 一次消息發送需要兩次網絡請求(half 消息 + commit/rollback 消息) 。
- 業務處理服務需要實現消息狀態回查接口。
最大努力通知
最大努力通知也稱為定期校對,是對MQ事務方案的進一步優化。它在事務主動方增加了消息校對的接口,如果事務被動方沒有接收到消息,此時可以調用事務主動方提供的消息校對的接口主動獲取。
最大努力通知的整體流程如下圖:
在可靠消息事務中,事務主動方需要將消息發送出去,並且消息接收方成功接收,這種可靠性發送是由事務主動方保證的;
但是最大努力通知,事務主動方盡最大努力(重試,輪詢....)將事務發送給事務接收方,但是仍然存在消息接收不到,此時需要事務被動方主動調用事務主動方的消息校對接口查詢業務消息並消費,這種通知的可靠性是由事務被動方保證的。
最大努力通知適用於業務通知類型,例如微信交易的結果,就是通過最大努力通知方式通知各個商戶,既有回調通知,也有交易查詢接口。
Saga 事務
Saga 事務源於 1987 年普林斯頓大學的 Hecto 和 Kenneth 發表的如何處理 long lived transaction(長活事務)論文。
Saga 事務核心思想是將長事務拆分為多個本地短事務,由 Saga 事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次調用補償操作。
Saga 事務基本協議如下:
- 每個 Saga 事務由一系列冪等的有序子事務(sub-transaction) Ti 組成。
- 每個 Ti 都有對應的冪等補償動作 Ci,補償動作用於撤銷 Ti 造成的結果。
TCC事務補償機制有一個預留(Try)動作,相當於先報存一個草稿,然后才提交;Saga事務沒有預留動作,直接提交。
對於事務異常,Saga提供了兩種恢復策略,分別如下:
向后恢復(backward recovery)
在執行事務失敗時,補償所有已完成的事務,是“一退到底”的方式。如下圖:
從上圖可知事務執行到了支付事務T3,但是失敗了,因此事務回滾需要從C3,C2,C1依次進行回滾補償。
對應的執行順序為:T1,T2,T3,C3,C2,C1
這種做法的效果是撤銷掉之前所有成功的子事務,使得整個 Saga 的執行結果撤銷。
向前恢復(forward recovery)
也稱之為:勇往直前,對於執行不通過的事務,會嘗試重試事務,這里有一個假設就是每個子事務最終都會成功。
流程如下圖:
適用於必須要成功的場景,事務失敗了重試,不需要補償。
Saga事務有兩種不同的實現方式,分別如下:
- 命令協調(Order Orchestrator)
- 事件編排(Event Choreographyo)
命令協調
中央協調器(Orchestrator,簡稱 OSO)以命令/回復的方式與每項服務進行通信,全權負責告訴每個參與者該做什么以及什么時候該做什么。整體流程如下圖:
上圖步驟如下:
- 事務發起方的主業務邏輯請求 OSO 服務開啟訂單事務
- OSO 向庫存服務請求扣減庫存,庫存服務回復處理結果。
- OSO 向訂單服務請求創建訂單,訂單服務回復創建結果。
- OSO 向支付服務請求支付,支付服務回復處理結果。
- 主業務邏輯接收並處理 OSO 事務處理結果回復。
中央協調器必須事先知道執行整個訂單事務所需的流程(例如通過讀取配置)。如果有任何失敗,它還負責通過向每個參與者發送命令來撤銷之前的操作來協調分布式的回滾。
基於中央協調器協調一切時,回滾要容易得多,因為協調器默認是執行正向流程,回滾時只要執行反向流程即可。
事件編排
沒有中央協調器(沒有單點風險)時,每個服務產生並觀察其他服務的事件,並決定是否應采取行動。
在事件編排方法中,第一個服務執行一個事務,然后發布一個事件。該事件被一個或多個服務進行監聽,這些服務再執行本地事務並發布(或不發布)新的事件。
當最后一個服務執行本地事務並且不發布任何事件時,意味着分布式事務結束,或者它發布的事件沒有被任何 Saga 參與者聽到都意味着事務結束。
上圖步驟如下:
- 事務發起方的主業務邏輯發布開始訂單事件。
- 庫存服務監聽開始訂單事件,扣減庫存,並發布庫存已扣減事件。
- 訂單服務監聽庫存已扣減事件,創建訂單,並發布訂單已創建事件。
- 支付服務監聽訂單已創建事件,進行支付,並發布訂單已支付事件。
- 主業務邏輯監聽訂單已支付事件並處理。
事件/編排是實現 Saga 模式的自然方式,它很簡單,容易理解,不需要太多的代碼來構建。如果事務涉及 2 至 4 個步驟,則可能是非常合適的。
優點
命令協調設計的優點如下:
- 服務之間關系簡單,避免服務之間的循環依賴關系,因為 Saga 協調器會調用 Saga 參與者,但參與者不會調用協調器。
- 程序開發簡單,只需要執行命令/回復(其實回復消息也是一種事件消息),降低參與者的復雜性。
- 易維護擴展,在添加新步驟時,事務復雜性保持線性,回滾更容易管理,更容易實施和測試。
事件/編排設計優點如下:
- 避免中央協調器單點故障風險。
- 當涉及的步驟較少服務開發簡單,容易實現。
缺點
命令協調設計缺點如下:
- 中央協調器容易處理邏輯容易過於復雜,導致難以維護。
- 存在協調器單點故障風險。
事件/編排設計缺點如下:
- 服務之間存在循環依賴的風險。
- 當涉及的步驟較多,服務間關系混亂,難以追蹤調測。
由於 Saga 模型中沒有 Prepare 階段,因此事務間不能保證隔離性。
當多個 Saga 事務操作同一資源時,就會產生更新丟失、臟數據讀取等問題,這時需要在業務層控制並發,例如:在應用層面加鎖,或者應用層面預先凍結資源。
總結
總結一下各個方案的常見的使用場景:
- 2PC/3PC:依賴於數據庫,能夠很好的提供強一致性和強事務性,但相對來說延遲比較高,比較適合傳統的單體應用,在同一個方法中存在跨庫操作的情況,不適合高並發和高性能要求的場景。
- TCC:適用於執行時間確定且較短,實時性要求高,對數據一致性要求高,比如互聯網金融企業最核心的三個服務:交易、支付、賬務。
- 本地消息表/MQ 事務:都適用於事務中參與方支持操作冪等,對一致性要求不高,業務上能容忍數據不一致到一個人工檢查周期,事務涉及的參與方、參與環節較少,業務上有對賬/校驗系統兜底。
- Saga 事務:由於 Saga 事務不能保證隔離性,需要在業務層控制並發,適合於業務場景事務並發操作同一資源較少的情況。 Saga 相比缺少預提交動作,導致補償動作的實現比較麻煩,例如業務是發送短信,補償動作則得再發送一次短信說明撤銷,用戶體驗比較差。Saga 事務較適用於補償動作容易處理的場景。
什么是Seata?
上面講了這么多的分布式事務的理論知識,都沒看到一個落地的實現,這不是吹牛逼嗎?
Seata 是一款開源的分布式事務解決方案,致力於提供高性能和簡單易用的分布式事務服務。Seata 將為用戶提供了 AT、TCC、SAGA 和 XA 事務模式,為用戶打造一站式的分布式解決方案。
- 對業務無侵入:即減少技術架構上的微服務化所帶來的分布式事務問題對業務的侵入
- 高性能:減少分布式事務解決方案所帶來的性能消耗
官方文檔:https://seata.io/zh-cn/index.html
seata的幾種術語:
- TC(Transaction Coordinator):事務協調者。管理全局的分支事務的狀態,用於全局性事務的提交和回滾。
- TM(Transaction Manager):事務管理者。用於開啟、提交或回滾事務。
- RM(Resource Manager):資源管理器。用於分支事務上的資源管理,向 TC 注冊分支事務,上報分支事務的狀態,接收 TC 的命令來提交或者回滾分支事務。
AT模式
seata目前支持多種事務模式,分別有AT、TCC、SAGA 和 XA ,文章篇幅有限,今天只講常用的AT模式。
AT模式的特點就是對業務無入侵式,整體機制分二階段提交(2PC)
- 一階段:業務數據和回滾日志記錄在同一個本地事務中提交,釋放本地鎖和連接資源。
- 二階段:
- 提交異步化,非常快速地完成
- 回滾通過一階段的回滾日志進行反向補償。
在 AT 模式下,用戶只需關注自己的業務SQL,用戶的業務SQL 作為一階段,Seata 框架會自動生成事務的二階段提交和回滾操作。
一個典型的分布式事務過程:
- TM 向 TC 申請開啟一個全局事務,全局事務創建成功並生成一個全局唯一的 XID;
- XID 在微服務調用鏈路的上下文中傳播;
- RM 向 TC 注冊分支事務,將其納入 XID 對應全局事務的管轄;
- TM 向 TC 發起針對 XID 的全局提交或回滾決議;
- TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求。
搭建Seata TC協調者
seata的協調者其實就是阿里開源的一個服務,我們只需要下載並且啟動它。
下載地址:http://seata.io/zh-cn/blog/download.html
陳某下載的版本是
1.3.0
,各位最好和我版本一致,這樣不會出現莫名的BUG。
下載完成后,直接解壓即可。但是此時還不能直接運行,還需要做一些配置。
創建TC所需要的表
TC運行需要將事務的信息保存在數據庫,因此需要創建一些表,找到seata-1.3.0源碼的script\server\db
這個目錄,將會看到以下SQL文件:
陳某使用的是Mysql數據庫,因此直接運行mysql.sql這個文件中的sql語句,創建的三張表如下圖:
修改TC的注冊中心
找到seata-server-1.3.0\seata\conf
這個目錄,其中有一個registry.conf
文件,其中配置了TC的注冊中心和配置中心。
默認的注冊中心是file
形式,實際使用中肯定不能使用,需要改成Nacos形式,改動的地方如下圖:
需要改動的地方如下:
- type:改成nacos,表示使用nacos作為注冊中心
- application:服務的名稱
- serverAddr:nacos的地址
- group:分組
- namespace:命名空間
- username:用戶名
- password:密碼
最后這份文件都會放在項目源碼的根目錄下,源碼下載方式見文末
修改TC的配置中心
TC的配置中心默認使用的也是file
形式,當然要是用nacos作為配置中心了。
直接修改registry.conf
文件,需要改動的地方如下圖:
需要改動的地方如下:
- type:改成nacos,表示使用nacos作為配置中心
- serverAddr:nacos的地址
- group:分組
- namespace:命名空間
- username:用戶名
- password:密碼
上述配置修改好之后,在TC啟動的時候將會自動讀取nacos的配置。
那么問題來了:TC需要存儲到Nacos中的配置都哪些,如何推送過去?
在seata-1.3.0\script\config-center
中有一個config.txt
文件,其中就是TC所需要的全部配置。
在seata-1.3.0\script\config-center\nacos
中有一個腳本nacos-config.sh
則是將config.txt中的全部配置自動推送到nacos中,運行下面命令(windows可以使用git bash運行):
# -h 主機,你可以使用localhost,-p 端口號 你可以使用8848,-t 命名空間ID,-u 用戶名,-p 密碼
$ sh nacos-config.sh -h 127.0.0.1 -p 8080 -g SEATA_GROUP -t 7a7581ef-433d-46f3-93f9-5fdc18239c65 -u nacos -w nacos
推送成功則可以在Nacos中查詢到所有的配置,如下圖:
修改TC的數據庫連接信息
TC是需要使用數據庫存儲事務信息的,那么如何修改相關配置呢?
上一節的內容已經將所有的配置信息都推送到了Nacos中,TC啟動時會從Nacos中讀取,因此我們修改也需要在Nacos中修改。
需要修改的配置如下:
## 采用db的存儲形式
store.mode=db
## druid數據源
store.db.datasource=druid
## mysql數據庫
store.db.dbType=mysql
## mysql驅動
store.db.driverClassName=com.mysql.jdbc.Driver
## TC的數據庫url
store.db.url=jdbc:mysql://127.0.0.1:3306/seata_server?useUnicode=true
## 用戶名
store.db.user=root
## 密碼
store.db.password=Nov2014
在nacos中搜索上述的配置,直接修改其中的值,比如修改store.mode
,如下圖:
當然Seata還支持Redis作為TC的數據庫,只需要改動以下配置即可:
store.mode=redis
store.redis.host=127.0.0.1
store.redis.port=6379
store.redis.password=123456
啟動TC
按照上述步驟全部配置成功后,則可以啟動TC,在seata-server-1.3.0\seata\bin
目錄下直接點擊seata-server.bat
(windows)運行。
啟動成功后,在Nacos的服務列表中則可以看到TC已經注冊進入,如下圖:
至此,Seata的TC就啟動完成了............
Seata客戶端搭建(RM)
上述已經將Seata的服務端(TC)搭建完成了,下面就以電商系統為例介紹一下如何編碼實現分布式事務。
用戶購買商品的業務邏輯。整個業務邏輯由3個微服務提供支持:
- 倉儲服務:對給定的商品扣除倉儲數量。
- 訂單服務:根據采購需求創建訂單。
- 帳戶服務:從用戶帳戶中扣除余額。
需要了解的知識:Nacos和openFeign,有不清楚的可以看我的前兩章教程,如下:
倉儲服務搭建
陳某整個教程使用的都是同一個聚合項目,關於Spring Cloud版本有不清楚的可以看我第一篇文章的說明。
添加依賴
新建一個seata-storage9020
項目,新增依賴如下:
由於使用的springCloud Alibaba
依賴版本是2.2.1.RELEASE
,其中自帶的seata版本是1.1.0
,但是我們Seata服務端使用的版本是1.3.0,因此需要排除原有的依賴,重新添加1.3.0的依賴。
注意:seata客戶端的依賴版本必須要和服務端一致。
創建數據庫
創建一個數據庫seata-storage
,其中新建兩個表:
storage
:庫存的業務表,SQL如下:
CREATE TABLE `storage` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL,
`num` bigint(11) NULL DEFAULT NULL COMMENT '數量',
`create_time` datetime(0) NULL DEFAULT NULL,
`price` bigint(10) NULL DEFAULT NULL COMMENT '單價,單位分',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `storage` VALUES (1, '碼猿技術專欄', 1000, '2021-10-15 22:32:40', 100);
- undo_log:回滾日志表,這是Seata要求必須有的,每個業務庫都應該創建一個,SQL如下:
CREATE TABLE `undo_log` (
`branch_id` bigint(20) NOT NULL COMMENT 'branch transaction id',
`xid` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'global transaction id',
`context` varchar(128) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL COMMENT 'undo_log context,such as serialization',
`rollback_info` longblob NOT NULL COMMENT 'rollback info',
`log_status` int(11) NOT NULL COMMENT '0:normal status,1:defense status',
`log_created` datetime(6) NOT NULL COMMENT 'create datetime',
`log_modified` datetime(6) NOT NULL COMMENT 'modify datetime',
UNIQUE INDEX `ux_undo_log`(`xid`, `branch_id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci COMMENT = 'AT transaction mode undo table' ROW_FORMAT = Compact;
配置seata相關配置
對於Nacos、Mysql數據源等相關信息就省略了,項目源碼中都有。主要講一下seata如何配置,詳細配置如下:
spring:
application:
## 指定服務名稱,在nacos中的名字
name: seata-storage
## 客戶端seata的相關配置
seata:
## 是否開啟seata,默認true
enabled: true
application-id: ${spring.application.name}
## seata事務組的名稱,一定要和config.tx(nacos)中配置的相同
tx-service-group: ${spring.application.name}-tx-group
## 配置中心的配置
config:
## 使用類型nacos
type: nacos
## nacos作為配置中心的相關配置,需要和server在同一個注冊中心下
nacos:
## 命名空間,需要server端(registry和config)、nacos配置client端(registry和config)保持一致
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
## 地址
server-addr: localhost:8848
## 組, 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
## 用戶名和密碼
username: nacos
password: nacos
registry:
type: nacos
nacos:
## 這里的名字一定要和seata服務端中的名稱相同,默認是seata-server
application: seata-server
## 需要server端(registry和config)、nacos配置client端(registry和config)保持一致
group: SEATA_GROUP
namespace: 7a7581ef-433d-46f3-93f9-5fdc18239c65
username: nacos
password: nacos
server-addr: localhost:8848
以上配置注釋已經很清楚,這里着重強調以下幾點:
- 客戶端seata中的nacos相關配置要和服務端相同,比如地址、命名空間..........
- tx-service-group:這個屬性一定要注意,這個一定要和服務端的配置一致,否則不生效;比如上述配置中的,就要在nacos中新增一個配置
service.vgroupMapping.seata-storage-tx-group=default
,如下圖:
注意:
seata-storage-tx-group
僅僅是后綴,要記得添加配置的時候要加上前綴service.vgroupMapping.
扣減庫存的接口
邏輯很簡單,這里僅僅是做了減庫存的操作,代碼如下:
這里的接口並沒有不同,還是使用@Transactional
開啟了本地事務,並沒有涉及到分布式事務。
到這里倉儲服務搭建好了..............
賬戶服務搭建
搭建完了倉儲服務,賬戶服務搭建很類似了。
添加依賴
新建一個seata-account9021
服務,這里的依賴和倉儲服務的依賴相同,直接復制
創建數據庫
創建一個seata-account
數據庫,其中新建了兩個表:
account
:賬戶業務表,SQL如下:
CREATE TABLE `account` (
`id` bigint(11) NOT NULL,
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用戶userId',
`money` bigint(11) NULL DEFAULT NULL COMMENT '余額,單位分',
`create_time` datetime(0) NULL DEFAULT NULL,
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
INSERT INTO `account` VALUES (1, 'abc123', 1000, '2021-10-19 17:49:53');
- undo_log:回滾日志表,同倉儲服務
配置seata相關配置
Seata相關配置和倉儲服務相同,只不過需要在nacos中添加一個service.vgroupMapping.seata-account-tx-group=default
,如下圖:
扣減余額的接口
具體邏輯自己完善,這里我直接扣減余額,代碼如下:
依然沒有涉及到分布式事務,還是使用@Transactional
開啟了本地事務,是不是很爽............
訂單服務搭建(TM)
這里為了節省篇幅,陳某直接使用訂單服務作為TM,下單、減庫存、扣款整個流程都在訂單服務中實現。
添加依賴
新建一個seata-order9022
服務,這里需要添加的依賴如下:
- Nacos服務發現的依賴
- seata的依賴
- openFeign的依賴,由於要調用賬戶、倉儲的微服務,因此需要額外添加一個openFeign的依賴
創建數據庫
新建一個seata_order
數據庫,其中新建兩個表,如下:
t_order
:訂單的業務表
CREATE TABLE `t_order` (
`id` bigint(11) NOT NULL AUTO_INCREMENT,
`product_id` bigint(11) NULL DEFAULT NULL COMMENT '商品Id',
`num` bigint(11) NULL DEFAULT NULL COMMENT '數量',
`user_id` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '用戶唯一Id',
`create_time` datetime(0) NULL DEFAULT NULL,
`status` int(1) NULL DEFAULT NULL COMMENT '訂單狀態 1 未付款 2 已付款 3 已完成',
PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Compact;
- undo_log:回滾日志表,同倉儲服務
配置和seata相關配置
Seata相關配置和倉儲服務相同,只不過需要在nacos中添加一個service.vgroupMapping.seata-order-tx-group=default
,如下圖:
扣減庫存的接口
這里需要通過openFeign調用倉儲服務的接口進行扣減庫存,接口如下:
以上只是簡單的通過openFeign調用,更細致的配置,比如降級,自己完善.........
扣減余額的接口
這里仍然是通過openFeign調用賬戶服務的接口進行扣減余額,接口如下:
創建訂單的接口
下訂單的接口就是一個事務發起方,作為TM,需要發起一個全局事務,詳細代碼如下圖:
有什么不同?不同之處就是使用了@GlobalTransactional
而不是@Transactional
。
@GlobalTransactional
是Seata提供的,用於開啟才能全局事務,只在TM中標注即可生效。
測試
分別啟動seata-account9021
、seata-storage9020
、seata-order9022
,如下圖:
下面調用下單接口,如下圖:
從控制台輸出的日志可以看出,流程未出現任何異常,事務已經提交,如下圖:
果然,查看訂單、余額、庫存表,數據也都是正確的。
但是,這僅僅是流程沒問題,並不能說明分布式事務已經配置成功了,因此需要手動造個異常。
在扣減余額的接口睡眠2秒鍾,因為openFeign的超時時間默認是1秒,這樣肯定是超時異常了,如下圖:
此時,調用創建訂單的接口,控制台日志輸出如下圖:
發現在扣減余額處理中超時了,導致了異常.......
此時,看下庫存的數據有沒有扣減,很高興,庫存沒有扣減成功,說明事務已經回滾了,分布式事務成功了。
總結
Seata客戶端創建很簡單,需要注意以下幾點內容:
- seata客戶端的版本需要和服務端保持一致
- 每個服務的數據庫都要創建一個
undo_log
回滾日志表 - 客戶端指定的事務分組名稱要和Nacos相同,比如
service.vgroupMapping.seata-account-tx-group=default
- 前綴:
service.vgroupMapping.
- 后綴:
{自定義}
- 前綴:
項目源碼已經上傳,關注公號
碼猿技術專欄
回復關鍵詞9528
獲取!
AT模式原理分析
AT模式最大的優點就是對業務代碼無侵入,一切都像在寫單體業務邏輯一樣。
TC相關的三張表:
global_table
:全局事務表,每當有一個全局事務發起后,就會在該表中記錄全局事務的IDbranch_table
:分支事務表,記錄每一個分支事務的ID,分支事務操作的哪個數據庫等信息lock_table
:全局鎖
一階段步驟
TM:seata-order.create()
方法執行時,由於該方法具有@GlobalTranscational
標志,該TM會向TC發起全局事務,生成XID(全局鎖)RM:StorageService.deduct()
:寫表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結果RM:AccountService.deduct()
:寫表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結果RM:OrderService.create()
:寫表,UNDO_LOG記錄回滾日志(Branch ID),通知TC操作結果
RM寫表的過程,Seata 會攔截業務SQL,首先解析 SQL 語義,在業務數據被更新前,將其保存成before image(前置鏡像),然后執行業務SQL,在業務數據更新之后,再將其保存成after image(后置鏡像),最后生成行鎖。以上操作全部在一個數據庫事務內完成,這樣保證了一階段操作的原子性。
二階段步驟
因為“業務 SQL”在一階段已經提交至數據庫, 所以 Seata 框架只需將一階段保存的快照數據和行鎖刪掉,完成數據清理即可。
正常:TM執行成功,通知TC全局提交,TC此時通知所有的RM提交成功,刪除UNDO_LOG回滾日志
異常:TM執行失敗,通知TC全局回滾,TC此時通知所有的RM進行回滾,根據UNDO_LOG反向操作,使用before image還原業務數據,刪除UNDO_LOG,但在還原前要首先要校驗臟寫,對比“數據庫當前業務數據”和 “after image”,如果兩份數據完全一致就說明沒有臟寫,可以還原業務數據,如果不一致就說明有臟寫,出現臟寫就需要轉人工處理。
AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動生成,用戶只需編寫業務 SQL,便能輕松接入分布式事務,AT 模式是一種對業務無任何侵入的分布式事務解決方案。
總結
本文介紹了七種分布式事務解決方案,以及阿里開源的Seata,從入門到實現,文中如有錯誤之處,歡迎留言指正。
本文只介紹了Seata的AT模式,其實Seata還支持TCC、Saga事務模式,關於這一部分內容和Seata源碼分析會在下期文章中介紹。
作者碼字不易,在看、收藏、轉發一波,謝謝支持!
案例源碼已經上傳,關注公號【碼猿技術專欄】,回復關鍵詞【9528】獲取!