微服務架構 | 11.1 整合 Seata AT 模式實現分佈式事務


前言

參考資料
《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 模式的工作流程

  • TMTC 申請開啟一個全局事務,全局事務創建成功並生成一個全局唯一的 XID
  • XID 在微服務調用鏈路的上下文中傳播;
  • RMTC 註冊分支事務,將其納入 XID 對應全局事務的管轄;
  • TMTC 發起針對 XID 的全局提交或回滾決議;
  • TC 調度 XID 下管轄的全部分支事務完成提交或回滾請求;

Seata AT 模式的工作流程

1.3 Seata 服務端的存儲模式

  • Seata 服務端的存儲模式有三種:file、db 和 redis:
    • file:默認,單機模式,全局事務會話信息持久化在本地文件 ${SEATA_HOME}\bin\sessionStore\root.data 中,性能較高(file 類型不支持註冊中心的動態發現和動態配置功能);
    • db:需要修改配置,高可用模式,Seata 全局事務會話信息由全局事務、分支事務、全局鎖構成,對應表:globaltablebranchtablelock_table
    • redis:需要修改配置,高可用模式;

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
  • 在客戶端獲取服務器地址並沒有直接採用服務名稱,而是增加了一層事務分組映射到集群的配置。這樣做的好處在於,事務分組可以作為資源的邏輯隔離單位,當某個集群出現故障時,可以把故障縮減到服務級別,實現快速故障轉移,只需要切換對應的分組即可;

事務分組的實現原理

2. Seata 服務端的安裝

Seata 安裝的是 AT 模型中的 TC,為方便理解這裡稱為服務端;
Seata 作為一個事務中間件,有很多種部署安裝方式,有安裝包部署、源碼部署和 Docker 部署,這裡介紹前兩種。版本選 1.4.2;

2.1 安裝包安裝 Seata

2.1.1 下載 Seata

下載 Seata

2.1.2 修改存儲模式為 db

  • 修改存儲模式:
    • 修改 ${SEATA_HOME}\conf\file.conf 文件,store.mode=”db”。如下圖所示:
      修改存儲模式
  • 修改 MySQL 連接信息:
    • 修改 ${SEATA_HOME}\conf\file.conf 文件里的 db 模塊為自己需要連接的 MySQL 地址;
      修改 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 配置上傳進 Nacos 配置中心

2.1.4 啟動 Seata 服務器

  • 先啟動 Nacos,再執行 ${SEATA_HOME}\bin\seata-server.bat 文件;

  • 啟動成功後能在 Nacos 服務器里能看見 Seata 服務;

在 Nacos 服務器里能看見 Seata 服務

2.2 源碼安裝 Seata

2.2.1 拉取代碼

Seata GitHub

2.2.2 修改配置文件

  • 源碼的配置文件在 seata-server 模塊下的 resource 資源文件里,有 file.conf 和 registry.conf 文件;
  • 跟 2.1 安裝包安裝一樣修改即可;

2.2.3 啟動服務

  • 先啟動 Nacos 服務器;
  • 執行 mvm install 將項目安裝到本地;
  • 然後執行 seata-server 模塊的 Server.run() 方法即可;

Seata 源碼啟動成功

  • 同樣,在 Nacos 服務器里能看見 Seata 服務;

在 Nacos 服務器里能看見 Seata 服務

3. Spring Cloud 集成 Seata 實現分佈式事務

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-groupseata.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 特性,把業務數據的更新和回滾日誌寫入同一個本地事務中進行提交;
    AT 模式第一階段執行流程

    • 提交前,向TC註冊分支事務:申請 tbl_repo 表中主鍵值等於 1 的記錄的全局鎖;
    • 本地事務提交:業務數據的更新和前面步驟中生成的 UNDO_LOG 一併提交;
    • 將本地事務提交的結果上報給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;

4.4 關於事務的隔離性保證

  • 在 AT 模式中,當多個全局事務操作同一張表時,它的事務隔離性保證是基於全局鎖來實現的;

4.4.1 寫隔離

  • 一階段本地事務提交前,需要確保先拿到全局鎖

  • 拿不到全局鎖 ,不能提交本地事務。

  • 全局鎖的嘗試被限制在一定範圍內,超出範圍將放棄,並回滾本地事務,釋放本地鎖;

  • 舉例:

    • tx1 一階段拿到全局鎖,tx2 等待;
      tx1 拿到全局鎖,tx2 等待
    • tx1 二階段全局提交,釋放全局鎖,tx2 拿到全局鎖提交本地事務;
      tx1 二階段全局提交,釋放全局鎖
    • 如果 tx1 的二階段全局回滾,則 tx1 需要重新獲取該數據的本地鎖,進行反向補償的更新操作,實現分支的回滾;
      • 此時,如果 tx2 仍在等待該數據的全局鎖,同時持有本地鎖,則 tx1 的分支回滾會失敗;
      • 分支的回滾會一直重試,直到 tx2 的全局鎖等鎖超時,放棄全局鎖並回滾本地事務釋放本地鎖,tx1 的分支回滾最終成功;
  • 因為整個過程全局鎖在 tx1 結束前一直是被 tx1 持有的,所以不會發生臟寫的問題;

4.4.2 讀隔離

  • 在數據庫本地事務隔離級別讀已提交(Read Committed) 或以上的基礎上,Seata(AT 模式)的默認全局隔離級別是讀未提交(Read Uncommitted) ;
    • 在該隔離級別,所有事務都可以看到其他未提交事務的執行結果,產生臟讀。這在最終一致性事務模型中是允許存在的,並且在大部分分佈式事務場景中都可以接受臟讀
    • 如果應用在特定場景下,必需要求全局的讀已提交 ,目前 Seata 的方式是通過 SELECT FOR UPDATE 語句的代理;
      讀已提交執行流程
    • SELECT FOR UPDATE 語句的執行會申請全局鎖 ,如果全局鎖被其他事務持有,則釋放本地鎖(回滾 SELECT FOR UPDATE 語句的本地執行)並重試;
    • 這個過程中,查詢是被 block 住的,直到全局鎖拿到,即讀取的相關數據是已提交的,才返回;


最後

新人製作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!