目錄
- 前言
- 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 事務模式,為開發者提供了一站式的分布式事務解決方案;
- Seata 的 AT 模式基于 1 個全局 ID 和 3 個元件模型:
- Transaction ID XID:全局唯一的事務 ID;
- Transaction Coordinator TC:事務協調器,維護全局事務的運作狀态,負責協調并驅動全局事務的送出或復原;
- Transaction Manager TM:控制全局事務的邊界,負責開啟一個全局事務,并最終發起全局送出或全局復原的決議;
- Resource Manager RM:控制分支事務,負責分支注冊、狀态彙報,并接收事務協調器的指令,驅動分支(本地)事務的送出和復原;
- 為友善了解這裡稱 TC 為服務端;
- 使用 AT 模式時有一個前提,RM 必須是支援本地事務的關系型資料庫;
- TM 向 TC 申請開啟一個全局事務,全局事務建立成功并生成一個全局唯一的
;XID
- XID 在微服務調用鍊路的上下文中傳播;
- RM 向 TC 注冊分支事務,将其納入
對應全局事務的管轄;XID
- TM 向 TC 發起針對
的全局送出或復原決議;XID
- TC 排程
下管轄的全部分支事務完成送出或復原請求;XID
- Seata 服務端的存儲模式有三種:file、db 和 redis:
- file:預設,單機模式,全局事務會話資訊持久化在本地檔案
中,性能較高(file 類型不支援注冊中心的動态發現和動态配置功能);${SEATA_HOME}\bin\sessionStore\root.data
- db:需要修改配置,高可用模式,Seata 全局事務會話資訊由全局事務、分支事務、全局鎖構成,對應表:
、globaltable
branchtable
lock_table
- redis:需要修改配置,高可用模式;
- file:預設,單機模式,全局事務會話資訊持久化在本地檔案
- 由于 Spring Cloud 并沒有提供分布式事務處理的标準,是以它不像配置中心那樣插拔式地內建各種主流的解決方案;
- Spring Cloud Alibaba Seata 本質上還是基于 Spring Boot 自動裝配來內建的,在沒有提供标準化配置的情況下隻能根據不同的分布式事務架構進行配置和整合;
- 在 Seata Clien 端的 file.conf 配置中有一個屬性
,它表示事務分組映射,是 Seata 的資源邏輯,類似于服務執行個體,它的主要作用是根據分組來擷取 Seata Serve r的服務執行個體;vgroup_mapping
- 服務分組的工作機制:
- 首先,在應用程式中需要配置事務分組,也就是使用 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 用戶端會根據應用程式的
去指定位置(file.conf 或者遠端配置中心)查找txServiceGroup
對應的配置值,該值代表TC叢集(Seata Server)的名稱;service.vgroup_mapping.${txServiceGroup}
- 最後,程式會根據叢集名稱去配置中心或者 file.conf 中獲得對應的服務清單,也就是
clusterName.grouplist
- 首先,在應用程式中需要配置事務分組,也就是使用 GlobalTransactionScanner 構造方法中的
- 在用戶端擷取伺服器位址并沒有直接采用服務名稱,而是增加了一層事務分組映射到叢集的配置。這樣做的好處在于,事務分組可以作為資源的邏輯隔離機關,當某個叢集出現故障時,可以把故障縮減到服務級别,實作快速故障轉移,隻需要切換對應的分組即可;
Seata 安裝的是 AT 模型中的 TC,為友善了解這裡稱為服務端;
Seata 作為一個事務中間件,有很多種部署安裝方式,有安裝包部署、源碼部署和 Docker 部署,這裡介紹前兩種。版本選 1.4.2;
- 進入 Seata 官網下載下傳 binary 二進制檔案安裝包(也可以在官方 GitHub 倉庫裡下):http://seata.io/zh-cn/blog/download.html;
- 修改存儲模式:
- 修改
檔案,store.mode="db"。如下圖所示:${SEATA_HOME}\conf\file.conf
- 修改
- 修改 MySQL 連接配接資訊:
-
檔案裡的 db 子產品為自己需要連接配接的 MySQL 位址;${SEATA_HOME}\conf\file.conf
-
- 在 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;
- 注冊中心:
-
檔案裡的 registry.type,以及下面的注冊中心位址資訊;${SEATA_HOME}\conf\registry.conf
-
- 配置中心:
- 也是在這個檔案裡,往下翻,如下圖:
- 将 Seata 用戶端和服務端的配置資訊上傳到 Nacos 伺服器:
- Seata 用戶端和服務端的配置資訊儲存在
檔案裡,該檔案隻在源碼包裡有,筆者是源碼安裝 Seata 時做的這步;${SEATA_HOME}/script/config-center/config.txt
- 在
目錄下執行以下${SEATA_HOME}\script\config-center\nacos
腳本即可;nacos-config.sh
- 上傳完後可見下圖:
- Seata 用戶端和服務端的配置資訊儲存在
- 先啟動 Nacos,再執行
${SEATA_HOME}\bin\seata-server.bat
- 啟動成功後能在 Nacos 伺服器裡能看見 Seata 服務;
- 通路位址:https://github.com/seata/seata;
- 派生後拉取代碼;
- 源碼的配置檔案在 seata-server 子產品下的 resource 資源檔案裡,有 file.conf 和 registry.conf 檔案;
- 跟 2.1 安裝包安裝一樣修改即可;
- 先啟動 Nacos 伺服器;
- 執行
将項目安裝到本地;mvm install
- 然後執行 seata-server 子產品的
方法即可;Server.run()
- 同樣,在 Nacos 伺服器裡能看見 Seata 服務;
- 配置示例參考官方提供的:https://github.com/seata/seata-samples/blob/master/doc/quick-integration-with-spring-cloud.md
- 需要給四個服務都引入以下依賴:
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-seata</artifactId>
</dependency>
- Seata 在 1.0 後支援将
目錄下的兩個配置檔案 file.conf 和 registry.conf 寫進 .yml 格式檔案裡了(1.0 版本前不支援);${SEATA_HOME}/script/client/conf
- .yml 格式的配置檔案在
目錄下;${SEATA_HOME}script/client/spring
- 需要修改
和seata.tx-service-group
一緻,配置中心、注冊中心等;seata.service.vgroup-mapping
- 另一種配置方法:
- 除此之外,還可以将 file.conf 和 registry.conf 兩個檔案添加進 resource 目錄下;
- Seata 通過代理資料源的方式實作分支事務;MyBatis 和 JPA 都需要注入
, 不同的是,MyBatis 還需要額外注入io.seata.rm.datasource.DataSourceProxy
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();
}
}
- 在業務相關的資料庫中添加 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
- 在業務的發起方的方法上使用
開啟全局事務,Seata 會将事務的 xid 通過攔截器添加到調用其他服務的請求中,實作分布式事務;@GlobalTransactional
- AT 模式是基于 XA 事務模型演進而來的,是以它的整體機制也是一個改進版的兩階段送出協定;
- 第一階段:業務資料和復原日志記錄在同一個本地事務中送出,釋放本地鎖和連接配接資源;
- 第二階段:送出異步化,非常快速地完成。復原通過第一階段的復原日志進行反向補償;
- 在業務流程中執行庫存扣減操作的資料庫操作時,Seata 會基于資料源代理對原執行的 SQL 進行解析(Seata 在 0.9.0 版本之後支援自動代理);
- 然後将業務資料在更新前後儲存到
日志表中,利用本地事務的 ACID 特性,把業務資料的更新和復原日志寫入同一個本地事務中進行送出;undo_log
- 送出前,向TC注冊分支事務:申請
表中主鍵值等于 1 的記錄的全局鎖;tbl_repo
- 本地事務送出:業務資料的更新和前面步驟中生成的
一并送出;UNDO_LOG
- 将本地事務送出的結果上報給TC;
- 送出前,向TC注冊分支事務:申請
- AT 模式和 XA 最大的不同點:分支的本地事務可以在第一階段送出完成後馬上釋放本地事務鎖定的資源;AT 模式降低了鎖的範圍,進而提升了分布式事務的處理效率;
- TC 接收到所有事務分支的事務狀态彙報之後,決定對全局事務進行送出或者復原;
- 如果決定是全局送出,說明此時所有分支事務已經完成了送出,隻需要清理
日志即可。這也是和 XA 最大的不同點;UNDO_LOG
- 分支事務收到 TC 的送出請求後把請求放入一個異步任務隊列中,并馬上傳回送出成功的結果給 TC;
- 從異步隊列中執行分支,送出請求,批量删除相應
日志;UNDO_LOG
- 整個全局事務鍊中,任何一個事務分支執行失敗,全局事務都會進入事務復原流程;
- 也就是根據
中記錄的資料鏡像進行補償;UNDO_LOG
- 通過 XID 和 branch ID 查找到相應的
記錄;UNDO_LOG
- 資料校驗:拿
中的 afterImage 鏡像資料與目前業務表中的資料進行比較,如果不同,說明資料被目前全局事務之外的動作做了修改,那麼事務将不會復原;UNDO_LOG
- 如果 afterImage 中的資料和目前業務表中對應的資料相同,則根據
中的 beforelmage 鏡像資料和業務 SQL 的相關資訊生成復原語句并執行;UNDO_LOG
- 送出本地事務,并把本地事務的執行結果(即分支事務復原的結果)上報給 TC;
- 通過 XID 和 branch ID 查找到相應的
- 在 AT 模式中,當多個全局事務操作同一張表時,它的事務隔離性保證是基于全局鎖來實作的;
- 一階段本地事務送出前,需要確定先拿到全局鎖;
- 拿不到全局鎖 ,不能送出本地事務。
- 拿全局鎖的嘗試被限制在一定範圍内,超出範圍将放棄,并復原本地事務,釋放本地鎖;
- 舉例:
- tx1 一階段拿到全局鎖,tx2 等待;
- tx1 二階段全局送出,釋放全局鎖,tx2 拿到全局鎖送出本地事務;
- 如果 tx1 的二階段全局復原,則 tx1 需要重新擷取該資料的本地鎖,進行反向補償的更新操作,實作分支的復原;
- 此時,如果 tx2 仍在等待該資料的全局鎖,同時持有本地鎖,則 tx1 的分支復原會失敗;
- 分支的復原會一直重試,直到 tx2 的全局鎖等鎖逾時,放棄全局鎖并復原本地事務釋放本地鎖,tx1 的分支復原最終成功;
- 因為整個過程全局鎖在 tx1 結束前一直是被 tx1 持有的,是以不會發生髒寫的問題;
- 在資料庫本地事務隔離級别讀已送出(Read Committed) 或以上的基礎上,Seata(AT 模式)的預設全局隔離級别是讀未送出(Read Uncommitted) ;
- 在該隔離級别,所有事務都可以看到其他未送出事務的執行結果,産生髒讀。這在最終一緻性事務模型中是允許存在的,并且在大部分分布式事務場景中都可以接受髒讀;
- 如果應用在特定場景下,必需要求全局的讀已送出 ,目前 Seata 的方式是通過
語句的代理;SELECT FOR UPDATE
-
語句的執行會申請全局鎖 ,如果全局鎖被其他事務持有,則釋放本地鎖(復原SELECT FOR UPDATE
語句的本地執行)并重試;SELECT FOR UPDATE
- 這個過程中,查詢是被 block 住的,直到全局鎖拿到,即讀取的相關資料是已送出的,才傳回;
新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公衆号,會分享一些更日常的東西!
如需轉載,請标注出處!