總結分散式事務的7種解決方案(理論+方案)

2021-09-22 16:00:26

隨著業務的快速發展、業務複雜度越來越高,幾乎每個公司的系統都會從單體走向分散式,特別是轉向微服務架構。隨之而來就必然遇到分散式事務這個難題。

這篇文章首先介紹了相關的基礎理論,然後總結了最經典的事務方案,最後給出了子事務亂序執行(冪等、空補償、懸掛問題)的解決方案,分享給大家。

基礎理論

在講解具體方案之前,我們先了解一下分散式事務所涉及到的基礎理論知識。

我們拿轉賬作為例子,A需要轉100元給B,那麼需要給A的餘額-100元,給B的餘額+100元,整個轉賬要保證,A-100和B+100同時成功,或者同時失敗。看看在各種場景下,是如何解決這個問題的。

事務

把多條語句作為一個整體進行操作的功能,被稱為資料庫事務。資料庫事務可以確保該事務範圍內的所有操作都可以全部成功或者全部失敗。

事務具有 4 個屬性:原子性、一致性、隔離性、永續性。這四個屬性通常稱為 ACID 特性。

  • Atomicity(原子性):一個事務中的所有操作,要麼全部完成,要麼全部不完成,不會結束在中間某個環節。事務在執行過程中發生錯誤,會被恢復到事務開始前的狀態,就像這個事務從來沒有執行過一樣。
  • Consistency(一致性):在事務開始之前和事務結束以後,資料庫的完整性沒有被破壞。完整性包括外來鍵約束、應用定義的等約束不會被破壞。
  • Isolation(隔離性):資料庫允許多個並行事務同時對其資料進行讀寫和修改的能力,隔離性可以防止多個事務並行執行時由於交叉執行而導致資料的不一致。
  • Durability(永續性):事務處理結束後,對資料的修改就是永久的,即便系統故障也不會丟失。

假如我們的業務系統不復雜,可以在一個資料庫、一個服務內對資料進行修改,完成轉賬,那麼,我們可以利用資料庫事務,保證轉賬業務的正確完成。

分散式事務

銀行跨行轉賬業務是一個典型分散式事務場景,假設A需要跨行轉賬給B,那麼就涉及兩個銀行的資料,無法通過一個資料庫的本地事務保證轉賬的ACID,只能夠通過分散式事務來解決。

分散式事務就是指事務的發起者、資源及資源管理器和事務協調者分別位於分散式系統的不同節點之上。在上述轉賬的業務中,使用者A-100操作和使用者B+100操作不是位於同一個節點上。本質上來說,分散式事務就是為了保證在分散式場景下,資料操作的正確執行。

分散式事務在分散式環境下,為了滿足可用性、效能與降級服務的需要,降低一致性與隔離性的要求,一方面遵循 BASE 理論(BASE相關理論,涉及內容非常多,感興趣的同學,可以參考BASE理論):

  • 基本業務可用性(Basic Availability)
  • 柔性狀態(Soft state)
  • 最終一致性(Eventual consistency)

同樣的,分散式事務也部分遵循 ACID 規範:

  • 原子性:嚴格遵循
  • 一致性:事務完成後的一致性嚴格遵循;事務中的一致性可適當放寬
  • 隔離性:並行事務間不可影響;事務中間結果可見性允許安全放寬
  • 永續性:嚴格遵循

分散式事務的解決方案

由於分散式事務方案,無法做到完全的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事務時序圖如下:
34361ca2293a168c5725bb1f6f9f80b.png

如果有任何一個參與者prepare失敗,那麼TM會通知所有完成prepare的參與者進行回滾。

XA事務的特點是:

  • 簡單易理解,開發較容易
  • 對資源進行了長時間的鎖定,並行度低

如果讀者想要進一步研究XA,go語言以及PHP、Python、Java、C#、Node等都可參考DTM

SAGA

Saga是這一篇資料庫論文sagas提到的一個方案。其核心思想是將長事務拆分為多個本地短事務,由Saga事務協調器協調,如果正常結束那就正常完成,如果某個步驟失敗,則根據相反順序一次呼叫補償操作。

把上面的轉賬作為例子,一個成功完成的SAGA事務時序圖如下:

c4b9b43e0238e33834ee39540f7cb01.png

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事務時序圖如下:

9411356d82de37670337837a2cec8eb.png

TCC的Confirm/Cancel階段在業務邏輯上是不允許返回失敗的,如果因為網路或者其他臨時故障,導致不能返回成功,TM會不斷的重試,直到Confirm/Cancel返回成功。

TCC特點如下:

  • 並行度較高,無長期資源鎖定。
  • 開發量較大,需要提供Try/Confirm/Cancel介面。
  • 一致性較好,不會發生SAGA已扣款最後又轉賬失敗的情況
  • TCC適用於訂單類業務,對中間狀態有約束的業務

如果讀者想要進一步研究TCC,可參考DTM

本地訊息表

本地訊息表這個方案最初是 ebay 架構師 Dan Pritchett 在 2008 年發表給 ACM 的文章。設計核心是將需要分散式處理的任務通過訊息的方式來非同步確保執行。

大致流程如下:

d7b5e9a177d4d0de156bb542096ee6e.png

寫本地訊息和業務操作放在一個事務裡,保證了業務和發訊息的原子性,要麼他們全都成功,要麼全都失敗。

容錯機制:

  • 扣減餘額事務 失敗時,事務直接回滾,無後續步驟
  • 輪序生產訊息失敗, 增加餘額事務失敗都會進行重試

本地訊息表的特點:

  • 長事務僅需要分拆成多個任務,使用簡單
  • 生產者需要額外的建立訊息表
  • 每個本地訊息表都需要進行輪詢
  • 消費者的邏輯如果無法通過重試成功,那麼還需要更多的機制,來回滾操作

適用於可非同步執行的業務,且後續操作無需回滾的業務

事務訊息

在上述的本地訊息表方案中,生產者需要額外建立訊息表,還需要對本地訊息表進行輪詢,業務負擔較重。阿里開源的RocketMQ 4.3之後的版本正式支援事務訊息,該事務訊息本質上是把本地訊息表放到RocketMQ上,解決生產端的訊息傳送與本地事務執行的原子性問題。

事務訊息傳送及提交:

  • 傳送訊息(half訊息)
  • 伺服器端儲存訊息,並響應訊息的寫入結果
  • 根據傳送結果執行本地事務(如果寫入失敗,此時half訊息對業務不可見,本地邏輯不執行)
  • 根據本地事務狀態執行Commit或者Rollback(Commit操作釋出訊息,訊息對消費者可見)

正常傳送的流程圖如下:

e06a60c599e7aefe2f52e9525114c74.png

補償流程:

對沒有Commit/Rollback的事務訊息(pending狀態的訊息),從伺服器端發起一次「回查」
Producer收到回查訊息,返回訊息對應的本地事務的狀態,為Commit或者Rollback
事務訊息方案與本地訊息表機制非常類似,區別主要在於原先相關的本地表操作替換成了一個反查介面

事務訊息特點如下:

  • 長事務僅需要分拆成多個任務,並提供一個反查介面,使用簡單
  • 消費者的邏輯如果無法通過重試成功,那麼還需要更多的機制,來回滾操作

適用於可非同步執行的業務,且後續操作無需回滾的業務

如果讀者想要進一步研究事務訊息,可參考DTM,也可以參考Rocketmq

最大努力通知

發起通知方通過一定的機制最大努力將業務處理結果通知到接收方。具體包括:

有一定的訊息重複通知機制。因為接收通知方可能沒有接收到通知,此時要有一定的機制對訊息重複通知。
訊息校對機制。如果盡最大努力也沒有通知到接收方,或者接收方消費訊息後要再次消費,此時可由接收方主動向通知方查詢訊息資訊來滿足需求。
前面介紹的的本地訊息表和事務訊息都屬於可靠訊息,與這裡介紹的最大努力通知有什麼不同?

可靠訊息一致性,發起通知方需要保證將訊息發出去,並且將訊息發到接收通知方,訊息的可靠性關鍵由發起通知方來保證。

最大努力通知,發起通知方盡最大的努力將業務處理結果通知為接收通知方,但是可能訊息接收不到,此時需要接收通知方主動呼叫發起通知方的介面查詢業務處理結果,通知的可靠性關鍵在接收通知方。

解決方案上,最大努力通知需要:

  • 提供介面,讓接受通知放能夠通過介面查詢業務處理結果
  • 訊息佇列ACK機制,訊息佇列按照間隔1min、5min、10min、30min、1h、2h、5h、10h的方式,逐步拉大通知間隔 ,直到達到通知要求的時間視窗上限。之後不再通知

最大努力通知適用於業務通知型別,例如微信交易的結果,就是通過最大努力通知方式通知各個商戶,既有回撥通知,也有交易查詢介面

AT事務模式

這是阿里開源專案seata中的一種事務模式,在螞蟻金服也被稱為FMT。優點是該事務模式使用方式,類似XA模式,業務無需編寫各類補償操作,回滾由框架自動完成,缺點也類似XA,存在較長時間的鎖,不滿足高並行的場景。從效能的角度看,AT模式會比XA更高一些,但也帶來了髒回滾這樣的新問題。有興趣的同學可以參考seata-AT

例外處理

在分散式事務的各個環節都有可能出現網路以及業務故障等問題,這些問題需要分散式事務的業務方做到防空回滾,冪等,防懸掛三個特性。

異常情況

下面以TCC事務說明這些異常情況:

空回滾:

  在沒有呼叫 TCC 資源 Try 方法的情況下,呼叫了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然後直接返回成功。

  出現原因是當一個分支事務所在服務宕機或網路異常,分支事務呼叫記錄為失敗,這個時候其實是沒有執行Try階段,當故障恢復後,分散式事務進行回滾則會呼叫二階段的Cancel方法,從而形成空回滾。

冪等

  由於任何一個請求都可能出現網路異常,出現重複請求,所以所有的分散式事務分支,都需要保證冪等性

懸掛:

  懸掛就是對於一個分散式事務,其二階段 Cancel 介面比 Try 介面先執行。

  出現原因是在 RPC 呼叫分支事務try時,先註冊分支事務,再執行RPC呼叫,如果此時 RPC 呼叫的網路發生擁堵,RPC 超時以後,TM就會通知RM回滾該分散式事務,可能回滾完成後,Try 的 RPC 請求才到達參與者真正執行。

下面看一個網路異常的時序圖,更好的理解上述幾種問題
4f2c338265c84cabcb3f85e490a2a02.png

  • 業務處理請求4的時候,Cancel在Try之前執行,需要處理空回滾
  • 業務處理請求6的時候,Cancel重複執行,需要冪等
  • 業務處理請求8的時候,Try在Cancel後執行,需要處理懸掛

面對上述複雜的網路異常情況,目前看到各家建議的方案都是業務方通過唯一鍵,去查詢相關聯的操作是否已完成,如果已完成則直接返回成功。相關的判斷邏輯較複雜,易出錯,業務負擔重。

子事務屏障

在專案https://github.com/yedf/dtm中,出現了一種子事務屏障技術,使用該技術,能夠達到這個效果,看示意圖:
eb948d4f331b34c96bee89793b543fa.png

所有這些請求,到了子事務屏障後:不正常的請求,會被過濾;正常請求,通過屏障。開發者使用子事務屏障之後,前面所說的各種異常全部被妥善處理,業務開發人員只需要關注實際的業務邏輯,負擔大大降低。
子事務屏障提供了方法ThroughBarrierCall,方法的原型為:

func ThroughBarrierCall(db *sql.DB, transInfo *TransInfo, busiCall BusiFunc)

業務開發人員,在busiCall裡面編寫自己的相關邏輯,呼叫該函數。ThroughBarrierCall保證,在空回滾、懸掛等場景下,busiCall不會被呼叫;在業務被重複呼叫時,有冪等控制,保證只被提交一次。

子事務屏障會管理TCC、SAGA、事務訊息等,也可以擴充套件到其他領域

子事務屏障原理

子事務屏障技術的原理是,在本地資料庫,建立分支事務狀態表sub_trans_barrier,唯一鍵為全域性事務id-子事務id-子事務分支名稱(try|confirm|cancel)

  • 開啟事務
  • 如果是Try分支,則那麼insert ignore插入gid-branchid-try,如果成功插入,則呼叫屏障內邏輯
  • 如果是Confirm分支,那麼insert ignore插入gid-branchid-confirm,如果成功插入,則呼叫屏障內邏輯
  • 如果是Cancel分支,那麼insert ignore插入gid-branchid-try,再插入gid-branchid-cancel,如果try未插入並且cancel插入成功,則呼叫屏障內邏輯
  • 屏障內邏輯返回成功,提交事務,返回成功
  • 屏障內邏輯返回錯誤,回滾事務,返回錯誤

在此機制下,解決了網路異常相關的問題

  • 空補償控制--如果Try沒有執行,直接執行了Cancel,那麼Cancel插入gid-branchid-try會成功,不走屏障內的邏輯,保證了空補償控制
  • 冪等控制--任何一個分支都無法重複插入唯一鍵,保證了不會重複執行
  • 防懸掛控制--Try在Cancel之後執行,那麼插入的gid-branchid-try不成功,就不執行,保證了防懸掛控制

對於SAGA、事務訊息等,也是類似的機制。

子事務屏障小結

子事務屏障技術,為https://github.com/yedf/dtm首創,它的意義在於設計簡單易實現的演演算法,提供了簡單易用的介面,在首創,它的意義在於設計簡單易實現的演演算法,提供了簡單易用的介面,在這兩項的幫助下,開發人員徹底的從網路異常的處理中解放出來。

該技術目前需要搭配yedf/dtm事務管理器,目前SDK已經提供給Go、Python語言的開發者。其他語言的sdk正在規劃中。對於其他的分散式事務框架,只要提供了合適的分散式事務資訊,能夠按照上述原理,快速實現該技術。

總結

本文介紹了分散式事務的一些基礎理論,並對常用的分散式事務方案進行了講解,在文章的後半部分還給出了事務異常的原因、分類以及優雅的解決方案。

yedf/dtm支援了TCC、XA、SAGA、事務訊息、最大努力通知(使用事務訊息實現),提供了HTTP、gRPC協定支援,非常容易接入。

yedf/dtm已支援了Python、Java、PHP、C#、Node等語言的使用者端,參見:各語言SDK。

歡迎大家存取https://github.com/yedf/dtm專案,給顆星星支援!

以上就是總結分散式事務的7種解決方案(理論+方案)的詳細內容,更多請關注TW511.COM其它相關文章!