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

陶然陶然發表於2023-01-06

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

  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 訊息,要麼兩步操作都失敗並回滾。

  上游應用傳送待確認訊息到可靠訊息系統

  可靠訊息系統儲存待確認訊息並返回

  上游應用執行本地業務

  上游應用通知可靠訊息系統確認業務已執行併傳送訊息。

  可靠訊息系統修改訊息狀態為傳送狀態並將訊息投遞到 MQ 中介軟體。

  下游應用監聽 MQ 訊息元件並獲取訊息

  下游應用根據 MQ 訊息體資訊處理本地業務

  下游應用向 MQ 元件自動傳送 ACK 確認訊息被消費

  下游應用通知可靠訊息系統訊息被成功消費,可靠訊息將該訊息狀態更改為已完成。

  異常處理

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

  可靠訊息查詢超時的待確認狀態的訊息

  向上遊應用查詢業務執行的情況

  業務未執行,則刪除該訊息,保證業務和可靠訊息服務的一致性。業務已執行,則修改訊息狀態為已傳送,併傳送訊息到 MQ 元件。

  下游異常

  可靠訊息服務定時查詢狀態為已傳送並超時的訊息

  可靠訊息將訊息重新投遞到 MQ 元件中

  下游應用監聽訊息,在滿足冪等性的條件下,重新執行業務。

  下游應用通知可靠訊息服務該訊息已經成功消費。

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

  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是不會被查詢到的(當然隔離級別要在讀已提交以上),以此來滿足全域性一致性。

來自 “ 京東雲 ”, 原文作者:張碩;原文連結:http://server.it168.com/a2023/0106/6784/000006784708.shtml,如有侵權,請聯絡管理員刪除。

相關文章