侧边栏壁纸
  • 累计撰写 71 篇文章
  • 累计创建 28 个标签
  • 累计收到 21 条评论

目 录CONTENT

文章目录

Spring Boot 3.x 集成 Seata:分布式事务实战全攻略(AT 模式 + TCC 模式)

Administrator
2026-03-25 / 0 评论 / 0 点赞 / 0 阅读 / 0 字 / 正在检测是否收录...
温馨提示:
部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

在微服务架构中,一次业务操作往往需要跨越多个服务、多个数据库。传统的 ACID 事务只能保证单库的原子性,而跨服务调用时,"部分成功、部分失败"的问题会直接导致数据不一致。

举个典型场景:下单扣库存 + 扣余额,如果扣完库存后,扣余额的服务宕机了,库存已减但订单没创建,数据就乱了。

Seata 是阿里巴巴开源的分布式事务解决方案,目前已进入 Apache 孵化,是微服务体系里处理分布式事务的事实标准之一。本文将带你从零开始,在 Spring Boot 3.x + Nacos 环境下集成 Seata,覆盖最常用的 AT 模式TCC 模式,并给出完整踩坑记录。


一、Seata 核心概念速览

在动手之前,先理清几个关键术语:

角色说明
TC(Transaction Coordinator)事务协调者,独立部署的 Seata Server,维护全局事务状态
TM(Transaction Manager)事务管理器,标注 @GlobalTransactional 的服务,负责开启/提交/回滚全局事务
RM(Resource Manager)资源管理器,操作具体数据库的服务,向 TC 汇报分支事务状态

1.1 AT 模式(推荐入门)

AT 模式是 Seata 的默认模式,对业务代码无侵入:

  1. 一阶段:业务 SQL 执行,Seata 拦截并生成 undo_log(回滚日志)
  2. 二阶段提交:删除 undo_log,全局提交
  3. 二阶段回滚:读取 undo_log,执行逆向 SQL,还原数据

核心要求:每个业务库都要建 undo_log 表。

1.2 TCC 模式(高性能场景)

TCC 需要业务方自己实现三个方法:

  • Try:资源预留(锁定/冻结)
  • Confirm:真正执行业务逻辑
  • Cancel:释放预留资源

TCC 无需 undo_log,性能更高,但对业务有一定侵入性。


二、环境准备

2.1 版本说明

组件版本
Spring Boot3.2.x
Spring Cloud2023.0.x
Spring Cloud Alibaba2023.0.1.0
Seata Server2.1.0
Nacos2.3.x
MySQL8.0+
JDK17+

2.2 部署 Seata Server

Seata Releases 下载 seata-server-2.1.0.tar.gz,解压后修改配置:

conf/application.yml(核心配置片段)

seata:
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      data-id: seataServer.properties
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      namespace: ""
      group: SEATA_GROUP
      application: seata-server
  store:
    mode: db
    db:
      datasource: druid
      db-type: mysql
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://127.0.0.1:3306/seata?useUnicode=true&characterEncoding=utf8&useSSL=false
      user: root
      password: your_password
      min-conn: 10
      max-conn: 100
      global-table: global_table
      branch-table: branch_table
      lock-table: lock_table
      distributed-lock-table: distributed_lock

2.3 初始化 Seata 数据库

在 MySQL 中创建 seata 数据库,执行官方 SQL 脚本(script/server/db/mysql.sql),主要创建:

  • global_table:全局事务表
  • branch_table:分支事务表
  • lock_table:全局锁表

2.4 每个业务库创建 undo_log 表(AT 模式必须)

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';

三、Spring Boot 3.x 集成 Seata

3.1 引入依赖

<dependencies>
    <!-- Spring Cloud Alibaba Seata -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-seata</artifactId>
    </dependency>
    
    <!-- Nacos 注册与配置中心 -->
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba.cloud</groupId>
        <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
    </dependency>
    
    <!-- OpenFeign 服务调用 -->
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-openfeign</artifactId>
    </dependency>
    
    <!-- 数据库 -->
    <dependency>
        <groupId>com.mysql</groupId>
        <artifactId>mysql-connector-j</artifactId>
    </dependency>
    <dependency>
        <groupId>com.baomidou</groupId>
        <artifactId>mybatis-plus-boot-starter</artifactId>
        <version>3.5.7</version>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid-spring-boot-3-starter</artifactId>
        <version>1.2.21</version>
    </dependency>
</dependencies>

注意spring-cloud-alibaba 的 BOM 版本需要和 Spring Boot 3.x 对应,使用 2023.0.1.0

3.2 application.yml 配置

spring:
  application:
    name: order-service
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
      config:
        server-addr: 127.0.0.1:8848
        file-extension: yaml
    alibaba:
      seata:
        tx-service-group: my_tx_group  # 事务组名,需与 Seata Server 配置对应

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_tx_group
  service:
    vgroup-mapping:
      my_tx_group: default  # 映射到 Seata Server 的集群名
  registry:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      application: seata-server
  config:
    type: nacos
    nacos:
      server-addr: 127.0.0.1:8848
      group: SEATA_GROUP
      data-id: seataServer.properties
  data-source-proxy-mode: AT  # AT 模式(TCC 模式下可省略)

四、AT 模式实战:下单 + 扣库存

4.1 业务场景

  • order-service(订单服务):创建订单
  • stock-service(库存服务):扣减库存
  • account-service(账户服务):扣减余额

order-service 作为 TM,通过 Feign 调用 stock-serviceaccount-service

4.2 订单服务(TM 端)

@Service
@RequiredArgsConstructor
public class OrderServiceImpl implements OrderService {

    private final OrderMapper orderMapper;
    private final StockFeignClient stockFeignClient;
    private final AccountFeignClient accountFeignClient;

    /**
     * @GlobalTransactional 开启全局事务,这是 AT 模式的核心注解
     * rollbackFor 建议显式指定,避免非受检异常不触发回滚
     */
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    @Override
    public void createOrder(OrderDTO orderDTO) {
        // 1. 创建订单
        Order order = new Order();
        order.setUserId(orderDTO.getUserId());
        order.setCommodityCode(orderDTO.getCommodityCode());
        order.setCount(orderDTO.getCount());
        order.setMoney(orderDTO.getMoney());
        order.setStatus(0); // 创建中
        orderMapper.insert(order);
        
        // 2. 扣减库存(远程调用)
        stockFeignClient.deduct(orderDTO.getCommodityCode(), orderDTO.getCount());
        
        // 3. 扣减账户余额(远程调用)
        accountFeignClient.deduct(orderDTO.getUserId(), orderDTO.getMoney());
        
        // 4. 更新订单状态
        order.setStatus(1); // 已完成
        orderMapper.updateById(order);
        
        log.info("订单创建成功,XID: {}", RootContext.getXID());
    }
}

4.3 库存服务(RM 端)

@Service
@RequiredArgsConstructor
public class StockServiceImpl implements StockService {

    private final StockMapper stockMapper;

    /**
     * RM 端无需 @GlobalTransactional,Seata 会自动代理数据源
     * 拦截 SQL,生成 before/after image,写入 undo_log
     */
    @Transactional(rollbackFor = Exception.class)
    @Override
    public void deduct(String commodityCode, Integer count) {
        Stock stock = stockMapper.selectByCommodityCode(commodityCode);
        if (stock == null || stock.getCount() < count) {
            throw new RuntimeException("库存不足,commodityCode: " + commodityCode);
        }
        stock.setCount(stock.getCount() - count);
        stockMapper.updateById(stock);
        log.info("库存扣减成功,商品: {},扣减: {},剩余: {}", 
                commodityCode, count, stock.getCount());
    }
}

4.4 验证 AT 模式回滚

主动在账户服务中抛出异常,观察库存和订单数据是否自动回滚:

// AccountServiceImpl.java
@Override
public void deduct(Long userId, BigDecimal money) {
    // 模拟异常
    if (money.compareTo(new BigDecimal("500")) > 0) {
        throw new RuntimeException("余额不足!");
    }
    // ... 扣减逻辑
}

测试结果:当账户扣减失败时,已执行的库存扣减订单创建会被 Seata 自动回滚,undo_log 中的逆向 SQL 被执行,数据完整恢复。


五、TCC 模式实战:资金转账

AT 模式依赖数据库 undo_log,在高并发场景下会有性能瓶颈。TCC 模式通过业务层面的资源预留避免了这个问题。

5.1 TCC 接口定义

@LocalTCC
public interface AccountTccService {

    /**
     * Try 阶段:冻结资金(资源预留)
     * @TwoPhaseBusinessAction 声明这是一个 TCC 接口
     * commitMethod 对应 Confirm,rollbackMethod 对应 Cancel
     */
    @TwoPhaseBusinessAction(
        name = "accountTccService",
        commitMethod = "confirmDeduct",
        rollbackMethod = "cancelDeduct"
    )
    boolean tryDeduct(BusinessActionContext context,
                      @BusinessActionContextParameter(paramName = "userId") Long userId,
                      @BusinessActionContextParameter(paramName = "money") BigDecimal money);

    /**
     * Confirm 阶段:真正扣减,需保证幂等
     */
    boolean confirmDeduct(BusinessActionContext context);

    /**
     * Cancel 阶段:释放冻结资金,需保证幂等
     */
    boolean cancelDeduct(BusinessActionContext context);
}

5.2 TCC 接口实现

@Service
@RequiredArgsConstructor
@Slf4j
public class AccountTccServiceImpl implements AccountTccService {

    private final AccountMapper accountMapper;
    private final FreezeRecordMapper freezeRecordMapper;

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean tryDeduct(BusinessActionContext context, Long userId, BigDecimal money) {
        String xid = context.getXid();
        log.info("[TCC-Try] userId: {}, money: {}, xid: {}", userId, money, xid);
        
        // 检查是否已处理(幂等控制)
        FreezeRecord record = freezeRecordMapper.selectByXid(xid);
        if (record != null) {
            log.info("[TCC-Try] 已处理,幂等返回 true");
            return true;
        }
        
        // 查询账户
        Account account = accountMapper.selectByUserId(userId);
        if (account == null || account.getMoney().compareTo(money) < 0) {
            throw new RuntimeException("余额不足");
        }
        
        // 冻结资金:减少可用余额,增加冻结金额
        account.setMoney(account.getMoney().subtract(money));
        account.setFreezeMoney(account.getFreezeMoney().add(money));
        accountMapper.updateById(account);
        
        // 记录冻结流水(用于 Confirm/Cancel 幂等判断)
        FreezeRecord newRecord = new FreezeRecord();
        newRecord.setXid(xid);
        newRecord.setUserId(userId);
        newRecord.setMoney(money);
        newRecord.setState(0); // 0: 冻结中
        freezeRecordMapper.insert(newRecord);
        
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean confirmDeduct(BusinessActionContext context) {
        String xid = context.getXid();
        Long userId = Long.parseLong(context.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
        
        log.info("[TCC-Confirm] userId: {}, money: {}, xid: {}", userId, money, xid);
        
        // 幂等检查
        FreezeRecord record = freezeRecordMapper.selectByXid(xid);
        if (record == null || record.getState() == 1) {
            log.info("[TCC-Confirm] 已处理,幂等返回 true");
            return true;
        }
        
        // 清除冻结金额(资金真正扣除)
        Account account = accountMapper.selectByUserId(userId);
        account.setFreezeMoney(account.getFreezeMoney().subtract(money));
        accountMapper.updateById(account);
        
        // 更新流水状态
        record.setState(1); // 1: 已确认
        freezeRecordMapper.updateById(record);
        
        return true;
    }

    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cancelDeduct(BusinessActionContext context) {
        String xid = context.getXid();
        Long userId = Long.parseLong(context.getActionContext("userId").toString());
        BigDecimal money = new BigDecimal(context.getActionContext("money").toString());
        
        log.info("[TCC-Cancel] userId: {}, money: {}, xid: {}", userId, money, xid);
        
        // 幂等检查
        FreezeRecord record = freezeRecordMapper.selectByXid(xid);
        if (record == null) {
            // 空回滚:Try 阶段未执行就触发了 Cancel,直接返回成功
            log.info("[TCC-Cancel] 空回滚,直接返回 true");
            return true;
        }
        if (record.getState() == 2) {
            log.info("[TCC-Cancel] 已回滚,幂等返回 true");
            return true;
        }
        
        // 释放冻结资金:归还可用余额
        Account account = accountMapper.selectByUserId(userId);
        account.setMoney(account.getMoney().add(money));
        account.setFreezeMoney(account.getFreezeMoney().subtract(money));
        accountMapper.updateById(account);
        
        // 更新流水状态
        record.setState(2); // 2: 已取消
        freezeRecordMapper.updateById(record);
        
        return true;
    }
}

六、踩坑记录

坑1:vgroup-mapping 配置不匹配导致连接 Seata Server 失败

现象:启动时报 no available service 'default' found

原因tx-service-group 配置的值(如 my_tx_group),需要在 Nacos 的 seataServer.properties 中有对应的 service.vgroupMapping.my_tx_group=default 映射,且 default 必须与 Seata Server 的集群名一致。

解决:确保三方一致:

# Nacos 中 seataServer.properties
service.vgroupMapping.my_tx_group=default
# 应用配置
seata.service.vgroup-mapping.my_tx_group: default

坑2:Spring Boot 3.x 下 DataSourceProxy 自动配置冲突

现象:启动时报 No qualifying bean of type 'DataSource'The bean 'dataSource' could not be registered

原因spring-cloud-starter-alibaba-seata 在 Spring Boot 3.x 下,自动配置类路径有变化,可能导致数据源代理重复注册。

解决:在 application.yml 中显式排除自动配置,并手动配置数据源代理:

spring:
  autoconfigure:
    exclude:
      - com.alibaba.druid.spring.boot3.autoconfigure.DruidDataSourceAutoConfigure

然后手动配置 DataSourceProxy Bean:

@Configuration
public class SeataDataSourceConfig {

    @Bean
    @ConfigurationProperties(prefix = "spring.datasource.druid")
    public DataSource druidDataSource() {
        return new DruidDataSource();
    }

    @Primary
    @Bean("dataSource")
    public DataSourceProxy dataSource(DataSource druidDataSource) {
        return new DataSourceProxy(druidDataSource);
    }
}

坑3:TCC 空回滚与悬挂问题

现象:Cancel 在 Try 之前执行(网络延迟场景),或 Try 在 Cancel 后执行(悬挂)

解决策略

问题原因解决方案
空回滚Try 未执行,Cancel 被调用Cancel 中检查冻结记录,不存在则直接返回成功
幂等Confirm/Cancel 被重复调用通过 XID + 状态字段做幂等判断
悬挂Cancel 先执行,Try 后执行Cancel 执行时插入一条"已取消"记录,Try 阶段检查到后直接返回失败

坑4:MyBatis Plus 与 Seata AT 模式的兼容性

现象:使用 updateById 时,Seata 生成的 undo_log 不完整

原因:MyBatis Plus 动态 SQL 在某些情况下不会更新所有字段,导致 before image 与 after image 不匹配。

解决:关键更新操作使用 updateAllColumns,或者使用 LambdaUpdateWrapper 明确指定更新字段。


七、AT 模式 vs TCC 模式选型建议

维度AT 模式TCC 模式
业务侵入性无侵入,自动代理需要实现 Try/Confirm/Cancel
性能有 undo_log 写入开销高,无 undo_log
适用场景简单业务,快速接入高并发、高性能场景
锁粒度行锁(by undo_log)业务自定义(可更细)
数据库要求必须支持本地事务不限
开发成本中等(需处理幂等/空回滚/悬挂)

推荐策略

  • 业务初期、并发不高:优先 AT 模式,快速落地
  • 核心支付、资金流水等高并发场景:使用 TCC 模式,保障性能和一致性

八、总结

本文完整演示了 Spring Boot 3.x + Seata 2.1.0 的集成过程:

  1. AT 模式:零侵入,通过 @GlobalTransactional 即可开启全局事务,适合大多数业务场景
  2. TCC 模式:需要实现 Try/Confirm/Cancel 三个阶段,但性能更好,适合高并发金融场景
  3. 踩坑总结:重点关注 vgroup-mapping 配置、DataSourceProxy 冲突、TCC 幂等/空回滚/悬挂问题

分布式事务本质上是在 数据一致性系统可用性/性能 之间做权衡。Seata 提供了一套完善的解决方案,但落地时仍需结合具体业务场景选择合适的模式。


参考资料


如果本文对你有帮助,欢迎点赞收藏!有问题欢迎在评论区交流~

0

评论区