基於Seata探尋分佈式事務的實現方案

語言: CN / TW / HK
作者:京東物流技術與數據智能部 張碩

1 背景知識

隨着業務的快速發展、業務複雜度越來越高,幾乎每個公司的系統都會從單體走向分佈式,特別是轉向微服務架構。隨之而來就必然遇到分佈式事務這個難題,這篇文章通過seata框架總結了分佈式事務的幾種解決方案

1.1 ACID

關係型數據庫具有解決複雜事務場景的能力,關係型數據庫的事務滿足 ACID 的特性。

  • Atomicity:原子性(要麼都做,要麼都不做)
  • Consistency:一致性(數據庫只有一個狀態,不存在未確定狀態)
  • Isolation:隔離性(事務之間互不干擾)
  • Durability:永久性(事務一旦提交,數據庫記錄永久不變)

1.2 CAP

CAP 是指在一個分佈式系統下, 包含三個要素:Consistency(一致性)、Availability(可用性)、Partition tolerance(分區容錯性),並且三者不可得兼。

  • C:Consistency,一致性,所有數據變動都是同步的。
  • A:Availability,可用性,即在可以接受的時間範圍內正確地響應用户請求。
  • P:Partition tolerance,分區容錯性,即某節點或網絡分區故障時,系統仍能夠提供滿足一致性和可用性的服務。

1.3 BASE

BASE 理論主要是解決 CAP 理論中分佈式系統的可用性和一致性不可兼得的問題。BASE 理論包含以下三個要素:

  • BA:Basically Available,基本可用。
  • S:Soft State,軟狀態,狀態可以有一段時間不同步。
  • E:Eventually Consistent,最終一致,最終數據是一致的就可以了,而不是時時保持強一致。

2 實現模式

2.1 二段提交

第一階段(準備階段)

TM 通知所有參與事務的各個 RM,給每個 RM 發送 prepare 消息。
RM 接收到消息後進入準備階段後,要麼直接返回失敗,要麼創建並執行本地事務,寫本地事務日誌(redo 和 undo 日誌),但是不提交(此處只保留最後一步耗時最少的提交操作給第二階段執行)。

第二階段(提交 / 回滾階段)

Seata框架

基於兩階段提交模式,從設計上我們可以將整體分成三個大模塊,即TM、RM、TC,具體解釋如下:

  • TM(Transaction Manager):全局事務管理器,控制全局事務邊界,負責全局事務開啟、全局提交、全局回滾。
  • RM(Resource Manager):資源管理器,控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾。
  • TC(Transaction Coordinator):事務協調器,維護全局事務的運行狀態,負責協調並驅動全局事務的提交或回滾。

一個典型的分佈式事務過程:

  • TM 向 TC 申請開啟一個全局事務,全局事務創建成功並生成一個全局唯一的 XID。
  • XID 在微服務調用鏈路的上下文中傳播。
  • RM 向 TC 註冊分支事務,將其納入 XID 對應全局事務的管轄。
  • TM 向 TC 發起針對 XID 的全局提交或回滾決議。
  • TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求。

2.2 XA

在 XA 模式下,每一個 XA 事務都是一個事務參與者。分佈式事務開啟之後

首先在一階段執行“xa start”、“業務 SQL”、“xa end”和 “xa prepare” 完成 XA 事務的執行和預提交;

二階段如果提交的話就執行 “xa commit”,如果是回滾則執行“xa rollback”。這樣便能保證所有 XA 事務都提交或者都回滾。

無論 Phase2 的決議是 commit 還是 rollback,事務性資源的鎖都要保持到 Phase2 完成才釋放。

一個正常運行的業務,大概率是 90% 以上的事務最終應該是成功提交的,我們是否可以在 Phase1 就將本地事務提交呢?這樣 90% 以上的情況下,可以省去 Phase2 持鎖的時間,整體提高效率。

分支事務中數據的本地鎖由本地事務管理,在分支事務 Phase1 結束時釋放,這時候其他本地事務就能讀取到最新的數據。 - 同時,隨着本地事務結束,連接也得以釋放。 - 分支事務中數據的全局鎖在事務協調器管理,在決議 Phase2 全局提交時,全局鎖馬上可以釋放,注意這裏是先釋放鎖,再進行分支事務的提交過程。只有在決議全局回滾的情況下,全局鎖才被持有至分支的 Phase2 結束,即所有分支事務回滾結束。

這個設計,極大地減少了分支事務對資源(數據和連接)的鎖定時間,給整體併發和吞吐的提升提供了基礎。但是分佈式事務的隔離級別變化了

XA 模式和 下面的AT 模式一樣是一種對業務無侵入性的解決方案;但與 AT 模式不同的是,XA 模式將快照數據和行鎖等通過 XA 指令委託給了數據庫來完成,這樣 XA 模式實現更加輕量化

2.3 AT

AT 模式是一種無侵入的分佈式事務解決方案。在 AT 模式下,用户只需關注自己的“業務 SQL”,用户的 “業務 SQL” 就是全局事務一階段,Seata 框架會自動生成事務的二階段提交和回滾操作。

一階段

首先,應用要使用 Seata 的 JDBC 數據源代理,也就是前面提到的 RM 概念,所有對 DB 的操作都是通過 Seata RM 代理完成。在這層代理中,Seata 會自動控制 SQL 的執行,提交,回滾。

Seata代理會把業務數據在更新前後的數據鏡像(beforeImage & afterImage)組織成回滾日誌,利用本地事務的 ACID 特性,將業務數據的更新和回滾日誌的寫入在同一個本地事務中提交。這樣,可以保證:任何提交的業務數據的更新一定有相應的回滾日誌存在。

然後,本地事務在提交之前, 還需要通過 RM 向 TC 註冊本地分支,這個註冊過程中會根據剛才執行的 SQL 拿到所有涉及到的數據主鍵,以 resourceId + tableName + rowPK 作為鎖的 key,向 TC 申請所有涉及數據的寫鎖,當獲得所有相關數據的寫鎖後,再執行本地事務的 Commit 過程。如果有任何一行數據的寫鎖沒有拿到的話,TC 會以 fastfail 的方式回覆該 RM,RM 會以重試 + 超時機制重複該過程,直到超時。

完成本地事務後,RM 會向 TC 彙報本地事務的執行情況,並完成業務 RPC 的調用過程。

二階段

case1:如果 TM 決議是全局提交,此時分支事務實際上已經完成提交,TC 立刻釋放該全局事務的所有鎖,然後異步調用 RM 清理回滾日誌,Phase2 可以非常快速地完成。

case2:如果決議是全局回滾,RM 收到協調器發來的回滾請求,通過 XID 和 Branch ID 找到相應的回滾日誌記錄,通過回滾記錄生成反向的更新 SQL 並執行,以完成分支的回滾。當分支回滾順利結束時,通知 TC 回滾完成,這時候 TC 才釋放該分支事務相關的所有鎖。

注:RM 在進行回滾時,會先跟 afterImage 進行比較: - 如果一致:則執行逆向 SQL - 如果不一致: 再跟 beforeImage 進行比較 - 如果一致:説明沒必要執行回滾 SQL 了,數據已經恢復了 - 如果不一致:説明出現了髒數據,這時候就拋出異常,需要人工處理

2.4 TCC

TCC 模式需要用户根據自己的業務場景實現 Try、Confirm 和 Cancel 三個操作;事務發起方先在 TC 中註冊全局事務,然後在一階段執行 Try 方法,在二階段提交的話 TC 會去執行各個 RM 的 Confirm 方法,二階段回滾則 TC 會去執行各個 RM 的 Cancel 方法。

與 AT 模式一樣,Seata 會給實際方法的執行加切面,該切面會攔截所有對 TCC 接口的調用。在調用 Try 接口時,如果發現處在全局事務中,切面會先向 TC 註冊一個分支事務,和 AT 不同的是TCC 註冊分支事務是不加鎖的,註冊完成後去執行原來的 RPC 調用。當請求鏈路調用完成後,TC 通過分支事務的資源 ID 回調到正確的參與者去執行對應 TCC 資源的 Confirm 或 Cancel 方法。

TCC 模式的整體框架相對於 AT 來説更加簡單,主要是掃描 TCC 接口,註冊資源,攔截接口調用,註冊分支事務,最後回調二階段接口。最核心的實際上是 TCC 接口的實現邏輯。

1)使用原則

從 TCC 模型的框架可以發現,TCC 模型的核心在於 TCC 接口的設計。用户在接入 TCC 時,大部分工作都集中在如何實現 TCC 服務上。這就是 TCC 模式最主要的問題,對業務侵入比較大,要花很大的功夫來實現 TCC 服務。

設計一套 TCC 接口最重要的是什麼?主要有兩點,第一點,需要將操作分成兩階段完成。TCC(Try-Confirm-Cancel)分佈式事務模型相對於 XA 等傳統模型,其特徵在於它不依賴 RM 對分佈式事務的支持,而是通過對業務邏輯的分解來實現分佈式事務。

TCC 分佈式事務模型需要業務系統提供三段業務邏輯: 1. 初步操作 Try:完成所有業務檢查,預留必須的業務資源。 2. 確認操作 Confirm:真正執行的業務邏輯,不做任何業務檢查,只使用 Try 階段預留的業務資源。因此,只要 Try 操作成功,Confirm 必須能成功。另外,Confirm 操作需滿足冪等性,保證一筆分佈式事務能且只能成功一次。 3. 取消操作 Cancel:釋放 Try 階段預留的業務資源。同樣的,Cancel 操作也需要滿足冪等性。因此,TCC 模型的隔離性思想就是通過業務的改造,在第一階段結束之後,從底層數據庫資源層面的加鎖過渡為上層業務層面的加鎖,釋放底層數據庫鎖資源,放寬分佈式事務鎖協議,將鎖的粒度降到最低,以最大限度提高業務併發性能。

第二點,就是要根據自身的業務模型去控制併發,Seata 框架本身僅提供兩階段原子提交協議,保證分佈式事務原子性。事務的隔離需要交給業務邏輯來實現。隔離的本質就是控制併發,防止併發事務操作相同資源而引起的結果錯亂。例如:“賬户 A 上有 100 元,事務 T1 要扣除其中的 30 元,事務 T2 也要扣除 30 元,出現併發”。在第一階段 Try 操作中,需要先利用數據庫資源層面的加鎖,檢查賬户可用餘額,如果餘額充足,則預留業務資源加到各自的凍結裏,扣除本次交易金額,一階段結束後,雖然數據庫層面資源鎖被釋放了,但這筆資金被業務隔離,不允許除本事務之外的其它併發事務動用。

2)異常控制

空回滾

空回滾就是對於一個分佈式事務,在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識別出這是一個空回滾,然後直接返回成功。

Cancel 要識別出空回滾,直接返回成功。那關鍵就是要識別出這個空回滾。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常回滾;如果沒執行,那就是空回滾。因此,需要一張額外的事務控制表,其中有分佈式事務 ID 和分支事務 ID,第一階段 Try 方法裏會插入一條記錄,表示一階段執行了。Cancel 接口裏讀取該記錄,如果該記錄存在,則正常回滾;如果該記錄不存在,則是空回滾。

懸掛

懸掛就是對於一個分佈式事務,其二階段 Cancel 接口比 Try 接口先執行。因為允許空回滾的原因,Cancel 接口認為 Try 接口沒執行,空回滾直接返回成功,對於 Seata 框架來説,認為分佈式事務的二階段接口已經執行成功,整個分佈式事務就結束了。但是這之後 Try 方法才真正開始執行,預留業務資源,回想一下前面提到事務併發控制的業務加鎖,對於一個 Try 方法預留的業務資源,只有該分佈式事務才能使用,然而 Seata 框架認為該分佈式事務已經結束,也就是説,當出現這種情況時,該分佈式事務第一階段預留的業務資源就再也沒有人能夠處理了。

比如在 RPC 調用時,先註冊分支事務,再執行 RPC 調用,如果此時 RPC 調用的網絡發生擁堵,通常 RPC 調用是有超時時間的,RPC 超時以後,發起方就會通知 TC 回滾該分佈式事務,可能回滾完成後,RPC 請求才到達參與者,真正執行,從而造成懸掛。

冪等

冪等就是對於同一個分佈式事務的同一個分支事務,重複去調用該分支事務的第二階段接口,因此,要求 TCC 的二階段 Confirm 和 Cancel 接口保證冪等,不會重複使用或者釋放資源。如果冪等控制沒有做好,很有可能導致資損等嚴重問題。

解決思路

Try 方法主要需要考慮兩個問題,一個是 Try 方法需要能夠吿訴二階段接口,已經預留業務資源成功。第二個是需要檢查第二階段是否已經執行完成,如果已完成,則不再執行

Confirm 方法。因為 Confirm 方法不允許空回滾,也就是説,Confirm 方法一定要在 Try 方法之後執行。因此,Confirm 方法只需要關注重複提交的問題。需要一張事務執行記錄表,可以先鎖定事務記錄,如果事務記錄為空,則説明是一個空提交,不允許,終止執行。如果事務記錄不為空,則繼續檢查狀態是否為初始化,如果是,則説明一階段正確執行,那二階段正常執行即可。如果狀態是已提交,則認為是重複提交,直接返回成功即可;如果狀態是已回滾,也是一個異常,一個已回滾的事務,不能重新提交,需要能夠攔截到這種異常情況,並報警。

Cancel 方法。因為 Cancel 方法允許空回滾,並且要在先執行的情況下,讓 Try 方法感知到 Cancel 已經執行,所以和 Confirm 方法略有不同。首先依然是鎖定事務記錄。如果事務記錄為空,則認為 Try 方法還沒執行,即是空回滾。空回滾的情況下,應該先插入一條事務記錄,確保後續的 Try 方法不會再執行。如果插入成功,則説明 Try 方法還沒有執行,空回滾繼續執行。如果插入失敗,則認為Try 方法正在執行,等待 TC 的重試即可。如果一開始讀取事務記錄不為空,則説明 Try 方法已經執行完畢,再檢查狀態是否為初始化,如果是,則還沒有執行過其他二階段方法,正常執行 Cancel 邏輯。如果狀態為已回滾,則説明這是重複調用,允許冪等,直接返回成功即可。如果狀態為已提交,則同樣是一個異常,一個已提交的事務,不能再次回滾

2.5 Saga

Saga 模式是 Seata 即將開源的長事務解決方案。在 Saga 模式下,分佈式事務內有多個參與者,每一個參與者都是一個衝正補償服務,需要用户根據業務場景實現其正向操作和逆向回滾操作。

分佈式事務執行過程中,依次執行各參與者的正向操作,如果所有正向操作均執行成功,那麼分佈式事務提交。如果任何一個正向操作執行失敗,那麼分佈式事務會去退回去執行前面各參與者的逆向回滾操作,回滾已提交的參與者,使分佈式事務回到初始狀態。

Saga 正向服務與補償服務也需要業務開發者實現。有點像是 TCC 模式將 Try 過程和 Confirm 過程合併,所有參與者直接執行 Try + Confirm,如果有人失敗了,就反向依次 Cancel。

由於該模式主要用於長事務場景,所以通常是由事件驅動的,各個參與者之間是異步執行的。

Saga 模式適用於業務流程長且需要保證事務最終一致性的業務系統,Saga 模式一階段就會提交本地事務,無鎖、長流程情況下可以保證性能

Saga模式的優勢是:

  • 一階段提交本地數據庫事務,無鎖,高性能;
  • 參與者可以採用事務驅動異步執行,高吞吐;
  • 補償服務即正向服務的“反向”,易於理解,易於實現;

缺點:Saga 模式由於一階段已經提交本地數據庫事務,且沒有進行“預留”動作,所以不能保證隔離性。

事務隔離

縱觀 Seata 提供的所有分支事務模式, 除了 AT 模式和 XA 模式可以運行在讀已提交的隔離級別下, 其他模式都是運行在讀未提交的級別下。在有必要時,應用需要通過業務邏輯的巧妙設定,來解決分佈式事務隔離級別帶來的問題

AT 模式通過全局寫排他鎖,來保證事務間的寫隔離,將全局事務默認定義在讀未提交的隔離級別上,全局事務讀未提交,並不是説本地事務的db數據沒有正常提交,而是指全局事務二階段commit | rollback未真正處理完(即未釋放全局鎖),而且這時候其他事務會讀到一階段提交的內容。

有些應用如果需要達到全局的讀已提交,AT 也提供了相應的機制來達到目的,那就是 select for update + @GlobalLock, 當執行該命令時 RM 會去 TC 確認該鎖是否由他人佔有, 這樣如果有一個分佈式事務 T1 正在進行中時, 另一個事務 T2 會因為發現鎖衝突而阻塞後續代碼的執行, 當前面的分佈式事務 T1 結束時, 釋放了相應的資源鎖, T2 才能讀取到相應的數據, 這樣就達到讀已提交的效果

2.6 消息組件

利用 MQ 組件實現的二階段提交。此方案涉及 3 個模塊:

  • 上游應用,執行業務併發送 MQ 消息。
  • 可靠消息服務和 MQ 消息組件,協調上下游消息的傳遞,並確保上下游數據的一致性。
  • 下游應用,監聽 MQ 的消息並執行自身業務。

上游應用將本地業務執行和消息發送綁定在同一個本地事務中,保證要麼本地操作成功併發送 MQ 消息,要麼兩步操作都失敗並回滾。

  1. 上游應用發送待確認消息到可靠消息系統
  2. 可靠消息系統保存待確認消息並返回
  3. 上游應用執行本地業務
  4. 上游應用通知可靠消息系統確認業務已執行併發送消息。
  5. 可靠消息系統修改消息狀態為發送狀態並將消息投遞到 MQ 中間件。

  1. 下游應用監聽 MQ 消息組件並獲取消息
  2. 下游應用根據 MQ 消息體信息處理本地業務
  3. 下游應用向 MQ 組件自動發送 ACK 確認消息被消費
  4. 下游應用通知可靠消息系統消息被成功消費,可靠消息將該消息狀態更改為已完成。

異常處理

上游異常
可靠消息服務定時監聽消息的狀態,如果存在狀態為待確認並且超時的消息,則表示上游應用和可靠消息交互中的步驟 4 或者 5 出現異常。

  1. 可靠消息查詢超時的待確認狀態的消息
  2. 向上遊應用查詢業務執行的情況
  3. 業務未執行,則刪除該消息,保證業務和可靠消息服務的一致性。業務已執行,則修改消息狀態為已發送,併發送消息到 MQ 組件。

下游異常

  1. 可靠消息服務定時查詢狀態為已發送並超時的消息
  2. 可靠消息將消息重新投遞到 MQ 組件中
  3. 下游應用監聽消息,在滿足冪等性的條件下,重新執行業務。
  4. 下游應用通知可靠消息服務該消息已經成功消費。

實際過程中,還需要引入人工干預功能。比如引入重發次數限制,超過重發次數限制的將消息修改為死亡消息,等待人工處理。

3 總結

3.1 sql支持上

AT其實就是一個自實現的XA事務,其實可以知道,AT在sql支持上遠不及XA模式,AT需要做sql解析背後的實現只能自己解決,目前只能靠社區的貢獻者來提供解決方案,這是一個長期的關鍵性的問題,也有很多用户選擇在AT模式上重寫sql來獲取AT模式的支持,sql支持上XA是完勝的。

3.2 隔離性

AT模式是通過解析sql獲取涉及的主鍵id,生成行鎖。也就是AT模式的隔離靠的是全局鎖來保證的,粒度細至行級。鎖信息存儲在seata server側。XA的隔離級別是由本地數據庫保證,鎖存儲在各個本地數據庫中。由於XA模式一旦執行了prepare後,再也無法重入這個XA事務也無法跟其他XA事務共享鎖,因為XA協議僅僅是通過XID來start一個事務,本身不存在分支事務的説法。也就是説他只管自己

3.3 入侵性

通過上面的信息可以發現誰更底層,入侵性則更小,所以由數據庫自身支持的XA模式來説,入侵性無疑最小,使用成本最低。 XA的RM實際是在數據庫,而AT則是以中間件層部署在應用這一側的,不依賴數據庫本身的協議支持,這點對於微服務架構來説是至關重要的。應用層不需要為本地事務和分佈式事務多類不同場景來適配多套不通的驅動

3.4 補償性事務的問題

本質上seata框架支持了3大補償事務模式,AT, TCC,Saga都是補償型的。補償型事務處理機制是構建在事務資源之上的,事務資源本身對分佈式事務是無感知的,事務資源對分佈式事務無感知存在一個根本性問題,就是無法做到真正的全局一致性。 比如一條庫存記錄,處在補償型事務處理過程中,由100扣減為50,此時倉庫管理員連接數據庫查看就會查詢到50,之後事務異常回滾,庫存就會被補償回滾為100,顯然倉庫管理員查詢到的50就是髒數據。那XA的價值是什麼,與補償型事務不同,XA協議要求事務資源本身提供對規範和協議的支持。因為事務資源感知並參與分佈式事務處理過程,所以事務資源可以保證從任意視角對數據的訪問有效隔離,比如上面XA事務處理過程中,中間態的50是不會被查詢到的(當然隔離級別要在讀已提交以上),以此來滿足全局一致性。