Seata 分布式事务实战:从 AT 模式到 TCC 落地

Seata 分布式事务是微服务架构下解决跨库、跨服务数据一致性的主流方案。当一笔下单请求要同时扣减库存、创建订单、扣减账户余额,任何一个环节失败都必须让其他操作回滚,否则就会出现”少扣钱多发货”的资金漏洞。本文从 Seata 的 AT 自动补偿模式讲起,逐步延伸到 TCC 手动编排,帮你在大事务面前既能保住一致性,又不至于拖垮系统性能。

一、为什么单机事务撑不住分布式场景

在单库单表里,我们靠数据库自带的 ACID 和 @Transactional 就能保证”要么全成、要么全不做”。但微服务的核心就是把一个单体拆成多个独立部署的服务,每个服务拥有自己的数据库。当你在一笔业务里既要调库存服务、又要调订单服务、还要调账户服务,这三个操作落在三个不同的库里,本地事务就管不了全局了。

业界通常用 CAP 定理来权衡:分区容错在分布式下必选,于是只能在一致性(C)和可用性(A)之间取舍。强一致意味着要引入分布式事务协调者,付出一定的延迟代价;最终一致则把回滚改写成”补偿”,用异步对账兜底。Seata 的 AT 模式走的是”强一致加自动补偿”的折中路线,而 TCC 把补偿的编写权交回给业务,灵活但更重。

关于本地事务的边界与传播行为,建议先吃透 Spring 事务传播机制;当服务间调用走 gRPC 时,事务上下文的传递又多一层复杂度(参考 gRPC 微服务通信实战)。

二、Seata 的四大角色与整体架构

Seata 把分布式事务拆成几个核心角色:

  • TC(Transaction Coordinator):事务协调者,独立部署的服务,负责维护全局事务和分支事务的状态,驱动提交或回滚。
  • TM(Transaction Manager):事务管理器,嵌在发起方应用里,负责定义全局事务的边界(@GlobalTransactional)。
  • RM(Resource Manager):资源管理器,嵌在每个参与者应用里,管理分支事务与 TC 的注册、上报。
  • Undo Log:AT 模式下的关键设施,记录数据修改前镜像,用于自动回滚。

一次典型的全局事务流程是:TM 向 TC 申请开启全局事务拿到 XID,XID 通过上下文(HTTP Header 或 RPC 透传)传播到各 RM;每个 RM 执行业务 SQL 时向 TC 注册分支事务,并在本地落一份 undo_log;全部成功后 TC 通知各 RM 提交(异步清理 undo_log),任一失败则通知回滚(用 undo_log 反向补偿)。

三、AT 模式:无侵入的自动补偿

AT 模式是 Seata 最受欢迎的模式,最大特点是对业务代码零侵入:你只需要加一个 @GlobalTransactional 注解,Seata 就会在底层自动完成”解析 SQL、生成前置镜像、加全局锁、注册分支、异常时回滚”的全过程。

它依赖一张 undo_log 表,这张表必须建在每个参与分布式事务的业务的数据库里。事务提交前,Seata 把”修改前的数据快照”写进 undo_log;如果全局事务需要回滚,就用快照生成反向 SQL 把数据改回去,再删除快照。

-- 每个业务库都要建 undo_log 表
CREATE TABLE undo_log (
  id            BIGINT AUTO_INCREMENT PRIMARY KEY,
  branch_id     BIGINT       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,
  UNIQUE KEY ux_undo_log (xid, branch_id)
) ENGINE=InnoDB;

四、AT 模式代码落地(Spring Boot)

先在配置文件里声明事务分组,并指向 TC 的地址。Seata 1.5 之后默认用注册中心,这里以直连为例:

# application.yml
seata:
  enabled: true
  tx-service-group: my_tx_group      # 事务分组
  service:
    vgroup-mapping:
      my_tx_group: default           # 映射到 TC 集群
    grouplist:
      default: 127.0.0.1:8091        # TC 地址
  registry:
    type: file                       # 简单起见用 file,生产用 nacos

业务侧只需在发起方法上标注 @GlobalTransactional,其余分支方法照常用本地 @Transactional 即可。Seata 会自动代理数据源,拦截 SQL:

@Service
public class OrderService {
    @Autowired private InventoryMapper inventoryMapper;
    @Autowired private AccountMapper   accountMapper;
    @Autowired private OrderMapper     orderMapper;

    // 只加一个注解,三个库的写操作被纳入同一全局事务
    @GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
    public void createOrder(Long userId, Long itemId, int count) {
        inventoryMapper.deduct(itemId, count);    // 库存库
        accountMapper.deduct(userId, count * 10); // 账户库
        orderMapper.insert(userId, itemId, count);// 订单库
        // 任意一步抛异常,AT 会用 undo_log 自动回滚前面两步
    }
}

值得注意的是,AT 模式用”全局锁”防止脏写:在分支事务提交前,它会锁住涉及数据行的主键范围,直到全局事务结束才释放。这意味着两个并发的全局事务不能同时改同一行。当你的下单链路涉及分库分表时(例如用 分库分表 ShardingSphere 做水平拆分),AT 会把全局锁按实际路由的库分别加锁,配置上要额外留意。

五、AT 模式的局限与坑

AT 虽好,但有清晰的能力边界:

  • 热点行瓶颈:全局锁串行化使得高并发扣减同一行(如爆款商品库存)会严重排队,此时 AT 反而成为瓶颈。
  • 仅支持关系型:undo_log 机制依赖 SQL 解析,只对 MySQL、PostgreSQL、Oracle 等关系型数据库生效,Redis、MongoDB 等不行。
  • 隔离级别:AT 默认是”读未提交”的本地隔离,跨全局事务的脏读需要靠全局锁或人工 SELECT FOR UPDATE 解决。
  • 大事务 undo_log 膨胀:一次改成千上万行的 SQL,undo_log 也会跟着膨胀,回滚成本高。

如果你的场景是热点库存扣减、或需要跨非关系型存储,就该考虑 TCC 了。另外,数据库本身的高可用(主从复制、读写分离,见 MySQL 主从复制与读写分离)是分布式事务能稳定运行的地基,别指望 Seata 替你兜住底层存储的故障。

六、TCC 模式:把补偿权交回业务

TCC(Try-Confirm-Cancel)把每个参与者拆成三个方法:Try 预留资源(如冻结库存而非直接扣减)、Confirm 真正提交(把冻结转为扣减)、Cancel 释放预留(把冻结退回)。它不依赖 undo_log,而是靠业务自己写”反向操作”,因此能跨任意存储、且性能更高,代价是代码侵入重、要保证幂等。

// 1) 定义 TCC 接口,用 Seata 的注解声明三阶段
@LocalTCC
public interface InventoryTccAction {
    @TwoPhaseBusinessAction(name = "deductInventory",
            commitMethod = "commit", rollbackMethod = "rollback")
    boolean prepare(BusinessActionContext ctx, Long itemId, int count);

    boolean commit(BusinessActionContext ctx);   // Confirm
    boolean rollback(BusinessActionContext ctx); // Cancel
}

// 2) 实现:Try 冻结、Confirm 扣减、Cancel 解冻
@Component
public class InventoryTccActionImpl implements InventoryTccAction {
    @Override
    public boolean prepare(BusinessActionContext ctx, Long itemId, int count) {
        return inventoryMapper.freeze(itemId, count) > 0; // 冻结而非扣减
    }
    @Override
    public boolean commit(BusinessActionContext ctx) {
        Long itemId = ctx.getActionContext("itemId", Long.class);
        int  count  = ctx.getActionContext("count", Integer.class);
        return inventoryMapper.confirmDeduct(itemId, count) > 0;
    }
    @Override
    public boolean rollback(BusinessActionContext ctx) {
        Long itemId = ctx.getActionContext("itemId", Long.class);
        int  count  = ctx.getActionContext("count", Integer.class);
        return inventoryMapper.unfreeze(itemId, count) > 0;  // 解冻
    }
}

TCC 的成败关键在于幂等:网络抖动可能导致 Confirm 或 Cancel 被重复调用,因此每一阶段都要能安全地重复执行(通常用”已处理状态位”去重)。这点和本地事务传播里的嵌套回滚思路不同,建议结合 Spring 事务传播机制 对比理解”谁该回滚、回滚到哪”。

七、AT 还是 TCC?一表说清

选型时不必二选一,常见做法是”AT 兜底常规链路、TCC 攻坚热点”。下面这张对照表覆盖大多数决策点:

维度AT 模式TCC 模式
代码侵入几乎无(一个注解)重(三方法加幂等)
性能受全局锁约束高,无全局锁
存储支持仅关系型任意(含 Redis、MQ)
一致性强一致强一致(需自保证)
适用场景常规跨库写热点、跨存储、高性能

八、生产落地清单

无论选哪种模式,上线前都建议过一遍这张检查表,避免半夜被一致性告警叫醒:

检查项要点
TC 高可用TC 必须集群部署,否则它挂了全局事务全卡死
undo_log 表每个业务库都建,且和主表在同一库(同库事务)
XID 透传Feign、gRPC、Dubbo 都要配置拦截器把 XID 往下传
幂等TCC 的 Confirm、Cancel 必须可重入
空回滚与防悬挂处理 Try 未执行但 Cancel 先到的极端时序

微服务间的事务协调建立在稳定服务治理之上,先把 Spring Cloud 微服务治理 的注册发现与熔断限流做扎实,Seata 才能安心工作;若链路里用到了异步线程,注意 Java 21 虚拟线程 的线程模型可能影响 XID 的上下文传递。

九、小结

Seata 分布式事务用 AT 模式把”跨库回滚”变得几乎无侵入,又用 TCC 模式给热点与跨存储场景留出高性能通道。记住三件事:AT 靠 undo_log 与全局锁自动补偿但有热点瓶颈;TCC 把补偿交回业务、要自己保证幂等;生产环境务必保证 TC 高可用与 XID 透传。把一致性边界想清楚,分布式事务就不再是让人失眠的难题。

上一篇 MoE 混合专家模型:原理、路由与落地实战
下一篇 JWT 续期与无感刷新实战:双令牌与滑动会话方案