前言
參考資料:
《Spring Microservices in Action》
《Spring Cloud Alibaba 微服務原理與實戰》
《B站 尚矽谷 SpringCloud 框架開發教程 周陽》
《Seata 中文官網》
《Seata GitHub 官網》
《Seata 官方示例》
Seata 是一款開源的分散式事務解決方案,致力於在微服務架構下提供高效能和簡單易用的分散式事務服務;它提供了 AT、TCC、Saga 和 XA 事務模式,為開發者提供了一站式的分散式事務解決方案;
1. Seata 基礎知識
1.1 Seata 的 AT 模式
- Seata 的 AT 模式基於 1 個全域性 ID 和 3 個元件模型:
- Transaction ID XID:全域性唯一的事務 ID;
- Transaction Coordinator TC:事務協調器,維護全域性事務的執行狀態,負責協調並驅動全域性事務的提交或回滾;
- Transaction Manager TM:控制全域性事務的邊界,負責開啟一個全域性事務,並最終發起全域性提交或全域性回滾的決議;
- Resource Manager RM:控制分支事務,負責分支註冊、狀態彙報,並接收事務協調器的指令,驅動分支(本地)事務的提交和回滾;
- 為方便理解這裡稱 TC 為服務端;
- 使用 AT 模式時有一個前提,RM 必須是支援本地事務的關係型資料庫;
1.2 Seata AT 模式的工作流程
- TM 向 TC 申請開啟一個全域性事務,全域性事務建立成功並生成一個全域性唯一的
XID
; - XID 在微服務呼叫鏈路的上下文中傳播;
- RM 向 TC 註冊分支事務,將其納入
XID
對應全域性事務的管轄; - TM 向 TC 發起針對
XID
的全域性提交或回滾決議; - TC 排程
XID
下管轄的全部分支事務完成提交或回滾請求;
1.3 Seata 服務端的儲存模式
- Seata 服務端的儲存模式有三種:file、db 和 redis:
- file:預設,單機模式,全域性事務會話資訊持久化在本地檔案
${SEATA_HOME}\bin\sessionStore\root.data
中,效能較高(file 型別不支援註冊中心的動態發現和動態配置功能); - db:需要修改配置,高可用模式,Seata 全域性事務會話資訊由全域性事務、分支事務、全域性鎖構成,對應表:
globaltable
、branchtable
、lock_table
; - redis:需要修改配置,高可用模式;
- file:預設,單機模式,全域性事務會話資訊持久化在本地檔案
1.4 Seata 與 Spring Cloud 整合說明
- 由於 Spring Cloud 並沒有提供分散式事務處理的標準,所以它不像配置中心那樣插拔式地整合各種主流的解決方案;
- Spring Cloud Alibaba Seata 本質上還是基於 Spring Boot 自動裝配來整合的,在沒有提供標準化配置的情況下只能根據不同的分散式事務框架進行配置和整合;
1.5 關於事務分組的說明
- 在 Seata Clien 端的 file.conf 配置中有一個屬性
vgroup_mapping
,它表示事務分組對映,是 Seata 的資源邏輯,類似於服務例項,它的主要作用是根據分組來獲取 Seata Serve r的服務例項; - 服務分組的工作機制:
- 首先,在應用程式中需要配置事務分組,也就是使用 GlobalTransactionScanner 構造方法中的
txServiceGroup
引數,這個引數有如下幾種賦值方式:- 預設情況下,為
${spring.application.name}-seata-service-group
; - 在 Spring Cloud Alibaba Seata 中,可以使用
spring cloudalibaba.seata.tx-service-group
賦值; - 在 Seata-Spring-Boot-Starter 中,可以使用
seata.tx-service-group
賦值;
- 預設情況下,為
- 然後,Seata 客戶端會根據應用程式的
txServiceGroup
去指定位置(file.conf 或者遠端配置中心)查詢service.vgroup_mapping.${txServiceGroup}
對應的配置值,該值代表TC叢集(Seata Server)的名稱; - 最後,程式會根據叢集名稱去配置中心或者 file.conf 中獲得對應的服務列表,也就是
clusterName.grouplist
;
- 首先,在應用程式中需要配置事務分組,也就是使用 GlobalTransactionScanner 構造方法中的
- 在客戶端獲取伺服器地址並沒有直接採用服務名稱,而是增加了一層事務分組對映到叢集的配置。這樣做的好處在於,事務分組可以作為資源的邏輯隔離單位,當某個叢集出現故障時,可以把故障縮減到服務級別,實現快速故障轉移,只需要切換對應的分組即可;
2. Seata 服務端的安裝
Seata 安裝的是 AT 模型中的 TC,為方便理解這裡稱為服務端;
Seata 作為一個事務中介軟體,有很多種部署安裝方式,有安裝包部署、原始碼部署和 Docker 部署,這裡介紹前兩種。版本選 1.4.2;
2.1 安裝包安裝 Seata
2.1.1 下載 Seata
- 進入 Seata 官網下載 binary 二進位制檔案安裝包(也可以在官方 GitHub 倉庫裡下):http://seata.io/zh-cn/blog/download.html;
2.1.2 修改儲存模式為 db
- 修改儲存模式:
- 修改
${SEATA_HOME}\conf\file.conf
檔案,store.mode="db"。如下圖所示:
- 修改
- 修改 MySQL 連線資訊:
- 修改
${SEATA_HOME}\conf\file.conf
檔案裡的 db 模組為自己需要連線的 MySQL 地址;
- 修改
- 在 MySQL 上新建資料庫和表;
- SQL 建表語句如下:
- 該 SQL 檔案在原始碼包裡的
${SEATA_HOME}\script/server/db/mysql.sql
檔案;
-- 判斷資料庫存在,存在再刪除
DROP DATABASE IF EXISTS seata;
-- 建立資料庫,判斷不存在,再建立
CREATE DATABASE IF NOT EXISTS seata;
-- 使用資料庫
USE seata;
-- the table to store GlobalSession data
CREATE TABLE IF NOT EXISTS `global_table`
(
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`status` TINYINT NOT NULL,
`application_id` VARCHAR(32),
`transaction_service_group` VARCHAR(32),
`transaction_name` VARCHAR(128),
`timeout` INT,
`begin_time` BIGINT,
`application_data` VARCHAR(2000),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`xid`),
KEY `idx_gmt_modified_status` (`gmt_modified`, `status`),
KEY `idx_transaction_id` (`transaction_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store BranchSession data
CREATE TABLE IF NOT EXISTS `branch_table`
(
`branch_id` BIGINT NOT NULL,
`xid` VARCHAR(128) NOT NULL,
`transaction_id` BIGINT,
`resource_group_id` VARCHAR(32),
`resource_id` VARCHAR(256),
`branch_type` VARCHAR(8),
`status` TINYINT,
`client_id` VARCHAR(64),
`application_data` VARCHAR(2000),
`gmt_create` DATETIME(6),
`gmt_modified` DATETIME(6),
PRIMARY KEY (`branch_id`),
KEY `idx_xid` (`xid`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
-- the table to store lock data
CREATE TABLE IF NOT EXISTS `lock_table`
(
`row_key` VARCHAR(128) NOT NULL,
`xid` VARCHAR(128),
`transaction_id` BIGINT,
`branch_id` BIGINT NOT NULL,
`resource_id` VARCHAR(256),
`table_name` VARCHAR(32),
`pk` VARCHAR(36),
`gmt_create` DATETIME,
`gmt_modified` DATETIME,
PRIMARY KEY (`row_key`),
KEY `idx_branch_id` (`branch_id`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8;
2.1.3 指明註冊中心與配置中心,上傳 Seata 配置
- 註冊中心:
- 修改
${SEATA_HOME}\conf\registry.conf
檔案裡的 registry.type,以及下面的註冊中心地址資訊;
- 修改
- 配置中心:
- 也是在這個檔案裡,往下翻,如下圖:
- 將 Seata 客戶端和服務端的配置資訊上傳到 Nacos 伺服器:
- Seata 客戶端和服務端的配置資訊儲存在
${SEATA_HOME}/script/config-center/config.txt
檔案裡,該檔案只在原始碼包裡有,筆者是原始碼安裝 Seata 時做的這步; - 在
${SEATA_HOME}\script\config-center\nacos
目錄下執行以下nacos-config.sh
指令碼即可; - 上傳完後可見下圖:
- Seata 客戶端和服務端的配置資訊儲存在
- 也是在這個檔案裡,往下翻,如下圖:
2.1.4 啟動 Seata 伺服器
-
先啟動 Nacos,再執行
${SEATA_HOME}\bin\seata-server.bat
檔案; -
啟動成功後能在 Nacos 伺服器裡能看見 Seata 服務;
2.2 原始碼安裝 Seata
2.2.1 拉取程式碼
- 訪問地址:https://github.com/seata/seata;
- 派生後拉取程式碼;
2.2.2 修改配置檔案
- 原始碼的配置檔案在 seata-server 模組下的 resource 資原始檔裡,有 file.conf 和 registry.conf 檔案;
- 跟 2.1 安裝包安裝一樣修改即可;
2.2.3 啟動服務
- 先啟動 Nacos 伺服器;
- 執行
mvm install
將專案安裝到本地; - 然後執行 seata-server 模組的
Server.run()
方法即可;
- 同樣,在 Nacos 伺服器裡能看見 Seata 服務;
3. Spring Cloud 整合 Seata 實現分散式事務
- 配置示例參考官方提供的:https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md
3.1 引入 pom.xml 依賴檔案
- 需要給四個服務都引入以下依賴:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
3.2 修改 bootstrap.yml 配置檔案
-
Seata 在 1.0 後支援將
${SEATA_HOME}/script/client/conf
目錄下的兩個配置檔案 file.conf 和 registry.conf 寫進 .yml 格式檔案裡了(1.0 版本前不支援); -
.yml 格式的配置檔案在
${SEATA_HOME}script/client/spring
目錄下; -
需要修改
seata.tx-service-group
和seata.service.vgroup-mapping
一致,配置中心、註冊中心等; -
另一種配置方法:
- 除此之外,還可以將 file.conf 和 registry.conf 兩個檔案新增進 resource 目錄下;
3.3 注入資料來源
-
Seata 通過代理資料來源的方式實現分支事務;MyBatis 和 JPA 都需要注入
io.seata.rm.datasource.DataSourceProxy
, 不同的是,MyBatis 還需要額外注入org.apache.ibatis.session.SqlSessionFactory
; -
MyBatis:
@Configuration
public class DataSourceProxyConfig {
@Bean
@ConfigurationProperties(prefix = "spring.datasource")
public DataSource dataSource() {
return new DruidDataSource();
}
@Bean
public DataSourceProxy dataSourceProxy(DataSource dataSource) {
return new DataSourceProxy(dataSource);
}
@Bean
public SqlSessionFactory sqlSessionFactoryBean(DataSourceProxy dataSourceProxy) throws Exception {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSourceProxy);
return sqlSessionFactoryBean.getObject();
}
}
3.4 新增 undo_log 表
- 在業務相關的資料庫中新增 undo_log 表,用於儲存需要回滾的資料;
CREATE TABLE `undo_log`
(
`id` BIGINT(20) NOT NULL AUTO_INCREMENT,
`branch_id` BIGINT(20) NOT NULL,
`xid` VARCHAR(100) NOT NULL,
`context` VARCHAR(128) NOT NULL,
`rollback_info` LONGBLOB NOT NULL,
`log_status` INT(11) NOT NULL,
`log_created` DATETIME NOT NULL,
`log_modified` DATETIME NOT NULL,
`ext` VARCHAR(100) DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
) ENGINE = InnoDB
AUTO_INCREMENT = 1
DEFAULT CHARSET = utf8
3.5 使用 @GlobalTransactional 開啟事務
- 在業務的發起方的方法上使用
@GlobalTransactional
開啟全域性事務,Seata 會將事務的 xid 通過攔截器新增到呼叫其他服務的請求中,實現分散式事務;
4. Seata AT 模式的實現原理
4.1 兩個階段
- AT 模式是基於 XA 事務模型演進而來的,所以它的整體機制也是一個改進版的兩階段提交協議;
- 第一階段:業務資料和回滾日誌記錄在同一個本地事務中提交,釋放本地鎖和連線資源;
- 第二階段:提交非同步化,非常快速地完成。回滾通過第一階段的回滾日誌進行反向補償;
4.2 AT 模式第一階段實現原理
-
在業務流程中執行庫存扣減操作的資料庫操作時,Seata 會基於資料來源代理對原執行的 SQL 進行解析(Seata 在 0.9.0 版本之後支援自動代理);
-
然後將業務資料在更新前後儲存到
undo_log
日誌表中,利用本地事務的 ACID 特性,把業務資料的更新和回滾日誌寫入同一個本地事務中進行提交;
- 提交前,向TC註冊分支事務:申請
tbl_repo
表中主鍵值等於 1 的記錄的全域性鎖; - 本地事務提交:業務資料的更新和前面步驟中生成的
UNDO_LOG
一併提交; - 將本地事務提交的結果上報給TC;
- 提交前,向TC註冊分支事務:申請
-
AT 模式和 XA 最大的不同點:分支的本地事務可以在第一階段提交完成後馬上釋放本地事務鎖定的資源;AT 模式降低了鎖的範圍,從而提升了分散式事務的處理效率;
4.3 AT 模式第二階段實現原理
- TC 接收到所有事務分支的事務狀態彙報之後,決定對全域性事務進行提交或者回滾;
4.3.1 事務提交
- 如果決定是全域性提交,說明此時所有分支事務已經完成了提交,只需要清理
UNDO_LOG
日誌即可。這也是和 XA 最大的不同點;
- 分支事務收到 TC 的提交請求後把請求放入一個非同步任務佇列中,並馬上返回提交成功的結果給 TC;
- 從非同步佇列中執行分支,提交請求,批量刪除相應
UNDO_LOG
日誌;
4.3.2 事務回滾
- 整個全域性事務鏈中,任何一個事務分支執行失敗,全域性事務都會進入事務回滾流程;
- 也就是根據
UNDO_LOG
中記錄的資料映象進行補償;
- 通過 XID 和 branch ID 查詢到相應的
UNDO_LOG
記錄; - 資料校驗:拿
UNDO_LOG
中的 afterImage 映象資料與當前業務表中的資料進行比較,如果不同,說明資料被當前全域性事務之外的動作做了修改,那麼事務將不會回滾; - 如果 afterImage 中的資料和當前業務表中對應的資料相同,則根據
UNDO_LOG
中的 beforelmage 映象資料和業務 SQL 的相關資訊生成回滾語句並執行; - 提交本地事務,並把本地事務的執行結果(即分支事務回滾的結果)上報給 TC;
- 通過 XID 和 branch ID 查詢到相應的
4.4 關於事務的隔離性保證
- 在 AT 模式中,當多個全域性事務操作同一張表時,它的事務隔離性保證是基於全域性鎖來實現的;
4.4.1 寫隔離
-
一階段本地事務提交前,需要確保先拿到全域性鎖;
-
拿不到全域性鎖 ,不能提交本地事務。
-
拿全域性鎖的嘗試被限制在一定範圍內,超出範圍將放棄,並回滾本地事務,釋放本地鎖;
-
舉例:
- tx1 一階段拿到全域性鎖,tx2 等待;
- tx1 二階段全域性提交,釋放全域性鎖,tx2 拿到全域性鎖提交本地事務;
- 如果 tx1 的二階段全域性回滾,則 tx1 需要重新獲取該資料的本地鎖,進行反向補償的更新操作,實現分支的回滾;
- 此時,如果 tx2 仍在等待該資料的全域性鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗;
- 分支的回滾會一直重試,直到 tx2 的全域性鎖等鎖超時,放棄全域性鎖並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功;
- tx1 一階段拿到全域性鎖,tx2 等待;
-
因為整個過程全域性鎖在 tx1 結束前一直是被 tx1 持有的,所以不會發生髒寫的問題;
4.4.2 讀隔離
- 在資料庫本地事務隔離級別讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的預設全域性隔離級別是讀未提交(Read Uncommitted) ;
- 在該隔離級別,所有事務都可以看到其他未提交事務的執行結果,產生髒讀。這在最終一致性事務模型中是允許存在的,並且在大部分分散式事務場景中都可以接受髒讀;
- 如果應用在特定場景下,必需要求全域性的讀已提交 ,目前 Seata 的方式是通過
SELECT FOR UPDATE
語句的代理;
SELECT FOR UPDATE
語句的執行會申請全域性鎖 ,如果全域性鎖被其他事務持有,則釋放本地鎖(回滾SELECT FOR UPDATE
語句的本地執行)並重試;- 這個過程中,查詢是被 block 住的,直到全域性鎖拿到,即讀取的相關資料是已提交的,才返回;