引言
在深入介紹 Seata 的實作之前,我們先在一個較高的層面一覽 Seata 的整體設計思想,其他 Seata 相關文章均收錄于
<Seata系列文章>中。
設計方案
整體架構
首先,很自然的,我們可以把一個分布式事務了解成一個包含了若幹分支事務的全局事務。全局事務的職責是協調其下管轄的分支事務達成一緻,要麼一起成功送出,要麼一起失敗復原。此外,通常分支事務本身就是一個滿足 ACID 的本地事務。

基于兩階段送出模式,從設計上我們可以将整體分成三個大子產品,即TM、RM、TC,具體解釋如下:
- TM(Transaction Manager):全局事務管理器,控制全局事務邊界,負責全局事務開啟、全局送出、全局復原。
- RM(Resource Manager):資料總管,控制分支事務,負責分支注冊、狀态彙報,并接收事務協調器的指令,驅動分支(本地)事務的送出和復原。
- TC(Transaction Coordinator):事務協調器,維護全局事務的運作狀态,負責協調并驅動全局事務的送出或復原。
一個典型的分布式事務過程:
- TM 向 TC 申請開啟一個全局事務,全局事務建立成功并生成一個全局唯一的 XID。
- XID 在微服務調用鍊路的上下文中傳播。
- RM 向 TC 注冊分支事務,将其納入 XID 對應全局事務的管轄。
- TM 向 TC 發起針對 XID 的全局送出或復原決議。
- TC 排程 XID 下管轄的全部分支事務完成送出或復原請求。
看到這,大家基本上就明白分布式事務處理的全貌了,實際上資料庫層面的 XA 協定,也是這樣做的。我們将整個這一部分從資料庫層抽離出來後,在進行分布式事務時,就不需要下層資料庫實作 XA 協定了,隻需要支援本地事務的 ACID 即可,分支的送出和復原機制,都依賴于本地事務的保障。這點對于微服務化的架構來說是非常重要的:應用層不需要為本地事務和分布式事務兩類不同場景來适配兩套不同的資料庫驅動。
那麼,Seata 就是将 XA 協定的實作理論從資料庫層面抽離出來這麼簡單麼?不僅僅如此,還記得前面提到的資料庫 XA 協定遇到性能問題無法優化的窘境麼,Seata 不僅解決了分布式事務的一緻性問題,還針對實際的應用場景,改善了 XA 方案的鎖機制,進而增加了并發能力。此外, Seata 不僅僅支援 2PC 模式, 還支援 TCC 等其他分布式事務處理模式, 使用者可以根據實際的應用場景自行選擇。
與 XA 的差別
我們先來看看 XA 協定的 2PC 過程:
無論 Phase2 的決議是 commit 還是 rollback,事務性資源的鎖都要保持到 Phase2 完成才釋放。
設想一個正常運作的業務,大機率是 90% 以上的事務最終應該是成功送出的,我們是否可以在 Phase1 就将本地事務送出呢?這樣 90% 以上的情況下,可以省去 Phase2 持鎖的時間,整體提高效率。
- 分支事務中資料的本地鎖由本地事務管理,在分支事務 Phase1 結束時釋放,這時候其他本地事務就能讀取到最新的資料。
- 同時,随着本地事務結束,連接配接也得以釋放。
- 分支事務中資料的全局鎖在事務協調器管理,在決議 Phase2 全局送出時,全局鎖馬上可以釋放,注意這裡是先釋放鎖,再進行分支事務的送出過程。隻有在決議全局復原的情況下,全局鎖才被持有至分支的 Phase2 結束,即所有分支事務復原結束。
這個設計,極大地減少了分支事務對資源(資料和連接配接)的鎖定時間,給整體并發和吞吐的提升提供了基礎。但是本地事務的鎖這麼早釋放,會不會有什麼問題呢?問題也是有的,就是分布式事務的隔離級别變化了,這個話題比較複雜,我們後面再詳細介紹。
分支事務
Seata 不僅支援像 XA 協定那種對業務無侵入的事務處理方式,還支援 TCC 等類型的處理方式,它們在不同的業務場景各顯神通,下面我将分别介紹它們。
AT
AT 模式是一種無侵入的分布式事務解決方案。在 AT 模式下,使用者隻需關注自己的“業務 SQL”,使用者的 “業務 SQL” 就是全局事務一階段,Seata 架構會自動生成事務的二階段送出和復原操作。
那麼 AT 模式是如何做到對業務無侵入的呢?
首先,應用要使用 Seata 的 JDBC 資料源代理,也就是前面提到的 RM 概念,所有對 DB 的操作都是通過 Seata RM 代理完成。在這層代理中,Seata 會自動控制 SQL 的執行,送出,復原。下圖中綠色部分是 JDBC 資料源的原生實作内容, 黃色部分就是 Seata 的資料源代理。
一階段
Seata 的 JDBC 資料源代理通過對業務 SQL 的解析,把業務資料在更新前後的資料鏡像(beforeImage & afterImage)組織成復原日志,利用本地事務的 ACID 特性,将業務資料的更新和復原日志的寫入在同一個本地事務中送出。這樣,可以保證:任何送出的業務資料的更新一定有相應的復原日志存在。
然後,本地事務在送出之前, 還需要通過 RM 向 TC 注冊本地分支,這個注冊過程中會根據剛才執行的 SQL 拿到所有涉及到的資料主鍵,以
resourceId + tableName + rowPK
作為鎖的 key,向 TC 申請所有涉及資料的寫鎖,當獲得所有相關資料的寫鎖後,再執行本地事務的 Commit 過程。如果有任何一行資料的寫鎖沒有拿到的話,TC 會以
fastfail
的方式回複該 RM,RM 會以重試 + 逾時機制重複該過程,直到逾時。
完成本地事務後,RM 會向 TC 彙報本地事務的執行情況,并完成業務 RPC 的調用過程。
這裡大家可能會有疑問,TM 一般是以 RPC 的方式調用 RM 的,那麼 TM 直接可以通過 RPC 的結果知道該 RM 的本地事務是否送出成功,那麼為什麼 RM 還需要向 TC 彙報本地事務的執行結果呢?實際上,TM 可以在某個 RM 執行失敗時,強制進行全局事務的送出,這時候如果 TC 發現某個 RM 的一階段過程都沒執行成功,就不會向其發送二階段的 Commit 指令了。
二階段
- 如果 TM 決議是全局送出,此時分支事務實際上已經完成送出,TC 立刻釋放該全局事務的所有鎖,然後異步調用 RM 清理復原日志,Phase2 可以非常快速地完成。
- 如果決議是全局復原,RM 收到協調器發來的復原請求,通過 XID 和 Branch ID 找到相應的復原日志記錄,通過復原記錄生成反向的更新 SQL 并執行,以完成分支的復原。當分支復原順利結束時,通知 TC 復原完成,這時候 TC 才釋放該分支事務相關的所有鎖。
這裡有一個需要注意的點,RM 在進行復原時,會先跟
afterImage
進行比較:
- 如果一緻:則執行逆向 SQL
- 如果不一緻: 再跟
進行比較beforeImage
- 如果一緻:說明沒必要執行復原 SQL 了,資料已經恢複了
- 如果不一緻:說明出現了髒資料,這時候就抛出異常,需要人工處理
復原失敗的思考
上述的復原失敗情況之是以會出現, 一般都是有人直接繞過系統直接操作 DB 資料導緻或者沒有正确的配置 RM 導緻的,因為即便是該 RM 執行單獨的本地事務,在進行适當的配置後(添加 GlobalLock 注解),也會在本地送出前試圖擷取 TC 中的資源鎖。
我認為 Seata 需要顯式地給現有函數加
GlobalLock
注解的方案有一個問題,如果一個表既會被本地事務更新, 也會在分布式事務中更新,那麼這個表的所有本地事務都需要加上該注解,才能完全杜絕復原失敗的問題。當然,這裡并不包括人直接繞過系統操作 DB 的場景,我認為這種繞過系統操作 DB 的場景需要單獨開發一個基于 Seata 的 SQL 執行系統,該系統需要保證本地事務送出前都需要擷取 TC 中的資源鎖,不過這種場景應該很少發生,我這裡不做過多的評論。
回到
GlobalLock
的問題中來,如果某一個子系統對外開放了一個分布式事務接口,那麼該接口更新過的任何一個表,如果在該系統的本地獨立事務中也會被修改,就會導緻前面所說的事務無法復原的問題。這時候,如果人工介入進來,一般需要先鎖住髒資料,然後根據資料庫執行記錄,人工修正資料,最後将 TC 中出錯的分支事務手動置為完成復原的狀态。聽起來好像也不是很難,但是如果對應的資料是熱點資料,在復原前更新了很多次,就需要人工确認冗長的修改曆史線,那簡直是一場災難。更有甚者,如果分布式事務中增加了某個人的存款餘額,比如0 -> 1000,復原前被其他獨立事務消費掉了500,那最後修完資料該使用者的實際餘額應該是-500,這就得聯系該使用者,追回這筆錢。這個例子,雖然有點極端,但是它就是一個例子。
那麼解決這個問題有什麼辦法呢,我簡單想了幾個:
- 在文檔和 Sample 中強調
注解的重要性,防止踩坑GlobalLock
- 評價:軟限制,出了問題影響依舊大,但是我覺得這個措施很有必要
- 全函數預設都有
注解GlobalLock
- 評價:性能影響大,有誤傷
-
注解,增加GlobalLock
注解,其效果和IgnoreGlobalLock
成反效果GlobalLock
- 評價:同樣有誤傷,如果某個服務,大多數表隻會被單獨本地事務修改,那麼加
注解的工作量也很大IgnoreGlobalLock
- 評價:同樣有誤傷,如果某個服務,大多數表隻會被單獨本地事務修改,那麼加
- 在 TC 中, 給每個 RM 維護一個分布式事務相關資源表 ResourceTableSet,内容為
,當進行分支事務注冊時,将對應資源表添加到該 ResourceTableSet,當該 ResourceTableSet 發生變化或者有新的 RM 連接配接到 TC 時, TC 主動将最新的 ResourceTableSet 推送給 RM,RM 本地事務執行 JDBC 代理時,如果函數不在全局事務或者全局鎖中,就先解析 SQL 的 AST,如果它涉及修改過程,并且修改的表在 ResourceTableSet 中,就自動更新該函數,使其達到與标有tableName
的函數一樣的效果,并且通知開發者該更新事件,這樣開發者就能找到漏加GlobalLock
注解的函數, 如果修改的表不在 ResourceTableSet , 我們可以将其所在的函數加入緩存中, 下次執行該函數時直接跳過分析, 恢複損耗的性能, 當 ResourceTableSet 更新時, 我們重新整理該緩存, 保證可靠性。同時,如果開發者明确的做了業務邏輯上的劃分,保證完全不會發生復原失敗的情況的話,我們可以提供一個GlobalLock
的注解,跳過上述過程,直接使用原生 JDBC 連接配接,進而減少性能的損耗。IgnoreAutoGlobalLock
- 評價:比較自動, 性能影響不是很大,具有一定程度的硬限制,但是 TC 中維護的 ResourceTableSet 存在空窗期,仍有潛在風險,可是風險比之前低了很多,并且系統在測試階段或者上線初期就能基本檢查出遺漏的函數,能減少由于開發者疏忽而引入的潛在風險
- 編譯期檢查?
- 評價:沒法在編譯期确定哪些函數會成為分布式事務中的分支事務(暴露出來, 但是沒人用),實作複雜,如果 SQL 中的表名是通過參數傳入,則無法檢查到
上述的反思, 我也給官方提了一個
issue, 感興趣的同學可以去看看。
TCC
TCC 模式需要使用者根據自己的業務場景實作 Try、Confirm 和 Cancel 三個操作;事務發起方先在 TC 中注冊全局事務,然後在一階段執行 Try 方法,在二階段送出的話 TC 會去執行各個 RM 的 Confirm 方法,二階段復原則 TC 會去執行各個 RM 的 Cancel 方法。
在 Seata 架構中,每個 TCC 接口對應了一個 Resource,TCC 接口可以是 RPC,也可以是服務内 JVM 調用。在業務啟動時,Seata 架構會自動掃描識别到 TCC 接口的調用方和釋出方。如果是 RPC 的話,就是 sofa:reference、sofa:service、dubbo:reference、dubbo:service 等, Seata 會檢查這些 RPC 接口是否有 TCC 相關的注解,有的話說明這個 RPC 是一個 TCC 接口,否則則是正常 RPC 過程,不劃入分布式事務中。
掃描到 TCC 接口的調用方和釋出方之後。如果是釋出方,會在業務啟動時向 TC 注冊 TCC Resource,與DataSource Resource 一樣,每個資源也會帶有一個資源 ID。
與 AT 模式一樣,Seata 會給實際方法的執行加切面,該切面會攔截所有對 TCC 接口的調用。在調用 Try 接口時,如果發現處在全局事務中,切面會先向 TC 注冊一個分支事務,和 AT 不同的是TCC 注冊分支事務是不加鎖的,注冊完成後去執行原來的 RPC 調用。當請求鍊路調用完成後,TC 通過分支事務的資源 ID 回調到正确的參與者去執行對應 TCC 資源的 Confirm 或 Cancel 方法。
TCC 模式的整體架構相對于 AT 來說更加簡單,主要是掃描 TCC 接口,注冊資源,攔截接口調用,注冊分支事務,最後回調二階段接口。最核心的實際上是 TCC 接口的實作邏輯。下面我将結合實際的例子,來介紹一下 TCC 模式相較于 AT 模式有什麼優勢和劣勢。
使用原則
從 TCC 模型的架構可以發現,TCC 模型的核心在于 TCC 接口的設計。使用者在接入 TCC 時,大部分工作都集中在如何實作 TCC 服務上。這就是 TCC 模式最主要的問題,對業務侵入比較大,要花很大的功夫來實作 TCC 服務。
設計一套 TCC 接口最重要的是什麼?主要有兩點,第一點,需要将操作分成兩階段完成。TCC(Try-Confirm-Cancel)分布式事務模型相對于 XA 等傳統模型,其特征在于它不依賴 RM 對分布式事務的支援,而是通過對業務邏輯的分解來實作分布式事務。
TCC 模型認為對于業務系統中一個特定的業務邏輯 ,其對外提供服務時,必須接受一些不确定性,即對業務邏輯初步操作的調用僅是一個臨時性操作,調用它的主業務服務保留了後續的取消權。如果主業務服務認為全局事務應該復原,它會要求取消之前的臨時性操作,這就對應從業務服務的取消操作。而當主業務服務認為全局事務應該送出時,它會放棄之前臨時性操作的取消權,這對應從業務服務的确認操作。每一個初步操作,最終都會被确認或取消。是以,針對一個具體的業務服務,TCC 分布式事務模型需要業務系統提供三段業務邏輯:
- 初步操作 Try:完成所有業務檢查,預留必須的業務資源。
- 确認操作 Confirm:真正執行的業務邏輯,不做任何業務檢查,隻使用 Try 階段預留的業務資源。是以,隻要 Try 操作成功,Confirm 必須能成功。另外,Confirm 操作需滿足幂等性,保證一筆分布式事務能且隻能成功一次。
- 取消操作 Cancel:釋放 Try 階段預留的業務資源。同樣的,Cancel 操作也需要滿足幂等性。
第二點,就是要根據自身的業務模型控制并發,這個對應 ACID 中的隔離性。
業務模型
下面我們以金融核心鍊路裡的賬務服務來分析一下。首先一個最簡化的賬務模型就是圖中所列,每個使用者或商戶有一個賬戶及其可用餘額。然後,分析下賬務服務的所有業務邏輯操作,無論是交易、充值、轉賬、退款等,都可以認為是對賬戶的加錢與扣錢。
是以,我們可以把賬務系統拆分成兩套 TCC 接口,即兩個 TCC Resource,一個是加錢 TCC 接口,一個是扣錢 TCC 接口。
那這兩套接口分别需要做什麼事情呢?如何将其分成兩個階段完成?下面将會舉例說明 TCC 業務模式的設計過程,并逐漸優化。
我們先來看扣錢的 TCC 資源怎麼實作。場景為 A 轉賬 30 元給 B。賬戶 A 的餘額中有 100 元,需要扣除其中 30 元。這裡的餘額就是所謂的業務資源,按照前面提到的原則,在第一階段需要檢查并預留業務資源,是以,我們在扣錢 TCC 資源的 Try 接口裡先檢查 A 賬戶餘額是否足夠,然後預留餘額裡的業務資源,即扣除 30 元。
在 Confirm 接口,由于業務資源已經在 Try 接口裡扣除掉了,那麼在第二階段的 Confirm 接口裡,可以什麼都不用做。而在 Cancel 接口裡,則需要把 Try 接口裡扣除掉的 30 元還給賬戶。這是一個比較簡單的扣錢 TCC 資源的實作,後面會繼續優化它。
而在加錢的 TCC 資源裡。在第一階段 Try 接口裡不能直接給賬戶加錢,如果這個時候給賬戶增加了可用餘額,那麼在一階段執行完後,賬戶裡的錢就可以被使用了。但是一階段執行完以後,有可能是要復原的。是以,真正加錢的動作需要放在 Confirm 接口裡。對于加錢這個動作,第一階段 Try 接口裡不需要預留任何資源,可以設計為空操作。那相應的,Cancel 接口沒有資源需要釋放,也是一個空操作。隻有真正需要送出時,再在 Confirm 接口裡給賬戶增加可用餘額。
這就是一個最簡單的扣錢和加錢的 TCC 資源的設計。在扣錢 TCC 資源裡,Try 接口預留資源扣除餘額,Confirm 接口空操作,Cancel 接口釋放資源,增加餘額。在加錢 TCC 資源裡,Try 接口無需預留資源,空操作;Confirm 接口直接增加餘額;Cancel 接口無需釋放資源,空操作。
業務并發模型
之前提到,設計一套 TCC 接口需要有兩點,一點是需要拆分業務邏輯成兩階段完成。這個我們已經介紹了。另外一點是要根據自身的業務模型控制并發。
Seata 架構本身僅提供兩階段原子送出協定,保證分布式事務原子性。事務的隔離需要交給業務邏輯來實作。隔離的本質就是控制并發,防止并發事務操作相同資源而引起的結果錯亂。
舉個例子,比如金融行業裡管理使用者資金,當使用者發起交易時,一般會先檢查使用者資金,如果資金充足,則扣除相應交易金額,增加賣家資金,完成交易。如果沒有事務隔離,使用者同時發起兩筆交易,兩筆交易的檢查都認為資金充足,實際上卻隻夠支付一筆交易,結果兩筆交易都支付成功,導緻資損。
可以發現,并發控制是業務邏輯執行正确的保證,但是像兩階段鎖這樣的并發通路控制技術要求一直持有資料庫資源鎖直到整個事務執行結束,特别是在分布式事務架構下,要求持有鎖到分布式事務第二階段執行結束,也就是說,分布式事務會加長資源鎖的持有時間,導緻并發性能進一步下降。
是以,TCC 模型的隔離性思想就是通過業務的改造,在第一階段結束之後,從底層資料庫資源層面的加鎖過渡為上層業務層面的加鎖,進而釋放底層資料庫鎖資源,放寬分布式事務鎖協定,将鎖的粒度降到最低,以最大限度提高業務并發性能。
還是以上面的例子舉例,“賬戶 A 上有 100 元,事務 T1 要扣除其中的 30 元,事務 T2 也要扣除 30 元,出現并發”。在第一階段 Try 操作中,需要先利用資料庫資源層面的加鎖,檢查賬戶可用餘額,如果餘額充足,則預留業務資源,扣除本次交易金額,一階段結束後,雖然資料庫層面資源鎖被釋放了,但這筆資金被業務隔離,不允許除本事務之外的其它并發事務動用。
并發的事務 T2 在事務 T1 一階段接口結束釋放了資料庫層面的資源鎖以後,就可以繼續操作,跟事務 T1 一樣,加鎖,檢查餘額,扣除交易金額。
事務 T1 和 T2 分别扣除的那一部分資金,互相之間無幹擾。這樣在分布式事務的二階段,無論 T1 是送出還是復原,都不會對 T2 産生影響,這樣 T1 和 T2 可以在同一個賬戶上并發執行。
大家可以感受下,一階段結束以後,實際上采用業務加鎖的方式,隔離賬戶資金,在第一階段結束後直接釋放底層資源鎖,該使用者和賣家的其他交易都可以立刻并發執行,而不用等到整個分布式事務結束,可以獲得更高的并發交易能力。
在這裡,TCC 模式和之前說過的 AT 模式差別是:
- AT 模式會持有鎖到全局事務送出,或在復原時持有鎖直到復原成功
- TCC 模式一階段結束就釋放鎖
想象一下一個業務要調用 A,B,C 三個子服務,如果是采用 AT 模式,那麼至少要等 C 結束後,才會釋放A,B,C 的相關資源鎖,而如果采用 TCC 模式,A 結束就會釋放 A 的鎖,B 結束就釋放 B 的鎖...。并發能力一下子就提高了 N 倍,這就是 TCC 相較于 AT 模式的優點————并發能力。
下面我們将會針對業務模型進行優化,大家可以更直覺的感受業務加鎖的思想。
業務模型優化
前面的模型大家肯定會想,為啥一階段就把錢扣除了?是的。之前隻是為了簡單說明 TCC 模型的設計思想。在實際中,為了更好的使用者體驗,在第一階段,一般不會直接把賬戶的餘額扣除,而是當機,這樣給使用者展示的時候,就可以很清晰的知道,哪些是可用餘額,哪些是當機金額。
那業務模型變成什麼樣了呢?如圖所示,需要在業務模型中增加當機金額字段,用來表示賬戶有多少金額處以當機狀态。
既然業務模型發生了變化,那扣錢和加錢的 TCC 接口也應該相應的調整。還是以前面的例子來說明。
在扣錢的 TCC 資源裡。Try 接口不再是直接扣除賬戶的可用餘額,而是真正的預留資源,當機部分可用餘額,即減少可用餘額,增加當機金額。Confirm 接口也不再是空操作,而是使用 Try 接口預留的業務資源,即将該部分當機金額扣除;最後在 Cancel 接口裡,就是釋放預留資源,把 Try 接口的當機金額扣除,增加賬戶可用餘額。加錢的 TCC資源由于不涉及當機金額的使用,是以無需更改。
通過這樣的優化,可以更直覺的感受到 TCC 接口的預留資源、使用資源、釋放資源的過程。
那并發控制又變成什麼樣了呢?跟前面大部分類似,在事務 T1 的第一階段 Try 操作中,先鎖定賬戶,檢查賬戶可用餘額,如果餘額充足,則預留業務資源,減少可用餘額,增加當機金額。并發的事務 T2 類似,加鎖,檢查餘額,減少可用餘額金額,增加當機金額。
這裡可以發現,事務 T1 和T2 在一階段執行完成後,都釋放了資料庫層面的資源鎖,但是在各自二階段的時候,互相之間并無幹擾,各自使用本事務内第一階段 Try 接口内當機金額即可。這裡大家就可以直覺感受到,在每個事務的第一階段,先通過資料庫層面的資源鎖,預留業務資源,即當機金額。雖然在一階段結束以後,資料庫層面的資源鎖被釋放了,但是第二階段的執行并不會被幹擾,這是因為資料庫層面資源鎖釋放以後通過業務隔離的方式為這部分資源加鎖,不允許除本事務之外的其它并發事務動用,進而保證該事務的第二階段能夠正确順利的執行。
通過這兩個例子,為大家講解了怎麼去設計一套完備的 TCC 接口。最主要的有兩點,一點是将業務邏輯拆分成兩個階段完成,即 Try、Confirm、Cancel 接口。其中 Try 接口檢查資源、預留資源、Confirm 使用資源、Cancel 接口釋放預留資源。另外一點就是并發控制,采用資料庫鎖與業務加鎖的方式結合。由于業務加鎖的特性不影響性能,是以,盡可能降低資料庫鎖粒度,過渡為業務加鎖,進而提高業務并發能力。
異常控制
在有了一套完備的 TCC 接口之後,是不是就真的高枕無憂了呢?答案是否定的。在微服務架構下,很有可能出現網絡逾時、重發,機器當機等一系列的異常 Case。一旦遇到這些 Case,就會導緻我們的分布式事務執行過程出現異常。最常見的主要是這三種異常,分别是空復原、幂等、懸挂。
是以,TCC 接口裡還需要解決這三類異常。實際上,這三類問題可以在 Seata 架構裡完成,隻不過現在的 Seata 架構還不具備,之後這些異常 Case 的處理會被移植到 Seata 架構裡,業務就無需關注這些異常情況,專注于業務邏輯即可。
雖然業務之後無需關心,但是了解一下其内部實作機制,也能更好的排查問題。下面我将為大家一一講解這三類異常出現的原因以及對應的解決方案。
空復原
首先是空復原。什麼是空復原?空復原就是對于一個分布式事務,在沒有調用 TCC 資源 Try 方法的情況下,調用了二階段的 Cancel 方法,Cancel 方法需要識别出這是一個空復原,然後直接傳回成功。
什麼樣的情形會造成空復原呢?可以看圖中的第 2 步,前面講過,注冊分支事務是在調用 RPC 時,Seata 架構的切面會攔截到該次調用請求,先向 TC 注冊一個分支事務,然後才去執行 RPC 調用邏輯。如果 RPC 調用邏輯有問題,比如調用方機器當機、網絡異常,都會造成 RPC 調用失敗,即未執行 Try 方法。但是分布式事務已經開啟了,需要推進到終态,是以,TC 會回調參與者二階段 Cancel 接口,進而形成空復原。
那會不會有空送出呢?理論上來說不會的,如果調用方當機,那分布式事務預設是復原的。如果是網絡異常,那 RPC 調用失敗,發起方應該通知 TC 復原分布式事務,這裡可以看出為什麼是理論上的,就是說發起方可以在 RPC 調用失敗的情況下依然通知 TC 送出,這時就會發生空送出,這種情況要麼是編碼問題,要麼開發同學明确知道需要這樣做。
那怎麼解決空復原呢?前面提到,Cancel 要識别出空復原,直接傳回成功。那關鍵就是要識别出這個空復原。思路很簡單就是需要知道一階段是否執行,如果執行了,那就是正常復原;如果沒執行,那就是空復原。是以,需要一張額外的事務控制表,其中有分布式事務 ID 和分支事務 ID,第一階段 Try 方法裡會插入一條記錄,表示一階段執行了。Cancel 接口裡讀取該記錄,如果該記錄存在,則正常復原;如果該記錄不存在,則是空復原。
幂等
接下來是幂等。幂等就是對于同一個分布式事務的同一個分支事務,重複去調用該分支事務的第二階段接口,是以,要求 TCC 的二階段 Confirm 和 Cancel 接口保證幂等,不會重複使用或者釋放資源。如果幂等控制沒有做好,很有可能導緻資損等嚴重問題。
什麼樣的情形會造成重複送出或復原?從圖中可以看到,送出或復原是一次 TC 到參與者的網絡調用。是以,網絡故障、參與者當機等都有可能造成參與者 TCC 資源實際執行了二階段防範,但是 TC 沒有收到傳回結果的情況,這時,TC 就會重複調用,直至調用成功,整個分布式事務結束。
怎麼解決重複執行的幂等問題呢?一個簡單的思路就是記錄每個分支事務的執行狀态。在執行前狀态,如果已執行,那就不再執行;否則,正常執行。前面在講空復原的時候,已經有一張事務控制表了,事務控制表的每條記錄關聯一個分支事務,那我們完全可以在這張事務控制表上加一個狀态字段,用來記錄每個分支事務的執行狀态。
這個過程有點類似于 AT 模式中的 Undo Log,我們不妨看下 Undo Log 的表結構:
CREATE TABLE `undo_log` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`branch_id` bigint(20) NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int(11) NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
`ext` varchar(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;
我們可以看到,通過該表中
branch_id
和
xid
我們可以确認該條記錄對應那個分支事務,然後
log_status
可以用來判斷該分支事務的執行情況。我們讓
log_status
有三個值,分别是初始化、已送出、已復原。Try 方法插入時,是初始化狀态。二階段 Confirm 和 Cancel 方法執行後修改為已送出或已復原狀态。當重複調用二階段接口時,先擷取該事務控制表對應記錄,檢查狀态,如果已執行,則直接傳回成功;否則正常執行。
懸挂
最後是防懸挂。按照慣例,咱們來先講講什麼是懸挂。懸挂就是對于一個分布式事務,其二階段 Cancel 接口比 Try 接口先執行。因為允許空復原的原因,Cancel 接口認為 Try 接口沒執行,空復原直接傳回成功,對于 Seata 架構來說,認為分布式事務的二階段接口已經執行成功,整個分布式事務就結束了。但是這之後 Try 方法才真正開始執行,預留業務資源,回想一下前面提到事務并發控制的業務加鎖,對于一個 Try 方法預留的業務資源,隻有該分布式事務才能使用,然而 Seata 架構認為該分布式事務已經結束,也就是說,當出現這種情況時,該分布式事務第一階段預留的業務資源就再也沒有人能夠處理了,對于這種情況,我們就稱為懸挂,即業務資源預留後沒有被繼續處理。
什麼樣的情況會造成懸挂呢?按照前面所講,在 RPC 調用時,先注冊分支事務,再執行 RPC 調用,如果此時 RPC 調用的網絡發生擁堵,通常 RPC 調用是有逾時時間的,RPC 逾時以後,發起方就會通知 TC 復原該分布式事務,可能復原完成後,RPC 請求才到達參與者,真正執行,進而造成懸挂。
怎麼實作才能做到防懸挂呢?根據懸挂出現的條件先來分析下,懸挂是指二階段 Cancel 執行完後,一階段才執行。也就是說,為了避免懸挂,如果二階段執行完成,那一階段就不能再繼續執行。是以,當一階段執行時,需要先檢查二階段是否已經執行完成,如果已經執行,則一階段不再執行;否則可以正常執行。那怎麼檢查二階段是否已經執行呢?大家是否想到了剛才解決空復原和幂等時用到的事務控制表,可以在二階段執行時插入一條事務控制記錄,狀态為已復原,這樣當一階段執行時,先讀取該記錄,如果記錄存在,就認為二階段已經執行;否則二階段沒執行。
異常控制實作
在分析完空復原、幂等、懸挂等異常 Case 的成因以及解決方案以後,下面我們就綜合起來考慮,一個 TCC 接口如何完整的解決這三個問題。
首先是 Try 方法。結合前面講到空復原和懸挂異常,Try 方法主要需要考慮兩個問題,一個是 Try 方法需要能夠告訴二階段接口,已經預留業務資源成功。第二個是需要檢查第二階段是否已經執行完成,如果已完成,則不再執行。是以,Try 方法的邏輯可以如圖所示:
先插入事務控制表記錄,如果插入成功,說明第二階段還沒有執行,可以繼續執行第一階段。如果插入失敗,則說明第二階段已經執行或正在執行,則抛出異常,終止即可。
接下來是 Confirm 方法。因為 Confirm 方法不允許空復原,也就是說,Confirm 方法一定要在 Try 方法之後執行。是以,Confirm 方法隻需要關注重複送出的問題。可以先鎖定事務記錄,如果事務記錄為空,則說明是一個空送出,不允許,終止執行。如果事務記錄不為空,則繼續檢查狀态是否為初始化,如果是,則說明一階段正确執行,那二階段正常執行即可。如果狀态是已送出,則認為是重複送出,直接傳回成功即可;如果狀态是已復原,也是一個異常,一個已復原的事務,不能重新送出,需要能夠攔截到這種異常情況,并報警。
最後是 Cancel 方法。因為 Cancel 方法允許空復原,并且要在先執行的情況下,讓 Try 方法感覺到 Cancel 已經執行,是以和 Confirm 方法略有不同。首先依然是鎖定事務記錄。如果事務記錄為空,則認為 Try 方法還沒執行,即是空復原。空復原的情況下,應該先插入一條事務記錄,確定後續的 Try 方法不會再執行。如果插入成功,則說明 Try 方法還沒有執行,空復原繼續執行。如果插入失敗,則認為Try 方法正在執行,等待 TC 的重試即可。如果一開始讀取事務記錄不為空,則說明 Try 方法已經執行完畢,再檢查狀态是否為初始化,如果是,則還沒有執行過其他二階段方法,正常執行 Cancel 邏輯。如果狀态為已復原,則說明這是重複調用,允許幂等,直接傳回成功即可。如果狀态為已送出,則同樣是一個異常,一個已送出的事務,不能再次復原。
通過這一部分的講解,大家應該對 TCC 模型下最常見的三類異常 Case,空復原、幂等、懸挂的成因有所了解,也從實際例子中知道了怎麼解決這三類異常,在解決了這三類異常的情況下,我們的 TCC 接口設計就是比較完備的了。雖然現在 Seata 架構中上述方案尚未實裝,但是以後一定會實裝的,到那時會由 Seata 架構來完成異常的處理,開發 TCC 接口的同學就不再需要關心了。
性能再優化
雖然 TCC 模型已經完備,但是随着業務的增長,對于 TCC 模型的挑戰也越來越大,可能還需要一些特殊的優化,才能滿足業務需求。下面我将會給大家講講,在 TCC 模型上還可以做哪些優化。
同庫模式
第一個優化方案是改為同庫模式。同庫模式簡單來說,就是分支事務記錄與業務資料在相同的庫中。什麼意思呢?之前提到,在注冊分支事務記錄的時候,架構的調用方切面會先向 TC 注冊一個分支事務記錄,注冊成功後,才會繼續往下執行 RPC 調用。TC 在收到分支事務記錄注冊請求後,會往自己的資料庫裡插入一條分支事務記錄,進而保證事務資料的持久化存儲。那同庫模式就是調用方切面不再向 TC 注冊了,而是直接往業務的資料庫裡插入一條事務記錄。
在講解同庫模式的性能優化點之前,先給大家簡單講講同庫模式的恢複邏輯。一個分布式事務的送出或復原還是由發起方通知 TC,但是由于分支事務記錄儲存在業務資料庫,而不是 TC 端。是以,TC 不知道有哪些分支事務記錄,在收到送出或復原的通知後,僅僅是記錄一下該分布式事務的狀态。那分支事務記錄怎麼真正執行第二階段呢?需要在各個參與者内部啟動一個異步任務,定期撈取業務資料庫中未結束的分支事務記錄,然後向 TC 檢查整個分布式事務的狀态,即圖中的 StateCheckRequest 請求。TC 在收到這個請求後,會根據之前儲存的分布式事務的狀态,告訴參與者是送出還是復原,進而完成分支事務記錄。
那這樣做有什麼好處呢?左邊是采用同庫模式前的調用關系圖,在每次調用一個參與者的時候,都是先向 TC 注冊一個分布式事務記錄,TC 再持久化存儲在自己的資料庫中,也就是說,一個分支事務記錄的注冊,包含一次 RPC 和一次持久化存儲。
右邊是優化後的調用關系圖。從圖中可以看出,每次調用一個參與者的時候,都是直接儲存在業務的資料庫中,進而減少與 TC 之間的 RPC 調用。優化後,有多少個參與者,就節約多少次 RPC 調用。
這就是同庫模式的性能方案。把分支事務記錄儲存在業務資料庫中,進而減少與 TC 的 RPC 調用。
異步化
另外一個性能優化方式就是異步化,什麼是異步化。TCC 模型的一個作用就是把兩階段拆分成了兩個獨立的階段,通過資源業務鎖定的方式進行關聯。資源業務鎖定方式的好處在于,既不會阻塞其他事務在第一階段對于相同資源的繼續使用,也不會影響本事務第二階段的正确執行。從理論上來說,隻要業務允許,事務的第二階段什麼時候執行都可以,反正資源已經業務鎖定,不會有其他事務動用該事務鎖定的資源。
假設隻有一個中間賬戶的情況下,每次調用支付服務的 Commit 接口,都會鎖定中間賬戶,中間賬戶存在熱點性能問題。
但是,在擔保交易場景中,七天以後才需要将資金從中間賬戶劃撥給商戶,中間賬戶并不需要對外展示。是以,在執行完支付服務的第一階段後,就可以認為本次交易的支付環節已經完成,并向使用者和商戶傳回支付成功的結果,并不需要馬上執行支付服務二階段的 Commit 接口,等到低峰期時,再慢慢消化,異步地執行。
Saga
Saga 模式是 Seata 即将開源的長事務解決方案。在 Saga 模式下,分布式事務内有多個參與者,每一個參與者都是一個沖正補償服務,需要使用者根據業務場景實作其正向操作和逆向復原操作。
分布式事務執行過程中,依次執行各參與者的正向操作,如果所有正向操作均執行成功,那麼分布式事務送出。如果任何一個正向操作執行失敗,那麼分布式事務會去退回去執行前面各參與者的逆向復原操作,復原已送出的參與者,使分布式事務回到初始狀态。
Saga 正向服務與補償服務也需要業務開發者實作。有點像是 TCC 模式将 Try 過程和 Confirm 過程合并,所有參與者直接執行 Try + Confirm,如果有人失敗了,就反向依次 Cancel。
由于該模式主要用于長事務場景,是以通常是由事件驅動的,各個參與者之間是異步執行的。
Saga 模式适用于業務流程長且需要保證事務最終一緻性的業務系統,Saga 模式一階段就會送出本地事務,無鎖、長流程情況下可以保證性能。
事務參與者可能是其它公司的服務或者是遺留系統的服務,無法進行改造和提供 TCC 要求的接口,可以使用 Saga 模式。
Saga模式的優勢是:
- 一階段送出本地資料庫事務,無鎖,高性能;
- 參與者可以采用事務驅動異步執行,高吞吐;
- 補償服務即正向服務的“反向”,易于了解,易于實作;
缺點:Saga 模式由于一階段已經送出本地資料庫事務,且沒有進行“預留”動作,是以不能保證隔離性。
Seata 目前是采用事件驅動的機制來實作的,Seata 實作了一個狀态機,可以編排服務的調用流程及正向服務的補償服務,生成一個 json 檔案定義的狀态圖,狀态機引擎驅動業務的運作,當發生異常的時候狀态機觸發復原,逐個執行補償服務。當然在什麼情況下觸發復原使用者是可以自定義決定的。
由于它基于事件驅動架構,每個步驟都是異步執行的,步驟與步驟之間通過事件隊列流轉,極大的提高系統吞吐量。每個步驟執行時會記錄事務日志,用于出現異常時復原時使用,事務日志會記錄在與業務表資料庫内,提高性能。
和 TCC 模式一樣 Saga 也會出現空復原、懸挂、幂等的問題,這些都可以參考 TCC 中介紹的方案進行。
前面講到 Saga 模式不保證事務的隔離性,在極端情況下可能出現髒寫。比如在分布式事務未送出的情況下,前一個服務的資料被修改了,而後面的服務發生了異常需要進行復原,可能由于前面服務的資料被修改後無法進行補償操作。這時的一種處理辦法可以是“重試”繼續往前完成這個分布式事務。由于整個業務流程是由狀态機編排的,即使是事後恢複也可以繼續往前重試。是以使用者可以根據業務特點配置該流程的事務處理政策是優先“復原”還是“重試”,當事務逾時的時候,Server 端會根據這個政策不斷進行重試。
此外,我們在業務設計的時候需要做到“甯可長款,不可短款”的原則,長款是指在出現差錯的時候站在我方的角度錢多了的情況,錢少了則是短款,因為如果長款可以給客戶退款,而短款則可能錢追不回來了,也就是說在業務設計的時候,一定是先扣客戶帳再入帳,如果因為隔離性問題造成覆寫更新,也不會出現錢少了的情況。
XA
XA 模式是 Seata 将來會開源的另一種無侵入的分布式事務解決方案,任何實作了 XA 協定的資料庫都可以作為資源參與到分布式事務中,目前主流資料庫,例如 MySql、Oracle、DB2、Oceanbase 等均支援 XA 協定。
XA 協定有一系列的指令,分别對應一階段和二階段操作。“xa start”和 “xa end”用于開啟和結束XA 事務;“xa prepare” 用于預送出 XA 事務,對應一階段準備;“xa commit”和“xa rollback”用于送出、復原 XA 事務,對應二階段送出和復原。
在 XA 模式下,每一個 XA 事務都是一個事務參與者。分布式事務開啟之後,首先在一階段執行“xa start”、“業務 SQL”、“xa end”和 “xa prepare” 完成 XA 事務的執行和預送出;二階段如果送出的話就執行 “xa commit”,如果是復原則執行“xa rollback”。這樣便能保證所有 XA 事務都送出或者都復原。
XA 模式下,使用者隻需關注自己的“業務 SQL”,Seata 架構會自動生成一階段、二階段操作;XA 模式的實作如下:
- 一階段:在 XA 模式的一階段,Seata 會攔截“業務 SQL”,在“業務 SQL”之前開啟 XA 事務(“xa start”),然後執行“業務 SQL”,結束 XA 事務“xa end”,最後預送出 XA 事務(“xa prepare”),這樣便完成 “業務 SQL”的準備操作。
- 二階段送出:執行“xa commit”指令,送出 XA 事務,此時“業務 SQL”才算真正的送出至資料庫。
- 二階段復原:執行“xa rollback”指令,復原 XA 事務,完成“業務 SQL”復原,釋放資料庫鎖資源。
XA 模式下,使用者隻需關注“業務 SQL”,Seata 會自動生成一階段、二階段送出和二階段復原操作。XA 模式和 AT 模式一樣是一種對業務無侵入性的解決方案;但與 AT 模式不同的是,XA 模式将快照資料和行鎖等通過 XA 指令委托給了資料庫來完成,這樣 XA 模式實作更加輕量化。
MT
在之前的版本中,Seata 中還有一個 MT 模式,它的一個重要作用就是,可以把非關系型資料庫的資源,通過 MT 模式分支的包裝,納入到全局事務的管轄中來。比如,Redis、HBase、RocketMQ 的事務消息等。
它的設計和 2PC 的風格類似,使用者需要實作自己的 MT 接口:
- 一階段 prepare 行為:調用 自定義 的 prepare 邏輯。
- 二階段 commit 行為:調用 自定義 的 commit 邏輯。
- 二階段 rollback 行為:調用 自定義 的 rollback 邏輯。
所謂 MT 模式,是指支援把自定義的分支事務納入到全局事務的管理中。不過該模式已經被删除了,我覺得它所做的工作通過 TCC 模式完全能夠做到。
事務隔離
Seata 的設計建立在一個共識上: 絕大部分應用在讀已送出的隔離級别下工作是沒有問題的。而實際上,這當中又有絕大多數的應用場景,實際上工作在讀未送出的隔離級别下同樣沒有問題。
縱觀 Seata 提供的所有分支事務模式, 除了 AT 模式和 XA 模式可以運作在讀已送出的隔離級别下, 其他模式都是運作在讀未送出的級别下。在有必要時,應用需要通過業務邏輯的巧妙設定,來解決分布式事務隔離級别帶來的問題,就像我們在 TCC 模式中介紹的例子。
AT模式
AT 模式前面我們已經介紹過, RM 在一階段會向 TC 申請資料的主鍵鎖, 鎖的結構是:
resourceId + tableName + rowPK
, 在資料庫本地隔離級别 讀已送出或以上 的前提下,AT 模式通過全局寫排他鎖,來保證事務間的寫隔離,将全局事務預設定義在讀未送出的隔離級别上,全局事務讀未送出,并不是說本地事務的db資料沒有正常送出,而是指全局事務二階段commit | rollback未真正處理完(即未釋放全局鎖),而且這時候其他事務會讀到一階段送出的内容。
預設情況下,AT 是工作在讀未送出的隔離級别下,保證絕大多數場景的高效性。有些應用如果需要達到全局的讀已送出,AT 也提供了相應的機制來達到目的,那就是
select for update
+ @GlobalLock, 當執行該指令時 RM 會去 TC 确認該鎖是否由他人占有, 這樣如果有一個分布式事務 T1 正在進行中時, 另一個事務 T2 會因為發現鎖沖突而阻塞後續代碼的執行, 目前面的分布式事務 T1 結束時, 釋放了相應的資源鎖, T2 才能讀取到相應的資料, 這樣就達到讀已送出的效果。
這裡大家可能會有點疑問, 因為 AT 模式下, TC 是先放鎖, 再執行各個 RM 的 Branch Commit 過程, 這是不是會出現
select for update
+ @GlobalLock 的髒讀啊? 答案是:不會。我們看 Branch Commit 過程, 它實際上做的隻是異步删除 Undo log, 真正執行的 SQL 在第一階段就已經執行完了。而復原時, 是每執行完一個分支事務, 再釋放該分支事務的鎖, 這時候會讀到全局事務開始之前的内容, 也不會出現髒讀。
XA模式
這裡我們以 MySQL 為例, 說一說 XA 的隔離級别問題。先看一下 MySQL XA 對本地隔離級别的要求:
However, for a distributed transaction, you must use the SERIALIZABLE isolation level to achieve ACID properties. It is enough to use REPEATABLE READ for a nondistributed transaction, but not for a distributed transaction
隻有在 Serializable 隔離級别下,XA 事務才能夠避免髒讀的。因為在 Serializable 隔離級别下, 所有讀操作都會施加排它鎖, 而在全局事務送出後, 才會釋放該鎖。在分布式事務中, 雖然不可能做到所有 XA 資料庫同時送出本地事務, 但是在一個分布式事務 T1 進行中, 其他事務 TN 不可能讀到 T1 的中間狀态, 它們隻會以一定的順序(因為鎖阻塞)看到 T1 開始前的狀态, 或者 T1 結束後的狀态,這就避免了髒讀。
事務傳播
XID 是一個全局事務的唯一辨別,事務傳播機制要做的就是把 XID 在服務調用鍊路中傳遞下去,并綁定到服務的事務上下文中,這樣,服務鍊路中的資料庫更新操作,就都會向該 XID 代表的全局事務注冊分支,納入同一個全局事務的管轄。
基于這個機制,Seata 是可以支援任何微服務 RPC 架構的。隻要在特定架構中找到可以透明傳播 XID 的機制即可,比如,Dubbo 的 Filter + RpcContext。
對于 Java EE 規範和 Spring 定義的事務傳播屬性,Seata 的支援如下:
- PROPAGATION_REQUIRED: 預設的spring事務傳播級别,使用該級别的特點是,如果上下文中已經存在事務,那麼就加入到事務中執行,如果目前上下文中不存在事務,則建立事務執行。
- Seata 的預設模式,在需要建立事務的所有地方使用
@GlobalTransactional
- Seata 的預設模式,在需要建立事務的所有地方使用
- PROPAGATION_SUPPORTS: 如果上下文存在事務,則支援事務加入事務,如果沒有事務,則使用非事務的方式執行。
- Seata 的預設模式, 隻在最外層業務函數加
, 中間層的時候不加該注解, 它就不會注冊新事務@GlobalTransactional
- Seata 的預設模式, 隻在最外層業務函數加
- PROPAGATION_MANDATORY: 該級别的事務要求上下文中必須要存在事務,否則就會抛出異常
- 業務方可以通過調用靜态函數
檢視是否處于事務中, 如果發現不在事務中, 則自己抛出異常RootContext#getXID
- 業務方可以通過調用靜态函數
- PROPAGATION_REQUIRES_NEW: 每次都會建立一個事務,并且同時将上下文中的事務挂起,執行目前建立事務完成以後(不關心成功與失敗),上下文事務恢複再執行。
- 定義一個殼函數, 在殼函數中, 先通過
擷取目前所處事務的 XID, 暫存起來, 然後清除 RootContext 中的 XID, 接下來調用自己的實際函數, 該實際函數需要打上 GlobalTransactional 注解和 Spring 的事務注解, spring 的事務注解需要明确辨別執行在 PROPAGATION_REQUIRES_NEW 傳播級别下, 實際函數執行結束後, 在殼函數中要 catch 住實際函數的所有異常, 最後将暫存的 XID 恢複進 RootContextRootContext#getXID
- 定義一個殼函數, 在殼函數中, 先通過
- PROPAGATION_NOT_SUPPORTED:上下文中存在事務,則挂起事務,執行目前邏輯,結束後恢複上下文的事務。
- 通過
發現處于事務中, 則 catch 住自己的所有異常RootContext#getXID
- 通過
- PROPAGATION_NEVER: 上下文中不能存在事務,一旦有事務,就抛出runtime異常,強制停止執行
-
發現處于事務中, 則抛出異常RootContext#getXID
-
- PROPAGATION_NESTED:如果上下文中存在事務,則嵌套事務執行,如果不存在事務,則建立事務。嵌套是子事務套在父事務中執行,子事務是父事務的一部分,在進入子事務之前,父事務建立一個復原點,叫save point,然後執行子事務,這個子事務的執行也算是父事務的一部分,然後子事務執行結束,父事務繼續執行。如果子事務復原,父事務會復原到進入子事務前建立的save point,然後嘗試其他的事務或者其他的業務邏輯,父事務之前的操作不會受到影響,更不會自動復原。如果父事務復原,子事務是不會送出的,我們說子事務是父事務的一部分,正是這個道理。事務的送出時,子事務是父事務的一部分,由父事務統一送出。
- 目前不支援。子事務失敗,父事務繼續執行,這個可以實作,參考 PROPAGATION_REQUIRES_NEW 就行了,但是這裡涉及到子事務結束時,并不直接送出,而是随父事務一起送出,這需要改 Seata 的源碼了,要在 TC 中儲存父子事務的綁定關系,然後子事務送出時,TC 先判斷一下目前事務是否有父事務, 并且傳播級别是否是
, 如果都滿足則不立即進行該事務的送出隻儲存意向, 在父事務送出時, 判斷其有沒有嵌套子事務, 如果有的話就按照其意向進行送出。PROPAGATION_NESTED
- 目前不支援。子事務失敗,父事務繼續執行,這個可以實作,參考 PROPAGATION_REQUIRES_NEW 就行了,但是這裡涉及到子事務結束時,并不直接送出,而是随父事務一起送出,這需要改 Seata 的源碼了,要在 TC 中儲存父子事務的綁定關系,然後子事務送出時,TC 先判斷一下目前事務是否有父事務, 并且傳播級别是否是
HA
目前 Seata 沒有真正意義上的 HA Cluster 方案,但是有一個臨時方案: TC 的資料可以存儲在 DB 中。這和預設的方案(存儲在本地檔案中)相比,性能會差一點, 但是借助資料庫的 HA 機制, TC 确實也能叢集化部署。
将來 Seata 的 HA-Cluster 設計可能會按如下思路進行:
- 用戶端釋出資訊的時候根據 transactionId 保證同一個 transaction 是在同一個 Seata Cluster 上,通過多個 Seata Cluster Master 水準擴充,提供并發處理性能。
- 在 server 端中一個 master 有多個 slave,master 中的資料實時同步到 slave 上,保證當 master 當機的時候,還能有其他 slave 頂上來可以用。
就目前的實作進度而言, 上圖中的 Master 和 Slave 可以了解為 DB 的 Master 和 Slave, 這些都是有現成的, 而且 Seata 支援将資料存在 DB 中, 并使用 DB 鎖來實作 TC 中的資源鎖。這就相當于多個 TC 節點綁定在一個 DB 叢集上,構成一個 TC 叢集對外開放服務。
不過說不定以後也會采用一緻性協定, 如 Paxos 或 Raft, 自己實作 Cluster 方案。
文章說明
更多有價值的文章均收錄于
貝貝貓的文章目錄版權聲明: 本部落格所有文章除特别聲明外,均采用 BY-NC-SA 許可協定。轉載請注明出處!
創作聲明: 本文基于下列所有參考内容進行創作,其中可能涉及複制、修改或者轉換,圖檔均來自網絡,如有侵權請聯系我,我會第一時間進行删除。
參考内容
[1]
fescar鎖設計和隔離級别的了解[2]
分布式事務中間件 Fescar - RM 子產品源碼解讀[3]
Fescar分布式事務實作原了解析探秘[4]
Seata TCC 分布式事務源碼分析[5]
深度剖析一站式分布式事務方案 Seata-Server[6]
分布式事務 Seata Saga 模式首秀以及三種模式詳解[7]
螞蟻金服大規模分布式事務實踐和開源詳解[8]
分布式事務 Seata TCC 模式深度解析[9]
Fescar (Seata)0.4.0 中文文檔教程[10]
Seata Github Wiki[11]
深度剖析一站式分布式事務方案Seata(Fescar)-Server