分布式事務總結

分布式事務的產生的原因

事務的ACID特性

原子性(A)
所謂的原子性就是說,在整個事務中的所有操作,要么全部完成,要么全部不做,沒有中間狀態。對于事務在執行中發生錯誤,所有的操作都會被回滾,整個事務就像從沒被執行過一樣。
一致性(C)
事務的執行必須保證系統的一致性,不會出現中間結果。我個人理解是C和AID特性對事務的描述不是一個層面的,AID是基礎特性。只要保證了AID,則C一定會得到滿足。
隔離性(I)
所謂的隔離性就是說,事務與事務之間不會互相影響,一個事務的中間狀態不會被其他事務感知。
持久性(D)
所謂的持久性,就是說一單事務完成了,那么事務對數據所做的變更就完全保存在了數據庫中,即使發生停電,系統宕機也是如此。

我們都知道典型的關系型數據庫都是支持單機事務的。單機數據庫事務是針對單個數據庫的。在分布式場景下,就產生了分布式事務。分布式事務就是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器分別位于不同的分布式系統的不同節點之上。就是一次大的操作由不同的小操作組成,這些小的操作分布在不同的服務器上,且屬于不同的應用,分布式事務需要保證這些小操作要么全部成功,要么全部失敗(事務的原子性)。本質上來說,分布式事務就是為了保證不同數據庫的數據一致性。

分布式事務產生的場景總結

數據庫水平拆分
隨著業務數據規模的快速發展,數據量越來越大,單庫單表逐漸成為瓶頸。所以我們對數據庫進行了水平拆分,將原單庫單表拆分成數據庫分片。這時候,如果一個操作既訪問01庫,又訪問02庫,而且要保證數據的一致性,那么就要用到分布式事務。
業務服務化拆分
隨著業務的快速發展,系統的訪問量和業務復雜程度都在快速增長,單系統架構逐漸成為業務發展瓶頸,解決業務系統的高耦合、可伸縮問題的需求越來越強烈。目前SOA、微服務架構已經席卷整個互聯網行業。將單業務系統拆分成多個業務系統,降低了各系統之間的耦合度,使不同的業務系統專注于自身業務,更有利于業務的發展和系統容量的伸縮。業務系統按照服務拆分之后,一個完整的業務往往需要調用多個服務,多個服務間的數據一致性就是典型的分布式事務場景。
總結
數據表水平拆分后,存在兩種跨庫操作場景。1)操作同一張邏輯表A的sql實際需要分發到多張物理表(比如A_1,A_2)進行執行。2)同一個業務操作的多張表被分拆到多個庫上。針對情況1,應該從業務層面杜絕(需要思考分片健選擇是否合理等),如果杜絕不了,也需要在數據庫序列化層面拆分為多個操作,也就是拆分為多條sql,然后再在業務層面去保證分布式一致性,而不是讓底層數據庫支持這種分表的一致性。
業務服務化拆分對應的就是庫的拆分,和上面數據表水平拆分的情況2一樣。都是庫表的重新組裝,這種是目前互聯網行業普遍存在的分布式事務場景,下面就來說說常見的解決方案。

常見的分布式事務解決方案

分布式解決方案分強一致性解決方案和最終一致性解決方案。強一致性解決方案在一次請求中保證要么成功要么失敗回滾到原始狀態,是現實上可以分基于二階段提交和基于回滾接口補償實現;最終一致性解決方案非實時保證數據的一致性,只保證多個系統最終的狀態是一致的,可以分帶后向恢復和前向恢復(靠不斷重試保證一定成功)的解決方案。
下面以支付場景下單扣款和扣積分為例講解一下各種解決方案如何實現。

支付場景

如何保證扣款和加積分保持一致?

強一致性解決方案

強一致性解決方案--XA

XA是一個分布式事務協議,由Tuxedo提出。XA中大致分為兩部分:事務管理器和本地資源管理器。其中本地資源管理器往往由數據庫實現,比如Oracle、DB2這些商業數據庫都實現了XA接口,而事務管理器作為全局的調度者,負責各個本地資源的提交和回滾。XA實現分布式事務的原理如下:


XA事務步驟原理

兩階段提交執行步驟

準備操作(prepare)與ACID
? A: 準備后,仍可提交與回滾
? I: 準備后,事務結果仍然只在事務內可見
? D: 準備后,事務結果已經持久
局限
? 協議成本
? 準備階段的持久成本
? 全局事務狀態的持久成本
? 潛在故障點多帶來的脆弱性
? 準備后,提交前的故障引發 一系列隔離與恢復難題

對比一下數據庫層面實現差異

單機事務sql
mysql>START transaction 'xatest';
mysql>INSERT INTO mytable (i) VALUES(10);
mysql> ROLLBACK 'xatest';
mysql> COMMIT 'xatest';

XA事務sql
mysql> XA START 'xatest';
mysql> INSERT INTO mytable (i) VALUES(10);
mysql> XA END 'xatest';
mysql> XA PREPARE 'xatest';
mysql> XA ROLLBACK 'xatest';
mysql> XA COMMIT 'xatest';

總的來說,XA協議比較簡單,而且一旦商業數據庫實現了XA協議,使用分布式事務的成本也比較低。但是,XA也有致命的缺點,那就是性能不理想,之間簡單測試了兩方事務(就是上面這種兩個DB操作的一致性)的性能是單庫性能的1/5左右,多方事務的性能會隨著參與方的數量成指數下降。XA目前在商業數據庫支持的比較理想,在mysql數據庫中支持的不太理想,mysql的XA實現,沒有記錄prepare階段日志,主備切換回導致主庫與備庫數據不一致。許多nosql也沒有支持XA,這讓XA的應用場景變得非常狹隘。

強一致性解決方案--基于接口補償

基于接口補償

步驟
1)把扣款和加積分的服務調用放在一個本地方法中。
2)當用戶請求登錄接口時,先執行加積分操作,加分成功后再執行扣款操作
3)如果扣款成功,那當然最好了,積分也加成功了。如果扣款失敗,則調用加積分對應的回滾接口(執行減積分的操作)。
局限
侵入性高,不適用于復雜場景
部分操作不能提供回滾接口

強一致性解決方案 -- TCC編程模式

所謂的TCC編程模式,也是兩階段提交的一個變種。TCC提供了一個編程框架,將整個業務邏輯分為三塊:Try、Confirm和Cancel三個操作。TCC就是通過代碼人為實現了兩階段提交,不同的業務場景所寫的代碼都不一樣,復雜度也不一樣,因此,這種模式并不能很好地被復用。

tcc

Try: 嘗試執行業務
? 完成所有業務檢查(一致性)
? 預留必須業務資源(準隔離性)
Confirm:確認執行業務
? 真正執行業務
? 不作任何業務檢查
? 只使用Try階段預留的業務資源
? Confirm操作滿足冪等性
Cancel: 取消執行業務
? 釋放Try階段預留的業務資源
? Cancel操作滿足冪等性
與2PC協議比較
? 位于業務服務層而非資源層
? 沒有單獨的準備(Prepare)階段,Try操作兼備資源操作與準備能力
? Try操作可以靈活選擇業務資源的鎖定粒度
? 較高開發成本
TCC具體步驟
TCC步驟

TCC代碼實現
偽代碼實現

實現
? 一個完整的業務活動由一個主業務服務與若干從業務服務組成
? 主業務服務負責發起并完成整個業務活動
? 從業務服務提供TCC型業務操作
? 業務活動管理器控制業務活動的一致性,它登記業務活動中的操作,并在業務活動提交時確認所有的TCC型操作的confirm操作,在業務活動取消時調用所有TCC型操作的 cancel操作
成本
? 實現TCC操作的成本
? 業務活動結束時confirm或cancel操作的執行成本
? 業務活動日志成本
適用范圍
? 強隔離性、嚴格一致性要求的業務活動
? 適用于執行時間較短的業務

最終一致性解決方案

最終一致性描述的是分布式系統中,當系統在數據一致的狀態執行更新之后,也應該保持一致的狀態。具體實現中可以表現為過程中異步軟一致性,但結果要強一致性。消息一致性方案是通過消息中間件保證上、下游應用數據操作的一致性。基本思路:是將本地操作和發送消息放在一個事務中,保證本地操作和消息發送要么兩者都成功或者都失敗。下游應用向消息系統訂閱該消息,收到消息后執行相應操作。本質上是:將分布式事務轉換成兩個本地事務,然后依靠下游業務的重試機制達到最終一致性。
Saga是一個長活事務可被分解成可以交錯運行的子事務集合。其中每個子事務都是一個保持數據庫一致性的真實事務。

Saga介紹

每個Saga由一系列sub-transaction Ti 組成
每個Ti 都有對應的補償動作Ci,補償動作用于撤銷Ti造成的結果
可以看到,和TCC相比,Saga沒有“預留”動作,它的Ti就是直接提交到庫。
Saga的執行順序有兩種:

(1) T1, T2, T3, ..., Tn
(2) T1, T2, ..., Tj, Cj,..., C2, C1,其中0 < j < n

Saga定義了兩種恢復策略:
1)backward recovery,向后恢復,補償所有已完成的事務,如果任一子事務失敗。即上面提到的第二種執行順序,其中j是發生錯誤的sub-transaction,這種做法的效果是撤銷掉之前所有成功的sub-transation,使得整個Saga的執行結果撤銷。
2)forward recovery,向前恢復,重試失敗的事務,假設每個子事務最終都會成功。適用于必須要成功的場景,執行順序是類似于這樣的:T1, T2, ..., Tj(失敗), Tj(重試),..., Tn,其中j是發生錯誤的sub-transaction。該情況下不需要Ci。
顯然,向前恢復沒有必要提供補償事務,如果你的業務中,子事務(最終)總會成功,或補償事務難以定義或不可能,向前恢復更符合你的需求。

Saga看起來很有希望滿足我們的需求。所有長活事務都可以這樣做嗎?這里有一些限制:

  • Saga只允許兩個層次的嵌套,頂級的Saga和簡單子事務
  • 在外層,全原子性不能得到滿足。也就是說,sagas可能會看到其他sagas的部分結果
  • 每個子事務應該是獨立的原子行為

補償也有需考慮的事項:補償事務從語義角度撤消了事務Ti的行為,但未必能將數據庫返回到執行Ti時的狀態。(例如,如果事務觸發導彈發射, 則可能無法撤消此操作)

協調式Saga VS編排式Saga

當系統命令啟動saga時,協調邏輯必須選擇并告知第一個saga參與者執行本地事務。一旦該事務完成,saga的排序協調選擇并調用下一個saga參與者。這個過程一直持續到saga執行了所有步驟。如果任何本地事務失敗,則saga必須以相反的順序執行補償事務。構建一個saga的協調邏輯有幾種不同的方法:

1)協同式(Choreography):把Saga的決策和執行順序邏輯分布在Saga的每一個參與方中,它們通過交換事件的方式來進行溝通。
2)編排式(Orchestration):把Saga的決策和執行順序邏輯集中在一個Saga編排器類中。Saga編排器發出命令式消息給各個參與方,指示這些參與方服務完成具體的本地事務操作。
更多關于兩種saga的描述可以參考附錄中saga的引用文章。

saga落地--個人看法

不管是協調式saga還是編排式saga,都只允許兩個層次的嵌套。saga倡導通過事件命令來實現不同參與方的通信。但是現實場景中,消息是屬于異步通信,存在延遲的可能性,與異步消息對應的是實時RPC,很多對延遲敏感的業務場景,與參與方之間的通信都是采用RPC。同時,我們需要區分參與方的重要程度,有些參與方是跟場景密切相關,時效性要求很高,可以說是生死與共;而有些參與方跟場景的重要性不高,對時效沒有要求。比如電商下單場景下,扣庫存、支付、扣積分等行為是生死與共的,下單用戶提醒IM消息、物流等相對就沒那么重要,失敗后異步重試都能接受。
我們可以借鑒編排式saga的思想,在分布式事務場景里面通過延遲回滾消息來實現參與方共進退的統一協調(而不是通過一個顯示的流程定義來實現)。通過將實時性要求高&&重要的請求的改為RPC交互,其他用異步消息模式。下面以電商下單場景為例,看如何變種saga滿足絕大部分場景下的分布式事務需求:

變種saga流程

1)交易系統創建訂單(往DB插入一條記錄),同時發送訂單創建消息。通過RocketMq事務性消息保證一致性。
2)接著執行完成訂單所需的同步核心RPC服務(非核心的系統通過監聽MQ消息自行處理,處理結果不會影響交易狀態)。執行成功更改訂單狀態,同時發送MQ消息。
3)交易系統接受自己發送的訂單創建消息,通過定時調度系統創建延時回滾任務(或者使用RocketMq的重試功能,設置第二次發送時間為定時任務的延遲創建時間。在非消息堵塞的情況下,消息第一次到達延遲為1ms左右,這時可能RPC還未執行完,訂單狀態還未設置為完成,第二次消費時間可以指定)。延遲任務先通過查詢訂單狀態判斷訂單是否完成,完成則不創建回滾任務,否則創建。 PS:多個RPC可以創建一個回滾任務,通過一個消費組接受一次消息就可以;也可以通過創建多個消費組,一個消息消費多次,每次消費創建一個RPC的回滾任務。 回滾任務失敗,通過MQ的重發來重試。

以上是交易系統和其他系統之間保持最終一致性的解決方案。

本地事件表 VS 事務消息

本地時間表和事務消息本質上都是保證MQ發送和本地DB操作原子性(要么一起成功、要么一起失敗)。目前MQ中支持事務消息的只有Rocketmq。
Rocketmq事務消息

流程圖

上圖是RocketMQ提供的保證MQ消息、DB事務一致性的方案。
MQ消息、DB操作一致性方案:
1)發送消息到MQ服務器,此時消息狀態為SEND_OK。此消息為consumer不可見。
2)執行DB操作;DB執行成功Commit DB操作,DB執行失敗Rollback DB操作。
3)如果DB執行成功,回復MQ服務器,將狀態為COMMIT_MESSAGE;如果DB執行失敗,回復MQ服務器,將狀態改為ROLLBACK_MESSAGE。注意此過程有可能失敗。
4)MQ內部提供一個名為“事務狀態服務”的服務,此服務會檢查事務消息的狀態,如果發現消息未COMMIT,則通過Producer啟動時注冊的TransactionCheckListener來回調業務系統,業務系統在checkLocalTransactionState方法中檢查DB事務狀態,如果成功,則回復COMMIT_MESSAGE,否則回復ROLLBACK_MESSAGE。
說明:
上面以DB為例,其實此處可以是任何業務或者數據源。
以上SEND_OK、COMMIT_MESSAGE、ROLLBACK_MESSAGE均是client jar提供的狀態,在MQ服務器內部是一個數字。
TransactionCheckListener 是在消息的commit或者rollback消息丟失的情況下才會回調(上圖中灰色部分)。這種消息丟失只存在于斷網或者rocketmq集群掛了的情況下。

本地事件表如何與DDD相得益彰
通常的業務處理過程都會更新數據庫然后發布領域事件,這里一個比較重要的點是:我們需要保證數據庫更新和事件發布之間的原子性,也即要么二者都成功,要么都失敗。如果我們的mq中間件不支持事務消息,一種較佳的實現方式是通過事件表來保證消息和本地事務的一致性。流程大致如下:

事件表步驟
  1. 在更新業務表的同時,將領域事件一并保存到數據庫的事件表中,此時業務表和事件表在同一個本地事務中,即保證了原子性,又保證了效率。
  2. 在后臺開啟一個任務(或者基于監聽事件表的binlog),將事件表中的事件發布到消息隊列中,發送成功之后刪除掉事件。
    發布領域事件的整個流程如下:
  3. 接受用戶請求;
  4. 處理用戶請求;
  5. 寫入業務表;
  6. 寫入事件表,事件表和業務表的更新在同一個本地數據庫事務中;
  7. 事務完成后,即時觸發事件的發送(可以定時掃描事件表,還可以借助諸如MySQL的binlog之類的機制);
  8. 后臺任務讀取事件表;
  9. 后臺任務發送事件到消息隊列;
  10. 發送成功后刪除事件。
    在事件表場景下,一種常見的做法是將領域事件保存到聚合根中,然后在Repository保存聚合根的時候,將事件保存到事件表中。

總結

分布式解決方案 簡介 優缺點
XA兩階段提交 資源管理器(數據庫等存儲管理結點)實現Xa規范 協議成本帶來交互成本高、持久成本高。雖能保障強一致性,單并發能力弱
提供回滾接口 業務方依次執行,出現異常調用回滾接口 業務侵入性高。一致性實時性高
TCC模式 原來一個接口需要改成三個接口,TCC框架通過攔截器捕獲Try操作異常,決定調用Confirm對應接口還是Cancal對應接口 開發成本高,一致性實時性強
saga 通過消息將長活事務分解成可以交錯運行的子事務集合,其中每個子事務都是一個保持數據庫一致性的真實事務。 只能滿足最終一致性,需要事務消息中間件或者使用事件表;優點是業務方開發成本低

分布式事務,本質上是對多個數據庫的事務進行統一控制,按照控制力度可以分為:不控制、部分控制和完全控制。不控制就是不引入分布式事務,部分控制就是各種變種的兩階段提交。部分控制的好處是并發量和性能很好,缺點是數據一致性減弱了,完全控制則是犧牲了性能,保障了一致性,具體用哪種方式,最終還是取決于業務場景。作為技術人員,一定不能忘了技術是為業務服務的,不要為了技術而技術,針對不同業務進行技術選型也是一種很重要的能力。

參考

螞蟻金服大規模分布式事務實踐及四種分布式事務模式
分布式事務:Saga模式

?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,401評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,011評論 3 413
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,263評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,543評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,323評論 6 404
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,874評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 42,968評論 3 439
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,095評論 0 286
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,605評論 1 331
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,551評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,720評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,242評論 5 355
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 43,961評論 3 345
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,358評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,612評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,330評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,690評論 2 370