關於全局事務的執行,雖然之前的文章中也有所涉及,但不夠細緻,今天再深入的看一下事務的整個執行過程是怎樣的。 1. TransactionManager io.seata.core.model.TransactionManager是事務管理器,它定義了一個全局事務的相關操作 DefaultTransa ...
關於全局事務的執行,雖然之前的文章中也有所涉及,但不夠細緻,今天再深入的看一下事務的整個執行過程是怎樣的。
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 協議的機制來管理分支事務的一種事務模式