7種分佈式事務的解決方案,一次講給你聽

本文約5300字,閱讀時長「5分鐘」

什麼是分佈式事務

分佈式事務是指事務的參與者、支持事務的服務器、資源服務器以及事務管理器「分別位於不同的分佈式系統的不同節點之上」

一個大的操作由N多的小的操作共同完成。而這些小的操作又分佈在不同的服務上。針對於這些操作,「要麼全部成功執行,要麼全部不執行」

爲什麼會有分佈式事務?

舉個例子:

轉賬是最經典的分佈式事務場景,假設用戶 A 使用銀行 app 發起一筆跨行轉賬給用戶 B,銀行系統首先扣掉用戶 A 的錢,然後增加用戶 B 賬戶中的餘額。

如果其中某個步驟失敗,此時就有可能會出現 2 種「異常」情況:

  • 1.用戶 A 的賬戶扣款成功,用戶 B 賬戶餘額增加失敗
  • 2.用戶 A 賬戶扣款失敗,用戶 B 賬戶餘額增加成功。

對於銀行系統來說,以上 2 種情況都是「不允許發生」,此時就需要事務來保證轉賬操作的成功。

「單體應用」中,我們只需要貼上@Transactional註解就可以開啓事務來保證整個操作的「原子性」

但是看似以上簡單的操作,在實際的應用架構中,不可能是單體的服務,我們會把這一系列操作交給「N個服務」去完成,也就是拆分成爲「分佈式微服務架構」

比如下訂單服務,扣庫存服務等等,必須要「保證不同服務狀態結果的一致性」,於是就出現了分佈式事務。

分佈式理論

CAP定理

在一個分佈式系統中,以下三點特性無法同時滿足,「魚與熊掌不可兼得」

一致性(C):
在分佈式系統中的所有數據備份,「在同一時刻是否擁有同樣的值」。(等同於所有節點訪問同一份最新的數據副本)

可用性(A):
在集羣中一部分節點「故障」後,集羣整體「是否還能響應」客戶端的讀寫請求。(對數據更新具備高可用性)

分區容錯性(P):
即使出現「單個組件無法可用,操作依然可以完成」

具體地講在分佈式系統中,在任何數據庫設計中,一個Web應用「至多隻能同時支持上面的兩個屬性」。顯然,任何橫向擴展策略都要依賴於數據分區。因此,設計人員必須在一致性與可用性之間做出選擇。

BASE理論

在分佈式系統中,我們往往追求的是可用性,它的重要程序比一致性要高,那麼如何實現高可用性呢?

前人已經給我們提出來了另外一個理論,就是BASE理論,它是用來對CAP定理進行進一步擴充的。BASE理論指的是:

  • 「Basically Available(基本可用)」
  • 「Soft state(軟狀態)」
  • 「Eventually consistent(最終一致性)」

BASE理論是對CAP中的一致性和可用性進行一個權衡的結果,理論的核心思想就是:我們無法做到強一致,但每個應用都可以根據自身的業務特點,採用適當的方式來使系統達到最終一致性(Eventual consistency)。

分佈式事務解決方案

兩階段提交(2PC)

熟悉mysql的同學對兩階段提交應該頗爲熟悉,mysql的事務就是通過「日誌系統」來完成兩階段提交的。

兩階段協議可以用於單機集中式系統,由事務管理器協調多個資源管理器;也可以用於分佈式系統,「由一個全局的事務管理器協調各個子系統的局部事務管理器完成兩階段提交」

這個協議有「兩個角色」

A節點是事務的協調者,B和C是事務的參與者。

事務的提交分成兩個階段

第一個階段是「投票階段」

  • 1.協調者首先將命令 「寫入日誌」
  • 2. 「發一個prepare命令」給B和C節點這兩個參與者
  • 3.B和C收到消息後,根據自己的實際情況, 「判斷自己的實際情況是否可以提交」
  • 4.將處理結果 「記錄到日誌」系統
  • 5.將結果 「返回」給協調者

第二個階段是「決定階段」

當A節點收到B和C參與者所有的確認消息後

  • 「判斷」所有協調者 「是否都可以提交」
    • 如果可以則 「寫入日誌」並且發起commit命令
    • 有一個不可以則 「寫入日誌」並且發起abort命令
  • 參與者收到協調者發起的命令, 「執行命令」
  • 將執行命令及結果 「寫入日誌」
  • 「返回結果」給協調者

可能會存在哪些問題?

  • 「單點故障」:一旦事務管理器出現故障,整個系統不可用

  • 「數據不一致」:在階段二,如果事務管理器只發送了部分 commit 消息,此時網絡發生異常,那麼只有部分參與者接收到 commit 消息,也就是說只有部分參與者提交了事務,使得系統數據不一致。

  • 「響應時間較長」:整個消息鏈路是串行的,要等待響應結果,不適合高併發的場景

  • 「不確定性」:當事務管理器發送 commit 之後,並且此時只有一個參與者收到了 commit,那麼當該參與者與事務管理器同時宕機之後,重新選舉的事務管理器無法確定該條消息是否提交成功。

三階段提交(3PC)

三階段提交又稱3PC,相對於2PC來說增加了CanCommit階段和超時機制。如果段時間內沒有收到協調者的commit請求,那麼就會自動進行commit,解決了2PC單點故障的問題。

但是性能問題和不一致問題仍然沒有根本解決。下面我們還是一起看下三階段流程的是什麼樣的?

  • 第一階段:「CanCommit階段」這個階段所做的事很簡單,就是協調者詢問事務參與者,你是否有能力完成此次事務。

    • 如果都返回yes,則進入第二階段
    • 有一個返回no或等待響應超時,則中斷事務,並向所有參與者發送abort請求
  • 第二階段:「PreCommit階段」此時協調者會向所有的參與者發送PreCommit請求,參與者收到後開始執行事務操作,並將Undo和Redo信息記錄到事務日誌中。參與者執行完事務操作後(此時屬於未提交事務的狀態),就會向協調者反饋“Ack”表示我已經準備好提交了,並等待協調者的下一步指令。

  • 第三階段:「DoCommit階段」在階段二中如果所有的參與者節點都可以進行PreCommit提交,那麼協調者就會從“預提交狀態”轉變爲“提交狀態”。然後向所有的參與者節點發送"doCommit"請求,參與者節點在收到提交請求後就會各自執行事務提交操作,並向協調者節點反饋“Ack”消息,協調者收到所有參與者的Ack消息後完成事務。相反,如果有一個參與者節點未完成PreCommit的反饋或者反饋超時,那麼協調者都會向所有的參與者節點發送abort請求,從而中斷事務。

補償事務(TCC)

TCC其實就是採用的補償機制,其核心思想是:「針對每個操作,都要註冊一個與其對應的確認和補償(撤銷)操作」。它分爲三個階段:

「Try,Confirm,Cancel」

  • Try階段主要是對 「業務系統做檢測及資源預留」,其主要分爲兩個階段
    • Confirm 階段主要是對 「業務系統做確認提交」,Try階段執行成功並開始執行 Confirm階段時,默認 Confirm階段是不會出錯的。即:只要Try成功,Confirm一定成功。
    • Cancel 階段主要是在業務執行錯誤,需要回滾的狀態下執行的業務取消, 「預留資源釋放」

比如下一個訂單減一個庫存:

執行流程:

  • Try階段:訂單系統將當前訂單狀態設置爲支付中,庫存系統校驗當前剩餘庫存數量是否大於1,然後將可用庫存數量設置爲庫存剩餘數量-1,
    • 如果Try階段 「執行成功」,執行Confirm階段,將訂單狀態修改爲支付成功,庫存剩餘數量修改爲可用庫存數量
    • 如果Try階段 「執行失敗」,執行Cancel階段,將訂單狀態修改爲支付失敗,可用庫存數量修改爲庫存剩餘數量

TCC 事務機制相比於上面介紹的2PC,解決了其幾個缺點:

  • 1. 「解決了協調者單點」,由主業務方發起並完成這個業務活動。業務活動管理器也變成多點,引入集羣。
  • 2. 「同步阻塞」:引入超時,超時後進行補償,並且不會鎖定整個資源,將資源轉換爲業務邏輯形式,粒度變小。
  • 3. 「數據一致性」,有了補償機制之後,由業務活動管理器控制一致性

總之,TCC 就是通過代碼人爲實現了兩階段提交,不同的業務場景所寫的代碼都不一樣,並且很大程度的「增加」了業務代碼的「複雜度」,因此,這種模式並不能很好地被複用。

本地消息表


執行流程:


  • 消息生產方,需要額外建一個消息表,並「記錄消息發送狀態」。消息表和業務數據要在一個事務裏提交,也就是說他們要在一個數據庫裏面。然後消息會經過MQ發送到消息的消費方。

    • 如果消息發送失敗,會進行重試發送。
  • 消息消費方,需要「處理」這個「消息」,並完成自己的業務邏輯。

    • 如果是 「業務上面的失敗」,可以給生產方 「發送一個業務補償消息」,通知生產方進行回滾等操作。
    • 此時如果本地事務處理成功,表明已經處理成功了
    • 如果處理失敗,那麼就會重試執行。
  • 生產方和消費方定時掃描本地消息表,把還沒處理完成的消息或者失敗的消息再發送一遍。

消息事務

消息事務的原理是將兩個事務「通過消息中間件進行異步解耦」,和上述的本地消息表有點類似,但是是通過消息中間件的機制去做的,其本質就是'將本地消息表封裝到了消息中間件中'。

執行流程:

  • 發送prepare消息到消息中間件
  • 發送成功後,執行本地事務
    • 如果事務執行成功,則commit,消息中間件將消息下發至消費端
    • 如果事務執行失敗,則回滾,消息中間件將這條prepare消息刪除
  • 消費端接收到消息進行消費,如果消費失敗,則不斷重試

這種方案也是實現了「最終一致性」,對比本地消息表實現方案,不需要再建消息表,「不再依賴本地數據庫事務」了,所以這種方案更適用於高併發的場景。目前市面上實現該方案的「只有阿里的 RocketMQ」

最大努力通知

最大努力通知的方案實現比較簡單,適用於一些最終一致性要求較低的業務。

執行流程:

  • 系統 A 本地事務執行完之後,發送個消息到 MQ;
  • 這裏會有個專門消費 MQ 的服務,這個服務會消費 MQ 並調用系統 B 的接口;
  • 要是系統 B 執行成功就 ok 了;要是系統 B 執行失敗了,那麼最大努力通知服務就定時嘗試重新調用系統 B, 反覆 N 次,最後還是不行就放棄。

Sagas 事務模型

Saga事務模型又叫做長時間運行的事務

其核心思想是「將長事務拆分爲多個本地短事務」,由Saga事務協調器協調,如果正常結束那就正常完成,如果「某個步驟失敗,則根據相反順序一次調用補償操作」

Seata框架中一個分佈式事務包含3種角色:

「Transaction Coordinator (TC)」:事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾。「Transaction Manager (TM)」:控制全局事務的邊界,負責開啓一個全局事務,並最終發起全局提交或全局回滾的決議。「Resource Manager (RM)」:控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾。

seata框架「爲每一個RM維護了一張UNDO_LOG表」,其中保存了每一次本地事務的回滾數據。

具體流程:1.首先TM 向 TC 申請「開啓一個全局事務」,全局事務「創建」成功並生成一個「全局唯一的 XID」

2.XID 在微服務調用鏈路的上下文中傳播。

3.RM 開始執行這個分支事務,RM首先解析這條SQL語句,「生成對應的UNDO_LOG記錄」。下面是一條UNDO_LOG中的記錄,UNDO_LOG表中記錄了分支ID,全局事務ID,以及事務執行的redo和undo數據以供二階段恢復。

4.RM在同一個本地事務中「執行業務SQL和UNDO_LOG數據的插入」。在提交這個本地事務前,RM會向TC「申請關於這條記錄的全局鎖」

如果申請不到,則說明有其他事務也在對這條記錄進行操作,因此它會在一段時間內重試,重試失敗則回滾本地事務,並向TC彙報本地事務執行失敗。

6.RM在事務提交前,「申請到了相關記錄的全局鎖」,然後直接提交本地事務,並向TC「彙報本地事務執行成功」。此時全局鎖並沒有釋放,全局鎖的釋放取決於二階段是提交命令還是回滾命令。

7.TC根據所有的分支事務執行結果,向RM「下發提交或回滾」命令。

  • RM如果「收到TC的提交命令」,首先「立即釋放」相關記錄的全局「鎖」,然後把提交請求放入一個異步任務的隊列中,馬上返回提交成功的結果給 TC。異步隊列中的提交請求真正執行時,只是刪除相應 UNDO LOG 記錄而已。

  • RM如果「收到TC的回滾命令」,則會開啓一個本地事務,通過 XID 和 Branch ID 查找到相應的 UNDO LOG 記錄。將 UNDO LOG 中的後鏡與當前數據進行比較,

    • 如果不同,說明數據被當前全局事務之外的動作做了修改。這種情況,需要根據配置策略來做處理。
    • 如果相同,根據 UNDO LOG 中的前鏡像和業務 SQL 的相關信息生成並執行回滾的語句並執行,然後提交本地事務達到回滾的目的,最後釋放相關記錄的全局鎖。

總結

本文介紹了分佈式事務的一些基礎理論,並對常用的分佈式事務方案進行了講解。

分佈式事務本身就是一個技術難題,業務中具體使用哪種方案還是需要不同的業務特點自行選擇,但是我們也會發現,分佈式事務會大大的提高流程的複雜度,會帶來很多額外的開銷工作,「代碼量上去了,業務複雜了,性能下跌了」

所以,當我們真實開發的過程中,能不使用分佈式事務就不使用。


    
    
    

往期推薦

一文掌握Redisson分佈式鎖原理|乾貨推薦


阿里巴巴Druid,輕鬆實現MySQL數據庫加密!


Google Guava,牛逼的腳手架


本文分享自微信公衆號 - Java中文社羣(javacn666)。
如有侵權,請聯繫 [email protected] 刪除。
本文參與“OSC源創計劃”,歡迎正在閱讀的你也加入,一起分享。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章