轉之:https://toutiao.io/posts/g6jmss/preview
目錄
-
什么是 Seata AT 模式
-
Seata AT 的使用方法
-
第一步,增加全局事務注解
-
第二步,配置代理數據源
-
第三步,新建 undo_log 表
-
Seata AT 的工作流程
-
工作流程總覽
-
圖解 AT 模式一階段流程
-
圖解二階段 Commit 流程
-
圖解二階段 Rollback 流程
-
本節小結
-
Seata AT 模式源碼模塊拆解
-
Seata AT 模式客戶端部分
-
數據源代理部分 —— 三類 Proxy
-
ExecuteTemplate.execute
-
執行器接口 execute 的實現
-
抽象方法 doExecute 的實現
-
ConnectionProxy 復寫的 commit 方法
-
執行一階段本地事務提交
-
GlobalLock 的具體作用
-
二階段異步刪除分支 UndoLog
-
二階段生成反向 SQL 回滾
-
Seata AT 模式服務端部分
-
服務端提交全局事務
-
服務端異步提交分支事務
-
服務端同步回滾分支事務
-
Seata AT 模式的全局鎖
-
全局鎖的組成和作用
-
全局鎖的注冊
-
全局鎖的查詢
-
全局鎖的釋放
-
Seata AT 模式潛在優化點
-
全文總結
什么是 Seata AT 模式
AT 模式是 Seata 主推的分布式事務解決方案,最早來源於阿里中間件團隊發布的 TXC服務,后來成功上雲改名 GTS。Seata 官方文檔中有關於 AT 模式的詳細介紹 —— AT Mode [1],它使得應用代碼可以像使用本地事務一樣使用分布式事務,完全屏蔽了底層細節,它和筆者之前介紹過的 Seata TCC 模式的區別有以下幾點:
-
使用上,TCC 依賴於用戶自行實現的三個方法成本較大;AT 依賴全局事務注解和代理數據源,其余代碼基本不需要改動,對業務無侵入、接入成本極小
-
TCC 的作用范圍在應用層,本質上是實現針對某種業務邏輯的正向和反向方法;AT 模式的作用范圍在於底層數據源,通過保存操作行記錄的前后快照和生成反向 SQL 語句進行補償操作,實現難度較大,優點是對上層應用透明
-
TCC 僅 try 階段加鎖,后續補償邏輯事務間各自獨立;AT 如果一階段分支事務成功則二階段一開始全局鎖即被釋放,否則需要夯住直到分支事務二階段回滾完成才能釋放全局鎖
Seata AT 的使用方法
我們先了解一下如何在應用里使用 AT 模式,流程非常簡單,Seata 也提供了 Seata-Samples [2] 方便大家了解如何使用該項目。
第一步,增加全局事務注解
首先依賴 Seata 的客戶端 SDK,然后在整個分布式事務發起方的業務方法上增加 @GlobalTransactional 注解,下面的例子來源於 Seata-Samples dubbo 案例,purchase 是事務發起方的業務方法,通過 RPC 調用了下游庫存服務和訂單服務提供的接口:
第二步,配置數據源
以 MySQL 為例:
第三步,新建 undo_log 表
在事務鏈涉及的服務的數據庫中新建 undo_log 表用來存儲 UndoLog 信息,用於二階段回滾操作,表中包含xid、branchId、rollback_info 等關鍵字段信息。
Seata AT 的工作流程
工作流程總覽
概括來講,AT 模式的工作流程分為兩階段。一階段進行業務 SQL 執行,並通過 SQL 攔截、SQL 改寫等過程生成修改數據前后的快照(Image),並作為 UndoLog 和業務修改在同一個本地事務中提交。
如果一階段成功那么二階段僅僅異步刪除剛剛插入的 UndoLog;如果二階段失敗則通過 UndoLog 生成反向SQL 語句回滾一階段的數據修改。其中關鍵的 SQL 解析和拼接工作借助了 Druid Parser 中的代碼,這部分本文並不涉及,感興趣的小伙伴可以去翻看源碼,並不是很復雜。
圖解 AT 模式一階段流程
一階段中分支事務的具體工作有:
-
根據需要執行的 SQL(UPDATE、INSERT、DELETE)類型生成相應的 SqlRecognizer
-
進而生成相應的 SqlExecutor
-
接着便進入核心邏輯查詢數據的前后快照,例如圖中標紅的部分,拿到修改數據行的前后快照之后,將二者整合生成 UndoLog,並嘗試將其和業務修改在同一事務中提交。
整個流程的流程圖如下:
值得注意的是,本地事務提交前必須先向服務端注冊分支,分支注冊信息中包含由表名和行主鍵組成的全局鎖,如果分支注冊過程中發現全局鎖正在被其他全局事務鎖定則拋出全局鎖沖突異常,客戶端需要循環等待,直到其他全局事務釋放鎖之后該本地事務才能提交。Seata 以這樣的機制保證全局事務間的寫隔離。
圖解二階段 Commit 流程
對服務端來說,等到一階段完成未拋異常,全局事務的發起方會向服務端申請提交這個全局事務,服務端根據xid 查詢出該全局事務后加鎖並關閉這個全局事務,目的是防止該事務后續還有分支繼續注冊上來,同時將其狀態從 Begin 修改為 Committing。
緊接着,判斷該全局事務下的分支類型是否均為 AT 類型,若是則服務端會進行異步提交,因為 AT 模式下一階段完成數據已經落地。服務端僅僅修改全局事務狀態為 AsyncCommitting,然后會有一個定時線程池去存儲介質(File 或者 Database)中查詢出待提交的全局事務日志進行提交,如果全局事務提交成功則會釋放全局鎖並刪除事務日志。整個流程如下圖所示:
對客戶端來說,先是接收到服務端發送的 branch commit 請求,然后客戶端會根據 resourceId 找到相應的ResourceManager,接着將分支提交請求封裝成 Phase2Context 插入內存隊列ASYNC_COMMIT_BUFFER,客戶端會有一個定時線程池去查詢該隊列進行 UndoLog 的異步刪除。
一旦客戶端提交失敗或者 RPC 超時,則服務端會將該全局事務狀態置位 CommitRetrying,之后會由另一個定時線程池去一直重試這些事務直至成功。整個流程如下圖所示:
圖解二階段 Rollback 流程
回滾相對復雜一些,如果發起方一階段拋異常會向服務端請求回滾該全局事務,服務端會根據 xid 查詢出這個全局事務,加鎖關閉事務使得后續不會再有分支注冊上來,並同時更改其狀態 Begin 為 Rollbacking,接着進行同步回滾以保證數據一致性。除了同步回滾這個點外,其他流程同提交時相似,如果同步回滾成功則釋放全局鎖並刪除事務日志,如果失敗則會進行異步重試。整個流程如下圖所示:
客戶端接收到服務端的 branch rollback 請求,先根據 resourceId 拿到對應的數據源代理,然后根據 xid 和branchId 查詢出 UndoLog 記錄,反序列化其中的 rollback 字段拿到數據的前后快照,我們稱該全局事務為A。
根據具體 SQL 類型生成對應的 UndoExecutor,校驗一下數據 UndoLog 中的前后快照是否一致或者前置快照和當前數據(這里需要 SELECT 一次)是否一致,如果一致說明不需要做回滾操作,如果不一致則生成反向 SQL 進行補償,在提交本地事務前會檢測獲取數據庫本地鎖是否成功,如果失敗則說明存在其他全局事務(假設稱之為 B)的一階段正在修改相同的行,但是由於這些行的主鍵在服務端已經被當前正在執行二階段回滾的全局事務 A 鎖定,因此事務 B 的一階段在本地提交前嘗試獲取全局鎖一定是失敗的,等到獲取全局鎖超時后全局事務 B 會釋放本地鎖,這樣全局事務 A 就可以繼續進行本地事務的提交,成功之后刪除本地UndoLog 記錄。整個流程如下圖所示:
本節小結
我們通過流程圖分析了一下 Seata AT 模式兩階段的工作流程,這里提一句,官方文檔針對 AT 模式的工作流程提供了一個非常易懂的例子 —— AT 模式工作機制 [3]。
筆者強烈建議感興趣的同學閱讀過后,再看下文的源碼分析
Seata AT 模式源碼模塊拆解
通過上面的文字和圖解,相信大家已經了解了 Seata AT 模式的基本工作原理,那么本節開始我們正式進入相關源碼的分析階段。第一步,由於 Seata 模塊不算少,我們先對整個 Seata 項目的模塊進行拆解,挑出其中需要重點關注的模塊,忽略那些次要的。
下文的源碼分析均基於 Seata v0.6.1 版本
相比於之前筆者對 Seata TCC 實現的分析,AT 模式的源碼就要復雜很多了,基本上大多數模塊均有涉及,因此在閱讀源碼之前,我們先對模塊的優先級進行篩選,包括下文會敘述哪些模塊和忽略哪些模塊。
首先,seata-tcc 與 AT 的功能無關可以不用看;seata-common、seata-core、seata-config、seata-discovery 這些只看名字也能知道大致的功能,后續閱讀代碼期間經常會看到其中的類,因此都可以暫時忽略;seata-tm、seata-rm 這兩者都是封裝的與 seata-server 進行通信的方法和步驟,這部分筆者已經在上一篇關於 TCC 的文章中敘述過了,不再贅述;seata-spring 主要是注解、切面織入、方法攔截等功能的實現,關鍵點包括全局事務的開啟,但是由於 AT 和 TCC 在全局事務開啟部分的邏輯是一致的,因此本文也不再贅述。
一通排查下來,和 AT 核心功能有關的模塊僅剩下 seata-rm-datasource 和 seata-server,仔細一想這也很合理,因為 Seata 中分支事務才是真正執行數據修改和補償的部分,因此對於 TCC 模式來說,TwoPhaseBusinessAction 注解的實現類是分支事務,對 AT 模式來說,代理數據源正是分支事務,因此核心邏輯必然在 seata-rm-datasource模塊中,而 TC 集群是協調整個全局事務的指揮者,自然 seata-server模塊也是我們需要特別關注的,但是由於服務端邏輯和 TCC 部分高度相似,除了 v0.6.1 中新增了 DB 模式作為日志存儲介質外,因此下文先選取客戶端 AT 模式相關源碼進行深入分析,最后簡要分析下與 AT 模式相關的服務端源碼。
Seata AT 模式客戶端部分
數據源代理部分 —— 三類 Proxy
下圖來源於 Seata 官方文檔:
Seata 中主要針對 java.sql 包下的 DataSource、Connection、Statement、PreparedStatement 四個接口進行了再包裝,包裝類分別為 DataSourceProxy、ConnectionProxy、StatementProxy、PreparedStatementProxy,很好一一對印,其功能是在 SQL 語句執行前后、事務 commit 或者 rollbakc 前后進行一些與 Seata 分布式事務相關的操作,例如分支注冊、狀態回報、全局鎖查詢、快照存儲、反向 SQL 生成等。
ExecuteTemplate 類的 execute 方法
AT 模式下,真正分支事務開始是在 StatementProxy 和 PreparedStatementProxy的 execute、executeQuery、executeUpdate 等具體執行方法中,這些方法均實現自 Statement 和 PreparedStatement的標准接口,而方法體內調用了 ExecuteTemplate.execute 做方法攔截,下面我們來看看這個方法的實現:
下面我們看看這個 executor.execute 方法的實現。
執行器接口 execute 的實現
execute 方法的實現位於 BaseTransactionalExecutor 類中:
BaseTransactionalExecutor 類中 execute 方法主要做了一些與全局事務相關的狀態值的設定,繼續追蹤進入 doExecute 方法的實現。
抽象方法 doExecute 的實現
終於進入正題,doExecute 方法位於 AbstractDMLBaseExecutor 類中,該類繼承自上文中的BaseTransactionalExecutor。
doExecute 方法體內先拿到具體的連接代理對象 connectionProxy,然后根據 Commit 標識進行不同方法的調用,但翻看代碼實現時發現,其實 executeCommitTrue方法就是先把 Commit 標識改成 false 然后再調用executeCommitFalse 方法。
executeCommitTrue 方法體中有一個無限循環,這么做的意義是,一旦分支注冊時拋出鎖沖突異常,則需要一直等待直到別的全局事務釋放該全局鎖之后才能提交自己的修改,否則一直阻塞等待。
下面我們仔細看一下 executeCommitFalse 方法的邏輯,它是實現 AT 模式的關鍵步驟。其中,beforeImage 是一個抽象方法,針對 INSERT、UPDATE、DELETE 有不同的實現,因為需要將這三種不同的 SQL 解析為相應的 SELECT 語句,查詢操作前數據的快照;同樣的 afterImage 也是一個抽象方法,來查詢操作后數據的快照;statementCallback.execute 語句真正執行 SQL;prepareUndoLog 整合beforeImage 和 afterImage 生成 UndoLog 對象。
executeCommitFalse 執行過后,會調用 connectionProxy.commit() 做事務提交,我們看看該代理方法的實現。
ConnectionProxy 復寫的 commit 方法
該 commit 方法實現自 Connection 接口的 commit 方法:
執行一階段本地事務提交
如果是分支事務,調用 processGlobalTransactionCommit 方法進行提交:
GlobalLock 的具體作用
如果是用 GlobalLock 修飾的本地業務方法,雖然該方法並非某個全局事務下的分支事務,但是它對數據資源的操作也需要先查詢全局鎖,如果存在其他 Seata 全局事務正在修改,則該方法也需等待。所以,如果想要Seata 全局事務執行期間,數據庫不會被其他事務修改,則該方法需要強制添加 GlobalLock 注解,來將其納入 Seata 分布式事務的管理范圍。
功能有點類似於 Spring 的 @Transactional 注解,如果你希望開啟事務,那么必須添加該注解,如果你沒有添加那么事務功能自然不生效,業務可能出 BUG;Seata 也一樣,如果你希望某個不在全局事務下的 SQL 操作不影響 AT 分布式事務,那么必須添加 GlobalLock 注解。
二階段異步刪除分支 UndoLog
如果一階段成功,則 TC 會通知客戶端 RM 進行第二階段的提交工作,這部分代碼最終實現位於AsyncWorker 類中的 branchCommit 方法。
插入 ASYNC_COMMIT_BUFFER 之后,AsyncWorker 類中會有一個定時任務,從隊列中取出分支提交信息Phase2Context,將其中的 xid 和 branchId 提取出來生成 DELETE SQL 語句,刪除本地數據庫中存儲的相應的 UndoLog。下面是該定時任務的關鍵方法 doBranchCommits 的實現:
二階段生成反向 SQL 回滾
如果一階段失敗,則二階段需要回滾一階段的數據庫更新操作,此時涉及到根據 UndoLog構造逆向 SQL 進行補償。這部分邏輯的入口位於 DataSourceManager 類中的 branchRollback 方法:
UndoLogManager 負責 UndoLog 的插入、刪除、補償等操作,其中核心方法即為 undo,我們可以看到其中有一個無限 for 循環,一旦當前事務進行二階段回滾時獲取本地鎖失敗,則進入循環等待邏輯,等待本地鎖被釋放之后自己再提交本地事務:
UndoExecutorFactory 類的 getUndoExecutor 方法會根據 UndoLog 中記錄的 SQLType 生成不同的UndoExecutor 返回:
UndoExecutor 中的 executeOn 方法 首先會調用一個抽象方法 buildUndoSQL,根據 INSERT、UPDATE、DELETE 三種不同的 SQL 類型生成相應的反向 SQL 語句。
下面我們以 DELETE 為例分析一下 MySQLUndoDeleteExecutor 類的 buildUndoSQL方法的實現,如果一階段已經刪除了某行數據,那么二階段補償自然需要構造一個 INSERT語句將被刪除的行重新插入。
Seata AT 模式服務端部分
AT 模式下,全局事務注冊、提交、回滾均和 TCC 模式一模一樣,均是根據一階段調用拋不拋異常決定。
區別在於兩點:
-
分支事務的注冊,TCC 模式下分支事務是在進入參與方 Try 方法之前的切面中注冊的,而且分支實現完畢不需要再次匯報分支狀態;但 AT 模式不一樣,分支事務是在代理數據源提交本地事務之前注冊的,注冊成功才能提交一階段本地事務,如果注冊失敗報鎖沖突則一直阻塞等待直到該全局鎖被釋放,且本地提交之后不論是否成功還需要再次向 TC 匯報一次分支狀態。
-
AT 模式由於一階段已經完成數據修改,因此二階段可以異步提交,但回滾是同步的,回滾失敗才會異步重試;但是 Seata 中 TCC 模式二階段 Confirm 是同步提交的,可以最大程度保證 TCC 模式的數據一致性,但是筆者認為在要求性能的場景下,TCC的二階段也可以改為異步提交
服務端提交全局事務
核心方法是 DefaultCore 類中的 commit 方法:
服務端異步提交分支事務
DefaultCoordinator 類中有一個 asyncCommitting 定時線程池,會定時調用 handleAsyncCommitting 方法從存儲介質(文件或者數據庫)中分批查詢出狀態為 AsyncCommitting 的全局事務列表,針對每個全局事務調用 doGlobalCommit 方法提交其下所有未提交的分支事務。
服務端同步回滾分支事務
一旦一階段失敗,全局事務發起方通知 TC 回滾全局事務的話,那么二階段的回滾調用是同步進行的,一旦同步回滾失敗才會進入異步重試階段。核心方法為 DefaultCore 類中的 doGlobalRollback 方法:
回滾的異步重試與異步提交相同,都是一個定時線程池去掃描存儲介質中尚未完成回滾的全局事務,因此這里不再贅述。
Seata AT 模式的全局鎖
全局鎖的組成和作用
全局鎖主要由表名加操作行的主鍵兩個部分組成,Seata AT 模式使用服務端保存全局鎖的方法保證:
-
全局事務之前的寫隔離
-
全局事務與被 GlobalLock 修飾方法間的寫隔離性
全局鎖的注冊
當客戶端在進行一階段本地事務提交前,會先向服務端注冊分支事務,此時會將修改行的表名、主鍵信息封裝成全局鎖一並發送到服務端進行保存,如果服務端保存時發現已經存在其他全局事務鎖定了這些行主鍵,則拋出全局鎖沖突異常,客戶端循環等待並重試。
全局鎖的查詢
被 @GlobalLock 修飾的方法雖然不在某個全局事務下,但是其在提交事務前也會進行全局鎖查詢,如果發現全局鎖正在被其他全局事務持有,則自身也會循環等待。
全局鎖的釋放
由於二階段提交是異步進行的,當服務端向客戶端發送 branch commit 請求后,客戶端僅僅是將分支提交信息插入內存隊列即返回,服務端只要判斷這個流程沒有異常就會釋放全局鎖。因此,可以說如果一階段成功則在二階段一開始就會釋放全局鎖,不會鎖定到二階段提交流程結束。
但是如果一階段失敗二階段進行回滾,則由於回滾是同步進行的,全局鎖直到二階段回滾完成才會被釋放。
Seata AT 模式潛在優化點
Seata AT 模式的源碼讀下來,其邏輯也存在可以優化的地方:
-
針對簡單 SQL 語句,其后置數據快照可以直接在內存中計算生成,而無需再走一次 SELECT
-
全局鎖可以保存在客戶端本地數據庫中,這樣減少與服務端的 RPC 調用次數
全文總結
本文基於 Seata v0.6.1 通過圖文結合的方式分析其 AT 模式的工作原理和源碼實現,剖析了其中核心的前后快照生成、全局鎖機制、服務端協調機制等邏輯,希望對大家有所啟發