深入瞭解分佈式事務組件 Seata:AT 模式(二)

在前面一篇文章,我們介紹了阿里開源的分佈式事務組件 Seata 的相關概念,重點介紹了 Seata 的 AT 模式。並通過一個 Spring-Cloud-JPA 的案例,演示了 AT 模式的使用入門。本文將會結合 Spring-Cloud-JPA 的案例,深入瞭解 Seata AT 模式的工作流程。本文基於 v0.8.1。

AT 模式與 TCC 模式

Seata AT 模式是基於兩階段提交模式設計的,以高效且對業務零侵入的方式,解決微服務場景下面臨的分佈式事務問題。它使得應用代碼可以像使用本地事務一樣使用分佈式事務,完全屏蔽了底層細節,Seata AT 模式與 Seata MT(TCC) 模式的區別有以下幾點:

  • 使用上,TCC 依賴於用戶自行實現的三個方法成本較大;AT 依賴全局事務註解和代理數據源,其餘代碼基本不需要改動,對業務無侵入、接入成本極小;
  • TCC 的作用範圍在應用層,本質上是實現針對某種業務邏輯的正向和反向方法;AT 模式的作用範圍在於底層數據源,通過保存操作行記錄的前後快照和生成反向 SQL 語句進行補償操作,實現難度較大,優點是對上層應用透明;
  • TCC 僅 try 階段加鎖,後續補償邏輯事務間各自獨立;AT 需要藉助於全局鎖和 GlobalLock 註解來解決不同全局事務間的寫衝突問題,如果一階段分支事務成功則二階段一開始全局鎖即被釋放,否則需要夯住直到分支事務二階段回滾完成才能釋放全局鎖。

AT 模式下,把每個數據庫被當做是一個 Resource,Seata 裡稱為 DataSource Resource。業務通過 JDBC 標準接口訪問數據庫資源時,Seata 框架會對所有請求進行攔截,做一些操作。每個本地事務提交時,Seata RM(Resource Manager,資源管理器) 都會向 TC(Transaction Coordinator,事務協調器) 註冊一個分支事務。當請求鏈路調用完成後,發起方通知 TC 提交或回滾分佈式事務,進入二階段調用流程。此時,TC 會根據之前註冊的分支事務回調到對應參與者去執行對應資源的第二階段。TC 是怎麼找到分支事務與資源的對應關係呢?每個資源都有一個全局唯一的資源 ID,並且在初始化時用該 ID 向 TC 註冊資源。在運行時,每個分支事務的註冊都會帶上其資源 ID。這樣 TC 就能在二階段調用時正確找到對應的資源。

關於 AT 模式用法,這裡就不贅述了,主要包括以下幾點:

  • 增加全局事務註解 @GlobalTransactional:在整個分佈式事務發起方的業務方法上增加;
  • 配置代理數據源:配置 Seata 的代理數據源
  • 新建 undo_log 表:在事務鏈涉及的服務的數據庫中新建 undo_log 表用來存儲 UndoLog 信息,用於二階段回滾操作,表中包含 xid、branchId、rollback_info 等關鍵字段信息。

AT 模式的架構與實現原理介紹

分佈式事務是一個全局事務,由多個分支事務組成,Seata AT 模式具體包括如下兩個階段:

  • 階段 1:分支(本地)事務執行。將一個本地事務做為一個分佈式事務分支,所以若干個分佈在不同微服務中的本地事務共同組成了一個全局事務,結構如下。
深入瞭解分佈式事務組件 Seata:AT 模式(二)
  • 階段 2:分支事務提交或回滾。階段 2 完成的是全局事物的最終提交或回滾,當全局事務中所有分支事務全部完成並且都執行成功,這時TM會發起全局事務提交,TC收到全全局事務提交消息後,會通知各分支事務進行提交;同理,當全局事務中所有分支事務全部完成並且某個分支事務失敗了,TM會通知TC協調全局事務回滾,進而TC通知各分支事務進行回滾。

在業務應用啟動過程中,由於引入了 Seata 客戶端,RmRpcClient會隨應用一起啟動,該RmRpcClient採用Netty實現,可以接收TC消息和向TC發送消息,因此RmRpcClient是與TC收發消息的關鍵模塊。

Seata 實現分佈式事務的一般過程如下:

深入瞭解分佈式事務組件 Seata:AT 模式(二)
  • TM 通知 TC 開始一個新的全局事務。TC 生成了一個代表全局事務的 XID。
  • XID 通過微服務的調用鏈傳播下去。
  • RM將本地事務註冊為XID到TC的相應全局事務的分支。
  • TM 通知 TC 提交或者回滾 XID 對應的全局事務
  • TC 驅動 XID 的對應全局事務下的所有分支事務以完成分支提交或回滾。

一階段流程

在一階段,Seata 會攔截業務 SQL,首先解析 SQL 語義,找到業務 SQL要更新的業務數據,在業務數據被更新前,將其保存成 before image,然後執行業務 SQL 更新業務數據,在業務數據更新之後,再將其保存成 after image,最後生成行鎖。以上操作全部在一個數據庫事務內完成,這樣保證了一階段操作的原子性。

一階段中分支事務的具體工作有:

深入瞭解分佈式事務組件 Seata:AT 模式(二)
  • 根據需要執行的 SQL(UPDATE、INSERT、DELETE)類型生成相應的 SqlRecognizer
  • 生成相應的 SqlExecutor
  • 進入核心邏輯查詢數據的前後快照,例如圖中標紅的部分,拿到修改數據行的前後快照之後,將二者整合生成 UndoLog,並嘗試將其和業務修改在同一事務中提交。

實現上,Seata 對數據源做了封裝代理,然後對於數據源的操作處理,就由 Seata 內部邏輯完成了。如我們之前例子中的數據源加載配置:

@Configuration

public class DataSourceConfig {

@Bean

@ConfigurationProperties(prefix = "spring.datasource")

public DruidDataSource druidDataSource() {

return new DruidDataSource();

}

@Primary

@Bean("dataSource")

public DataSource dataSource(DruidDataSource druidDataSource) {

return new DataSourceProxy(druidDataSource);

}

}

可以看到,我們使用的是 Seata 封裝的代理數據源 DataSourceProxy。 DataSourceProxy 初始化時,會進行 Resouce 註冊:

private void init(DataSource dataSource, String resourceGroupId) {

this.resourceGroupId = resourceGroupId;

Connection connection = dataSource.getConnection()

jdbcUrl = connection.getMetaData().getURL();

dbType = JdbcUtils.getDbType(jdbcUrl, null);

DefaultResourceManager.get().registerResource(this);

}

其實,數據源代理部分有三類 Proxy,Seata 除了對數據庫的 DataSource 進行了封裝,同樣也對 Connection,Statement 進行了封裝代理,分別為 ConnectionProxy 和 StatementProxy。

深入瞭解分佈式事務組件 Seata:AT 模式(二)

二階段 Commit 流程

AT 模式的第二階段會根據第一階段的情況決定是進行全局提交還是全局回滾操作。對服務端來說,等到一階段完成未拋異常,全局事務的發起方會向服務端申請提交這個全局事務,服務端根據 xid 查詢出該全局事務後加鎖並關閉這個全局事務,目的是防止該事務後續還有分支繼續註冊上來,同時將其狀態從 Begin 修改為 Committing。

緊接著,判斷該全局事務下的分支類型是否均為 AT 類型,若是則服務端會進行異步提交,因為 AT 模式下一階段完成數據已經落地。服務端僅僅修改全局事務狀態為 AsyncCommitting,然後會有一個定時線程池去存儲介質(File 或者 Database)中查詢出待提交的全局事務日誌進行提交,如果全局事務提交成功則會釋放全局鎖並刪除事務日誌。整個流程如下圖所示:

深入瞭解分佈式事務組件 Seata:AT 模式(二)

圖片來自https://chenjiayang.me

如果所有 Branch RM 都執行成功了,那麼就進行全局 Commit。因為此時我們不用回滾,而每個 Branch 本地數據庫操作已經完成了,那麼我們其實主要做的事情就是把本地的 Undolog 刪了即可。

對客戶端來說,先是接收到服務端發送的 branch commit 請求,然後客戶端會根據 resourceId 找到相應的 ResourceManager,接著將分支提交請求封裝成 Phase2Context 插入內存隊列 ASYNC_COMMIT_BUFFER,客戶端會有一個定時線程池去查詢該隊列進行 UndoLog 的異步刪除。

一旦客戶端提交失敗或者 RPC 超時,則服務端會將該全局事務狀態置位 CommitRetrying,之後會由另一個定時線程池去一直重試這些事務直至成功。

二階段 Rollback 流程

二階段如果是回滾的話,Seata 就需要回滾一階段已經執行的業務 SQL,還原業務數據。回滾方式便是用 before image 還原業務數據;但在還原前要首先要校驗髒寫,對比數據庫當前業務數據和 after image,如果兩份數據完全一致就說明沒有髒寫,可以還原業務數據,如果不一致就說明有髒寫,出現髒寫就需要轉人工處理。

深入瞭解分佈式事務組件 Seata:AT 模式(二)

回滾相對複雜一些,如果發起方一階段拋異常會向服務端請求回滾該全局事務,服務端會根據 xid 查詢出這個全局事務,加鎖關閉事務使得後續不會再有分支註冊上來,並同時更改其狀態 Begin 為 Rollbacking,接著進行同步回滾以保證數據一致性。除了同步回滾這個點外,其他流程同提交時相似,如果同步回滾成功則釋放全局鎖並刪除事務日誌,如果失敗則會進行異步重試。

客戶端接收到服務端的 branch rollback 請求,先根據 resourceId 拿到對應的數據源代理,然後根據 xid 和 branchId 查詢出 UndoLog 記錄,反序列化其中的 rollback 字段拿到數據的前後快照,我們稱該全局事務為 A。

根據具體 SQL 類型生成對應的 UndoExecutor,校驗一下數據 UndoLog 中的前後快照是否一致或者前置快照和當前數據(這裡需要 SELECT 一次)是否一致,如果一致說明不需要做回滾操作,如果不一致則生成反向 SQL 進行補償,在提交本地事務前會檢測獲取數據庫本地鎖是否成功,如果失敗則說明存在其他全局事務(假設稱之為 B)的一階段正在修改相同的行,但是由於這些行的主鍵在服務端已經被當前正在執行二階段回滾的全局事務 A 鎖定,因此事務 B 的一階段在本地提交前嘗試獲取全局鎖一定是失敗的,等到獲取全局鎖超時後全局事務 B 會釋放本地鎖,這樣全局事務 A 就可以繼續進行本地事務的提交,成功之後刪除本地 UndoLog 記錄。

小結

本文主要介紹了 AT 模式下, Seata 的客戶端和服務端的工作流程。AT 模式的一階段、二階段提交和回滾均由 Seata 框架自動生成,用戶只需編寫業務 SQL,便能輕鬆接入分佈式事務,AT 模式是一種對業務無任何侵入的分佈式事務解決方案。下篇文章開始將會結合源碼具體講解 AT 模式的實現。

微服務合集

參考

  1. [圖文] Seata AT 模式分佈式事務源碼分析
  2. Seata.io


分享到:


相關文章: