目錄
- 前言
- 1. Seata 基礎知識
- 1.1 Seata 的 AT 模式
- 1.2 Seata AT 模式的作業流程
- 1.3 Seata 服務端的存盤模式
- 1.4 Seata 與 Spring Cloud 整合說明
- 1.5 關于事務分組的說明
- 2. Seata 服務端的安裝
- 2.1 安裝包安裝 Seata
- 2.1.1 下載 Seata
- 2.1.2 修改存盤模式為 db
- 2.1.3 指明注冊中心與配置中心,上傳 Seata 配置
- 2.1.4 啟動 Seata 服務器
- 2.2 原始碼安裝 Seata
- 2.2.1 拉取代碼
- 2.2.2 修改組態檔
- 2.2.3 啟動服務
- 2.1 安裝包安裝 Seata
- 3. Spring Cloud 集成 Seata 實作分布式事務
- 3.1 引入 pom.xml 依賴檔案
- 3.2 修改 bootstrap.yml 組態檔
- 3.3 注入資料源
- 3.4 添加 undo_log 表
- 3.5 使用 @GlobalTransactional 開啟事務
- 4. Seata AT 模式的實作原理
- 4.1 兩個階段
- 4.2 AT 模式第一階段實作原理
- 4.3 AT 模式第二階段實作原理
- 4.3.1 事務提交
- 4.3.2 事務回滾
- 4.4 關于事務的隔離性保證
- 4.4.1 寫隔離
- 4.4.2 讀隔離
- 最后
前言
參考資料:
《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 住的,直到全域鎖拿到,即讀取的相關資料是已提交的,才回傳;
最后

轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/423594.html
標籤:架構設計
