二,分布式事务

2.1 Seata介绍

解决分布式事务的方案有很多,但实现起来都比较复杂,因此我们一般会使用开源的框架来解决分布式事务问题。在众多的开源分布式事务框架中,功能最完善、使用最多的就是阿里巴巴在 2019 年开源的 Seata 了。Seata 是什么? | Apache Seata

其实分布式事务产生的一个重要原因,就是参与事务的多个分支事务互相无感知,不知道彼此的执行状态。因此解决分布式事务的思想非常简单:

  • 就是找一个统一的事务协调者,与多个分支事务通信,检测每个分支事务的执行状态,保证全局事务下的每一个分支事务同时成功或失败即可。大多数的分布式事务框架都是基于这个理论来实现的。

Seata 也不例外,在 Seata 的事务管理中有三个重要的角色:

  • TC (Transaction Coordinator) 事务协调者:** 维护全局和分支事务的状态,协调全局事务提交或回滚。
  • TM (Transaction Manager) - 事务管理器: 定义全局事务的范围、开始全局事务、提交或回滚全局事务。
  • RM (Resource Manager) 资源管理器: 管理分支事务,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚。

Seata 的工作架构如图所示:

2953321-20250425150338677-504135174.png

其中,TMRM可以理解为 Seata 的客户端部分,引入到参与事务的微服务依赖中即可。将来TMRM就会协助微服务,实现本地分支事务与TC之间交互,实现事务的提交或回滚。

TC服务则是事务协调中心,是一个独立的微服务,需要单独部署。

2.2 部署TC服务

  1. 准备数据库表

    CREATE DATABASE IF NOT EXISTS `seata`;
    USE `seata`;
    
    
    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_status_gmt_modified` (`status` , `gmt_modified`),
        KEY `idx_transaction_id` (`transaction_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    
    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 = utf8mb4;
    
    
    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),
        `status`         TINYINT      NOT NULL DEFAULT '0' COMMENT '0:locked ,1:rollbacking',
        `gmt_create`     DATETIME,
        `gmt_modified`   DATETIME,
        PRIMARY KEY (`row_key`),
        KEY `idx_status` (`status`),
        KEY `idx_branch_id` (`branch_id`),
        KEY `idx_xid_and_branch_id` (`xid` , `branch_id`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    CREATE TABLE IF NOT EXISTS `distributed_lock`
    (
        `lock_key`       CHAR(20) NOT NULL,
        `lock_value`     VARCHAR(20) NOT NULL,
        `expire`         BIGINT,
        primary key (`lock_key`)
    ) ENGINE = InnoDB
      DEFAULT CHARSET = utf8mb4;
    
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('AsyncCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryCommitting', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('RetryRollbacking', ' ', 0);
    INSERT INTO `distributed_lock` (lock_key, lock_value, expire) VALUES ('TxTimeoutCheck', ' ', 0);
    
  2. 准备 seta 配置文件

  3. Docker 部署

    docker run --name seata \
    -p 8099:8099 \
    -p 7099:7099 \
    -e SEATA_IP=192.168.150.101 \
    -v ./seata:/seata-server/resources \
    --privileged=true \
    --network hm-net \
    -d \
    seataio/seata-server:1.5.2
    

2.3 微服务集成Seata

参与分布式事务的每一个微服务都需要集成 Seata,我们以trade-service为例。

  1. 引入依赖

    为了方便各个微服务集成 seata,我们需要把 seata 配置共享到 nacos,因此trade-service模块不仅仅要引入 seata 依赖,还要引入 nacos 依赖:

    <!--统一配置管理-->
      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
      </dependency>
      <!--读取bootstrap文件-->
      <dependency>
          <groupId>org.springframework.cloud</groupId>
          <artifactId>spring-cloud-starter-bootstrap</artifactId>
      </dependency>
      <!--seata-->
      <dependency>
          <groupId>com.alibaba.cloud</groupId>
          <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
      </dependency>
    
  2. 改造配置

    首先在 nacos 上添加一个共享的 seata 配置,命名为shared-seata.yaml

    seata:
      registry: # TC服务注册中心的配置,微服务根据这些信息去注册中心获取tc服务地址
        type: nacos # 注册中心类型 nacos
        nacos:
          server-addr: 192.168.150.101:8848 # nacos地址
          namespace: "" # namespace,默认为空
          group: DEFAULT_GROUP # 分组,默认是DEFAULT_GROUP
          application: seata-server # seata服务名称
          username: nacos
          password: nacos
      tx-service-group: hmall # 事务组名称
      service:
        vgroup-mapping: # 事务组与tc集群的映射关系
          hmall: "default"
    

    然后,改造trade-service模块,添加bootstrap.yaml

    spring:
      application:
        name: trade-service # 服务名称
      profiles:
        active: dev
      cloud:
        nacos:
          server-addr: 192.168.150.101 # nacos地址
          config:
            file-extension: yaml # 文件后缀名
            shared-configs: # 共享配置
              - dataId: shared-jdbc.yaml # 共享mybatis配置
              - dataId: shared-log.yaml # 共享日志配置
              - dataId: shared-swagger.yaml # 共享日志配置
              - dataId: shared-seata.yaml # 共享seata配置
    

    可以看到这里加载了共享的 seata 配置,然后改造 application.yaml 文件,内容如下:

    server:
      port: 8085
    feign:
      okhttp:
        enabled: true # 开启OKHttp连接池支持
      sentinel:
        enabled: true # 开启Feign对Sentinel的整合
    hm:
      swagger:
        title: 交易服务接口文档
        package: com.hmall.trade.controller
      db:
        database: hm-trade
    
  3. 添加数据库表

    seata 的客户端在解决分布式事务的时候需要记录一些中间数据,保存在数据库中。因此我们要先准备一个这样的表。

    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS `undo_log`
    (
        `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
        `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
        `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
        `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
        `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
        `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
        `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
        UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
      AUTO_INCREMENT = 1
      DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
    
    
  4. 测试

    我们找到trade-service模块下的com.hmall.trade.service.impl.OrderServiceImpl类中的createOrder方法,也就是下单业务方法。

    将其上的@Transactional注解改为 Seata 提供的@GlobalTransactional

2953321-20250425180024402-1555337375.png
@GlobalTransactional注解就是在标记事务的起点,将来 TM 就会基于这个方法判断全局事务范围,初始化全局事务。
我们重启trade-serviceitem-servicecart-service三个服务。再次测试,发现分布式事务的问题解决了!

2.4 XA模式

Seata 支持四种不同的分布式事务解决方案:

  • XA
  • TCC
  • AT
  • SAGA

XA 规范 是 X/Open 组织定义的分布式事务处理(DTP,Distributed Transaction Processing)标准,XA 规范 描述了全局的TM与局部的RM之间的接口,几乎所有主流的数据库都对 XA 规范 提供了支持。

2.4.1 两阶段提交

A 是规范,目前主流数据库都实现了这种规范,实现的原理都是基于两阶段提交。

正常情况:

2953321-20250425181308554-1369092179.png

异常情况:

2953321-20250425181315137-226489402.png

一阶段:

  • 事务协调者通知每个事务参与者执行本地事务
  • 本地事务执行完成后报告事务执行状态给事务协调者,此时事务不提交,继续持有数据库锁

二阶段:

  • 事务协调者基于一阶段的报告来判断下一步操作
  • 如果一阶段都成功,则通知所有事务参与者,提交事务
  • 如果一阶段任意一个参与者失败,则通知所有事务参与者回滚事务

2.4.2 Seta的XA模型

Seata 对原始的 XA 模式做了简单的封装和改造,以适应自己的事务模型,基本架构如图:

2953321-20250425181342115-1376168173.png

RM一阶段的工作:

  1. 注册分支事务到TC
  2. 执行分支业务sql但不提交
  3. 报告执行状态到TC

TC二阶段的工作:

  1. TC检测各分支事务执行状态
-   如果都成功,通知所有RM提交事务
-   如果有失败,通知所有RM回滚事务 

RM二阶段的工作:

  • 接收TC指令,提交或回滚事务

2.4.3 优缺点

XA模式的优点是什么?

  • 事务的强一致性,满足ACID原则
  • 常用数据库都支持,实现简单,并且没有代码侵入

XA模式的缺点是什么?

  • 因为一阶段需要锁定数据库资源,等待二阶段结束才释放,性能较差
  • 依赖关系型数据库实现事务

2.4.4 实现步骤

  1. 首先,我们要在配置文件中指定要采用的分布式事务模式。我们可以在 Nacos 中的共享 shared-seata.yaml 配置文件中设置:

    seata:
      data-source-proxy-mode: XA
    
  2. 其次,我们要利用@GlobalTransactional标记分布式事务的入口方法:

2953321-20250425181901475-417598372.png

2.5 AT模式

AT模式同样是分阶段提交的事务模型,不过缺弥补了XA模型中资源锁定周期过长的缺陷。

2.5.1 Seata的AT模型

2953321-20250425182007040-2099518318.png

阶段一RM的工作:

  • 注册分支事务
  • 记录undo-log(数据快照)
  • 执行业务sql并提交
  • 报告事务状态

阶段二提交时RM的工作:

  • 删除undo-log即可

阶段二回滚时RM的工作:

  • 根据undo-log恢复数据到更新前

2.5.2 流程梳理

我们用一个真实的业务来梳理下 AT 模式的原理。

比如,现在有一个数据库表,记录用户余额:

idmoney
1100

其中一个分支业务要执行的 SQL 为:

 update tb_account set money = money - 10 where id = 1

AT 模式下,当前分支事务执行流程如下:

一阶段

  1. TM发起并注册全局事务到TC
  2. TM调用分支事务
  3. 分支事务准备执行业务SQL
  4. RM拦截业务SQL,根据where条件查询原始数据,形成快照。
{
  "id": 1, "money": 100
}
  1. RM执行业务SQL,提交本地事务,释放数据库锁。此时 money = 90
  2. RM报告本地事务状态给TC

二阶段

  1. TM通知TC事务结束
  2. TC检查分支事务状态
    • 如果都成功,则立即删除快照
    • 如果有分支事务失败,需要回滚。读取快照数据({"id": 1, "money": 100}),将快照恢复到数据库。此时数据库再次恢复为100

流程图:

2953321-20250425182133466-108391786.png

2.5.3 AT与XA的区别

  • XA模式一阶段不提交事务,锁定资源;AT模式一阶段直接提交,不锁定资源。
  • XA模式依赖数据库机制实现回滚;AT模式利用数据快照实现数据回滚。
  • XA模式强一致;AT模式最终一致

可见,AT 模式使用起来更加简单,无业务侵入,性能更好。因此企业 90% 的分布式事务都可以用 AT 模式来解决。

2.5.4 实现步骤

  1. 添加 seata-at.sql 到微服务对应的数据库

    -- for AT mode you must to init this sql for you business database. the seata server not need it.
    CREATE TABLE IF NOT EXISTS `undo_log`
    (
        `branch_id`     BIGINT       NOT NULL COMMENT 'branch transaction id',
        `xid`           VARCHAR(128) NOT NULL COMMENT 'global transaction id',
        `context`       VARCHAR(128) NOT NULL COMMENT 'undo_log context,such as serialization',
        `rollback_info` LONGBLOB     NOT NULL COMMENT 'rollback info',
        `log_status`    INT(11)      NOT NULL COMMENT '0:normal status,1:defense status',
        `log_created`   DATETIME(6)  NOT NULL COMMENT 'create datetime',
        `log_modified`  DATETIME(6)  NOT NULL COMMENT 'modify datetime',
        UNIQUE KEY `ux_undo_log` (`xid`, `branch_id`)
    ) ENGINE = InnoDB
      AUTO_INCREMENT = 1
      DEFAULT CHARSET = utf8mb4 COMMENT ='AT transaction mode undo table';
    
    
  2. 修改 application.yaml 将事务模式修改为 AT 模式

    seata:
    	data-source-proxy-mode: AT #开启AT模式