Seata 1.5.2 原始碼學習(事務執行)

2022-11-22 12:00:21

關於全域性事務的執行,雖然之前的文章中也有所涉及,但不夠細緻,今天再深入的看一下事務的整個執行過程是怎樣的。

1. TransactionManager

io.seata.core.model.TransactionManager是事務管理器,它定義了一個全域性事務的相關操作

DefaultTransactionManager是TransactionManager的一個實現類

可以看到,所有操作(開啟、提交、回滾、查詢狀態、上報)都是呼叫TmNettyRemotingClient#sendSyncRequest()方法向TC發請求

2. GlobalTransaction

DefaultGlobalTransaction實現了GlobalTransaction,它代表一個全域性事務

有兩件事情需要留意,一是transactionManager是什麼? 二是GlobalTransactionRole又是什麼?

採用靜態內部類的形式來構造單例,還記得DefaultRMHandler和DefaultResourceManager也都是通過靜態內部類的形式構造單例

3. TransactionalTemplate

TransactionalTemplate是全域性事務執行模板,所有業務邏輯都在其定義的模板方法中執行

io.seata.tm.api.TransactionalTemplate#execute()

現在整個過程清楚了,首先根據事務傳播特性來建立一個事務物件,然後開啟事務,執行業務邏輯處理,最後提交事務,如果業務執行過程中拋異常,則回滾事務。

現在有一個問題,什麼情況下會進入TransactionalTemplate#execute(),或者說什麼時候呼叫該方法?

要回答這個問題,又得從io.seata.spring.annotation.GlobalTransactionScanner說起,這個前面已經說過了,想了解的可以再看看之前那篇 https://www.cnblogs.com/cjsblog/p/16866796.html

從GlobalTransactionScanner說起就太長了,直接快進到GlobalTransactionalInterceptor攔截器吧

當被呼叫的方法上有@GlobalTransactional註解時,就會被攔截,從而進入GlobalTransactionalInterceptor#invoke(),在invoke()裡會呼叫GlobalTransactionalInterceptor#handleGlobalTransaction(),於是順利進入TransactionalTemplate#execute()

也就是說,當進入第一個@GlobalTransactional方法時,此時全域性事務為空,於是建立一個角色為「GlobalTransactionRole.Launcher」的DefaultGlobalTransaction。當方法內部又呼叫了另一個@GlobalTransactional方法,於是再建立一個角色為「GlobalTransactionRole.Participant」的DefaultGlobalTransaction。以此類推,後面的都是事務「參與者」。

好了,現在事務已經建立,接下來就可以開啟事務並執行業務邏輯處理了

可以看到,只有角色為「GlobalTransactionRole.Launcher」的執行緒才可以執行事務的開啟提交回滾操作,而且這些操作的底層都是呼叫TransactionManager中的方法,最終是呼叫TmNettyRemotingClient#sendSyncRequest()方法向TC傳送同步請求

最後,看一下什麼時候回滾

catch捕獲到異常就回滾

以上這些說的都是TM,因為是TM在控制整個全域性事務的執行,至於RM本地事務的執行要看io.seata.rm.datasource.ConnectionProxy,這個在之前都講過了

4. GlobalLockTemplate

GlobalLockTemplate是全域性鎖模板,是需要全域性鎖的本地事務的一個執行器模板

那麼,在哪裡用這個"TX_LOCK"執行緒變數呢?在BaseTransactionalExecutor#execute()

預設ConnectionContext中isGlobalLockRequire為false

現在就很清晰了,當方法上加了@GlobalLock註解後,進入GlobalLockTemplate#execute(),在當前執行緒上繫結區域性變數TX_LOCK=true。當本地事務提交的時候,上下文(ConnectionContext)中isGlobalLockRequire為true,於是給TC發請求查詢鎖,如果這些資料沒有被任何事務加鎖,或者被當前事務加鎖,則都算獲取到鎖了,如果被別的事務加鎖了,則算獲取鎖失敗。

總結一下鎖互斥,分這麼幾種情況:

  1. 兩個@GlobalTransactional方法之間,會在註冊分支事務的時候檢查全域性鎖,註冊成功(獲取鎖成功)才能提交
  2. 兩個@GlobalLock方法之間,會在事務提交前檢查全域性鎖,獲取到鎖才能提交
  3. @GlobalTransactional方法與@GlobalLock方法之間,都是在提交前,一個是分支註冊檢查鎖,一個是直接檢查鎖

還有一個問題,哪些資料會被加鎖呢?這就要從io.seata.rm.datasource.exec.ExecuteTemplate#execute()說起了

長話短說,什麼樣的資料加鎖取決於資料庫,以及SQL語句,自行理解一下吧

5. 總結

1、Seata到底是如何實現分散式事務的?

  • 首先,每個業務系統都要引入seata的jar包,因此每個業務系統都是一個seata client,於是資料來源被seata代理,同時所有方法新增攔截器,對加了@GlobalTransactional的方法進行攔截處理;
  • 其次,進入事務方法後,按照模板方法定義,在try...catch...finally中先建立事務並開啟,接著執行業務處理,如果拋異常則回滾,如果順利執行完成,則提交;
  • 再次,被呼叫的遠端服務在其本地開啟事務並執行,將業務處理和undo_log放在同一個事務中,然後向TC註冊分支事務,成功後提交本地事務並向TC報告分支狀態
  • 最後,業務順利執行完或拋異常後TM向TC發請求可以提交或回滾全域性事務了,TC向所有已註冊的分支事務傳送提交或回滾請求

總之,資料來源代理和全域性事務掃描是seata實現分散式事務的基礎,而TM做的事情就是控制事務的執行,RM做的事就是處理好本地事務的執行,TC是協調器

2、Seata實現的全域性事務,它的事務隔離級別是怎樣的?會不會出現髒讀、幻讀、不可重複讀?

先看髒讀,在全域性事務提交之前,分支事務早已提交,因此,預設情況下,其它的事務是可以讀取到當前未提交的全域性事務的資料的,故而,預設情況下會發生髒讀。

舉個例子,假設現在有一個全域性事務A還沒提交,但是其中的分支事務A1已經提交,A2還在沒提交,這個時候另一個全域性事務B是可以讀取到A1已經提交的資料的,也就是在全域性事務B中讀到了還未提交的全域性事務A的資料,這就是髒讀。

那麼,如何避免髒讀呢?

思路是這樣的:首先要讓Seata意識到這個SQL語句執行時鎖,光知道需要鎖還不行,還得讓它在執行的時候檢查是否獲取到鎖了。一個SELECT語句需要鎖就是將其改寫成SELECT ... FOR UPDATE的形式,檢查鎖的話@GlobalTransactional或@GlobalLock都可以辦到。於是,解決版本就有兩個:

  • SELECT ... FOR UPDATE  +  @GlobalTransactional
  • SELECT ... FOR UPDATE  +  @GlobalLock

綜上所述,分支事務在提交前先進行分支註冊獲取全域性鎖,在全域性事務提交成功後釋放全域性鎖。此時,其它全域性事務可以讀取到已提交的分支事務的資料,但這是當前全域性事務還未提交,於是出現髒讀。辦法也很簡單,首先select加for update,其次業務方法加@GlobalTransactional或@GlobalLock註解。

同理,預設是可能出現幻讀和不可重複讀的,它倆屬於是髒寫,究其原因還是因為跨資料庫了,seata搞了個全域性鎖,這就相當於將業務中幾個不同的資料庫看成一個資料庫,全域性鎖就相當於這個巨量資料庫中的行級鎖,因此解決辦法還是一樣

不得不說,Seata真的是一個優秀的分散式事務框架

3、AT模式、TCC模式、Saga模式、XA模式的區別

AT模式是基於支援本地事務的關係型資料庫

TCC模式不依賴於資料庫的事務支援,另外TCC沒有全域性鎖,也就沒有鎖競爭,故而效率比AT模式高

Saga模式是seata提供的長事務解決方案

XA模式以 XA 協定的機制來管理分支事務的一種事務模式