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

多氯環己烷發表於2022-02-09


前言

參考資料
《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 住的,直到全域性鎖拿到,即讀取的相關資料是已提交的,才返回;


最後

新人制作,如有錯誤,歡迎指出,感激不盡!
歡迎關注公眾號,會分享一些更日常的東西!
如需轉載,請標註出處!
微服務架構 | 11.1 整合 Seata AT 模式實現分散式事務

相關文章