關于全域事務的執行,雖然之前的文章中也有所涉及,但不夠細致,今天再深入的看一下事務的整個執行程序是怎樣的,
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發請求查詢鎖,如果這些資料沒有被任何事務加鎖,或者被當前事務加鎖,則都算獲取到鎖了,如果被別的事務加鎖了,則算獲取鎖失敗,
總結一下鎖互斥,分這么幾種情況:
- 兩個@GlobalTransactional方法之間,會在注冊分支事務的時候檢查全域鎖,注冊成功(獲取鎖成功)才能提交
- 兩個@GlobalLock方法之間,會在事務提交前檢查全域鎖,獲取到鎖才能提交
- @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 協議的機制來管理分支事務的一種事務模式
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/537989.html
標籤:Java
上一篇:TreeUtils工具類一行代碼實作串列轉樹【第三版優化】 三級選單 三級分類 附視頻
下一篇:向下遞回以及向上遞回
