聊聊Seata分散式解決方案AT模式的實現原理

又見阿郎發表於2023-05-20

什麼是Seata分散式事務解決方案

Seata是一款開源的分散式事務解決方案,致力於提供高效能和簡單易用的分散式事務服務。為使用者提供了AT、TCC、SAGA和XA事務模式,為使用者打造一站式的分散式解決方案。

AT模式

AT模式目前來看是Seata框架獨有的一種模式,其它的分散式框架上並沒有此種模式的實現。其是由二階段提交演變來的,解決了二階段提交的同步阻塞等問題。
演變後的兩階段提交協議:

  • 一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源。
  • 二階段:
    • 提交非同步化,非常快速地完成。
    • 回滾透過一階段的回滾日誌進行反向補償。

Seata框架中有三個概念要闡述下。

  • TC:事務協調器,它是獨立的元件,需要獨立部署執行,Seata提供了這個獨立執行的程式,它負責維護全域性事務的執行狀態,接收TM指令發起全域性事務的提交與回滾,負責與RM通訊,協調各個分支事務的提交或回滾。
  • TM:事務管理器,TM需要嵌入應用程式,它負責開啟一個全域性事務,並定義全域性事務的範圍,它的目的是最終向TC發起全域性提交或回滾指令。
  • RM:與TC通訊,控制分支事務,負責分支註冊、報告分支事務狀態,並接收事務協調器TC的指令,命令分支事務完成本地事務的提交或回滾。

流程示意圖如下:

image

這個圖還是比較清晰地,建議是先好好的理解下全流程。

Seata AT模式的具體流程如下。
(1)訂單服務收到請求,訂單服務中的TM向TC申請開啟一個全域性事務。(2)TC收到請求,建立一個全域性事務,並將全域性事務ID(稱為XID)返回給訂單服務的TM。
(3)訂單服務的RM向TC註冊分支事務。
(4)訂單服務執行本地分支事務的業務邏輯並提交,釋放鎖定的資料庫資源。
(5)訂單服務向TC上報本地分支事務的提交結果。
(6)訂單服務呼叫遠端的積分服務,此時將XID透過引數傳給積分服務。(7)積分服務向TC註冊分支事務。
(8)積分服務執行本地分支事務的業務邏輯並提交,釋放鎖定的資料庫資源,並返回訂單服務。
(9)積分服務向TC上報本地分支事務的提交結果。
(10)訂單服務的TM向TC發起全域性事務的提交或回滾。
(11)TC向XID管轄下的全部分支事務發出提交或回滾的指令。

實現原理

我個人覺得框架其實也是一種需求的兌現,只是不像平常開發時那樣有產品經理給你輸出需求文件(應該也會有,但是少),業務流程不是傳統的那種XXX業務,框架的需求一般是偏向技術一些,我把它認為是技術需求;而日常我們做的開發一般是業務需求的兌現。

上面的流程可以看作是需求,那麼現在需求出來了,程式猿要怎麼實現?

設計思路

TM的設計

流程的開始與結束是由TM決定的,這個TM就是訂單服務Controller入口進去後的某個Service方法(當然也可能不是,看你的程式碼架構,我這裡只按照我自己的平常的開發模式來。)在這個Service方法中,處理了訂單服務以及積分服務的業務。當Service方法完成後,那麼就是整個業務做完了,事務即完成。因此,要我來實現,這個TM對應的Service方法,我會選擇用一個註解包裹,透過動態代理的方式,在這個方法的前後分別負責全域性事務的開始與結束流程。

RM的設計

RM負責執行具體的業務,將資料入庫同時上報給TC。由於二階段回滾時需要根據回滾日誌replay,那麼就一定需要記錄業務資料執行的日誌。那怎麼記錄?回想了下Mybatis中的資料來源代理,這裡也是一樣的思路。必須攔截或是代理原先的資料來源,解析原執行的sql,注入Seata的邏輯,增強其執行邏輯。我們看下RM要做的事情,第一階段:

image

第二階段提交:

image

第二階段回滾:

image

在執行sql的過程中,各個代理物件起到的作用如下

image

所以在RM端最關鍵的就是資料來源代理以及遠端通訊。這兩塊尤其是前者,就是AT模式的技術實現。

TC的設計

TC端需要管理Seata全域性事務會話資訊,一般是由全域性事務、分支事務、全域性鎖構成,對應表globaltable、branchtable、lock_table。因此在安裝部署的時候(file模式除外)都會建立這三個表。從上面來看,目前我們還沒有實現隔離性,所謂的隔離性是指多個使用者併發訪問資料庫時,資料庫為每個使用者開啟的事務不能被其他事務的操作所干擾,多個併發事務之間要相互隔離。這也是這裡有一個全域性鎖表的原因。每次本地事務提交前,都會向TC端申請註冊分支,同時還會申請全域性鎖,RM端透過拿到的全域性鎖保證了讀寫的隔離性,因此一旦當前事務持有全域性鎖,那麼其他的事務不能提交。

寫隔離

兩個全域性事務 tx1 和 tx2,分別對 a 表的 m 欄位進行更新操作,m 的初始值 1000。

  1. tx1 先開始,開啟本地事務,拿到本地鎖,更新操作 m = 1000 - 100 = 900。本地事務提交前,先拿到該記錄的全域性鎖 ,本地提交釋放本地鎖。
  2. tx2 後開始,開啟本地事務,拿到本地鎖,更新操作 m = 900 - 100 = 800。本地事務提交前,嘗試拿該記錄的全域性鎖,tx1 全域性提交前,該記錄的全域性鎖被 tx1 持有,tx2 需要重試等待全域性鎖。
  3. tx1 二階段全域性提交,釋放全域性鎖。tx2 拿到全域性鎖提交本地事務。

image

如果 tx1 的二階段全域性回滾,則 tx1 需要重新獲取該資料的本地鎖,進行反向補償的更新操作,實現分支的回滾。

此時,如果 tx2 仍在等待該資料的全域性鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗。分支的回滾會一直重試,直到 tx2 的全域性鎖等鎖超時,放棄全域性鎖並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功。

因為整個過程全域性鎖在 tx1 結束前一直是被 tx1 持有的,所以不會發生髒寫的問題。

image

讀隔離

image

seata at 模式預設的隔離級別為讀未提交(因為已經提交的sql有可能會回滾)。如果要實現讀已提交,select語句需要更改為 SELECT FOR UPDATE 語句。

SELECT FOR UPDATE 語句的執行會申請全域性鎖,如果全域性鎖被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試。這個過程中,查詢是被 block 住的,直到 全域性鎖 拿到,即讀取的相關資料是已提交的,才返回。

總結

Seata AT可以給你帶來一種“無侵入”式的程式設計體驗,你不需要改動任何業務程式碼,只需要一個註解和少量的配置資訊,就可以實現分散式事務。

總結來看,AT模式主要是是基於 DataSource 代理實現的,透過代理 DataSource、Connection、PreparedStatement,攔截 SQL 執行,增強其執行邏輯,由代理側加入額外的能力以提供分散式事務服務類似自動擋駕駛模式,分散式事務這個強大且複雜的服務能力由Seata框架託管,對業務實現無侵入式,使用者仍然只專注於業務 SQL。