分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑

分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑

关键词标签:Seata、分布式事务、AT 模式、TCC、Saga、全局锁、回滚补偿

一个容易被忽略的事实:Seata 的 AT 模式之所以能"无侵入"地回滚业务 SQL,靠的不是数据库事务,而是在业务库中额外维护一张 undo_log 表 记录前后镜像,再加一把由 TC 维护的全局锁 。这意味着------如果你的业务表没有主键、SQL 形态超出 Seata 解析器覆盖范围、或者把数据写进了 information_schema 这类系统表,AT 模式很可能在第一阶段就无法正确生成回滚信息。这不是 bug,是 AT 的设计边界。

本系列前面讲过线程池、AQS、CompletableFuture 这些"单机并发"的底座,也讲过 Spring Boot 自动配置的加载链路。这一篇我们把视角从单机拉到分布式,专门聊 Seata 三种模式(AT / TCC / SAGA)的取舍逻辑 和边界条件 。下文中的 order-service、storage-service 仅为讲解示例(非真实系统)。


一、先想清楚:分布式事务到底在解决什么

分布式事务的本质矛盾只有一个:多个独立资源(DB、MQ、RPC 服务)的本地事务无法用一个 XA 事务串起来,或者串起来代价太高。

Seata 的定位不是替代 XA,而是提供一套**"最终一致 + 自动补偿"**的框架。它把一次全局事务拆成:

  • TC (Transaction Coordinator):独立部署的协调者,维护全局事务和分支事务状态;
  • TM (Transaction Manager):发起全局事务的一方,通常是业务入口;
  • RM (Resource Manager):管理分支事务的资源,通常是每个微服务里的数据源代理。

核心流程是经典的两阶段:第一阶段各分支各自提交本地事务并注册分支;第二阶段 TC 根据全局状态决定提交或回滚。三种模式的差别,全在"第一阶段做了什么"和"第二阶段怎么补偿"。


二、AT 模式:无侵入的代价是全局锁

2.1 它到底做了什么

AT(Auto Transaction)的核心机制可以用一句话概括:拦截业务 SQL,解析出 before image 和 after image,写入 undo_log,然后提交本地事务。

关键点是:第一阶段本地事务就已经提交了。这就是为什么 AT 通常比 XA 性能更好------它没有把数据库连接一直挂到第二阶段。

那么回滚怎么办?靠 undo_log 里的前后镜像反向生成补偿 SQL。下面是 Seata 官方脚本 mysql.sql 中的 undo_log 表结构:

复制代码
CREATE TABLE `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;

回滚时,Seata 会校验当前数据的 after image 是否仍等于 undo_log 里记录的 after image 。如果不等,说明数据在全局事务提交后被别的事务改过,此时会抛出 SQLUndoFailedException(其内部封装了 RollbackRetryTimeoutException 等场景),由 TC 决定重试或进入人工处理。这个校验逻辑是为了防止"脏回滚"覆盖别人的更新。

2.2 全局锁:AT 真正的命门

第一阶段提交本地事务前,Seata 会向 TC 申请全局锁 (TC 侧维护 lock_table)。申请成功才提交,否则重试。这是 AT 保证隔离性的核心。

复制代码
// 示意:AT 模式下一个典型的业务方法(order-service 假设示例)
@GlobalTransactional(name = "create-order", rollbackFor = Exception.class)
public void createOrder(OrderDTO dto) {
    // 分支 1:本地订单库
    orderMapper.insert(buildOrder(dto));
    // 分支 2:远程扣库存(storage-service 也是 Seata RM)
    storageClient.deduct(dto.getSkuId(), dto.getCount());
    // 分支 3:远程扣余额
    accountClient.debit(dto.getUserId(), dto.getAmount());
}

这段代码看起来和普通 @Transactional 没区别,但 @GlobalTransactional 会通过 GlobalTransactionalInterceptor 开启全局事务,把 XID 通过 RPC 上下文透传到下游。

2.3 常见误区

  • 误区一:AT 能保证强一致。 不能。它保证的是最终一致 ,且默认隔离级别是读未提交(因为第一阶段就提交了)。如果你在全局事务未结束时读到了中间态数据,是正常的。
  • 误区二:AT 支持所有 SQL。 不支持。多表关联更新、INSERT ... SELECT、部分复杂子查询、DDL 语句,Seata 的 SQL 解析器可能解析失败。解析失败时,该分支不会生成 undo_log,回滚就无从谈起。
  • 误区三:全局锁不占数据库锁。 全局锁是 TC 内存 + lock_table 表维护的,和数据库行锁是两码事。但它会造成跨服务的串行等待------两个全局事务改同一行数据,后到的那个会一直重试直到超时。

三、TCC 模式:把补偿逻辑交给业务

3.1 三段式

TCC(Try-Confirm-Cancel)把每个分支拆成三个方法:

  • Try:预留资源(冻结库存、预扣余额);

  • Confirm :确认使用预留资源,必须幂等;

  • Cancel :释放预留资源,必须幂等且能空回滚。

    @LocalTCC
    public interface StorageTccAction {

    复制代码
      @TwoPhaseBusinessAction(name = "storageTcc", commitMethod = "confirm", rollbackMethod = "cancel")
      boolean tryDeduct(BusinessActionContext ctx,
                        @BusinessActionContextParameter(paramName = "skuId") Long skuId,
                        @BusinessActionContextParameter(paramName = "count") Integer count);
    
      boolean confirm(BusinessActionContext ctx);
    
      boolean cancel(BusinessActionContext ctx);

    }

@LocalTCC 表示这是本地 TCC,BusinessActionContext 会携带 XID 和分支 ID,用于幂等判断。

3.2 TCC 的三个经典坑

坑一:空回滚。 Try 还没执行(比如网络超时,TC 以为失败了),Cancel 先到了。此时不能报错,要识别"没有对应的 Try 记录"直接返回成功。做法通常是 Cancel 时先查预留记录,查不到就返回 true。

坑二:幂等。 网络抖动导致 Confirm/Cancel 被重复调用。必须靠唯一键(XID + branchId)去重。

坑三:悬挂。 Cancel 比 Try 先到,Cancel 空回滚返回成功;随后 Try 才到达并真的扣了库存。此时全局事务已结束,库存永远扣着。解决办法是 Try 执行前先检查是否已有对应的 Cancel 记录,有则拒绝执行。

这三个坑不是理论,是 TCC 落地的必备防御。任何声称"TCC 很简单"的说法,都忽略了这三个状态机。


四、SAGA 模式:长事务的最终答案

SAGA 的思路和 TCC 不同:它没有 Try,每个正向操作直接提交,失败时按逆序执行补偿操作。

Seata 的 SAGA 基于状态机(StateMachine),用 JSON 定义流程:

复制代码
{
  "Name": "orderSaga",
  "StartState": "CreateOrder",
  "States": {
    "CreateOrder": {
      "Type": "ServiceTask",
      "ServiceName": "orderService",
      "ServiceMethod": "create",
      "CompensateState": "CancelOrder",
      "Next": "DeductStorage"
    },
    "DeductStorage": {
      "Type": "ServiceTask",
      "ServiceName": "storageService",
      "ServiceMethod": "deduct",
      "CompensateState": "RestoreStorage",
      "Next": "Succeed"
    },
    "CancelOrder": { "Type": "CompensateState" },
    "RestoreStorage": { "Type": "CompensateState" },
    "Succeed": { "Type": "Succeed" }
  }
}

SAGA 适合长流程、跨多个服务、无法长时间持有资源 的场景,比如订单履约、跨系统对账。代价是:没有隔离性,中间状态对外可见,补偿逻辑必须由业务自己保证正确。


五、三种模式怎么选:一张决策表

维度 AT TCC SAGA
侵入性 低(加注解) 高(写三个方法) 中(写状态机 + 补偿)
隔离性 读未提交(全局锁) 由 Try 的预留程度决定 无
性能 中(全局锁有竞争) 高 高
适用事务长度 短 短 长
补偿正确性 框架自动 业务保证 业务保证
典型场景 常规 CRUD 跨服务 资金、库存等强约束 履约、审批流

一句话取舍:能用 AT 就用 AT,AT 覆盖不了(复杂 SQL、强隔离、非事务资源)再上 TCC,流程长且补偿天然可逆就用 SAGA。


六、什么时候别用 / 别踩的坑

  1. 别用 AT 处理资金。 读未提交的隔离性 + 全局锁重试,在资金场景下风险不可控。资金请用 TCC 或本地消息表。
  2. 别在 AT 事务里做 RPC 之外的副作用。 发 MQ、调第三方支付、写 Redis,这些不在 undo_log 覆盖范围内,回滚不会撤销它们。
  3. 别忽略 undo_log 的清理。 如果 TC 长时间不可用,undo_log 会堆积,需要配置 log_status 和定时清理策略,否则表会膨胀。
  4. 别把全局锁当成数据库锁。 它跨服务串行,热点数据(比如秒杀库存)在 AT 下会变成全局串行瓶颈。
  5. TCC 的 Cancel 必须能空回滚、必须幂等、必须防悬挂,这三条缺一不可,否则线上一定出数据不一致。
  6. SAGA 的补偿不是回滚,是"反向业务操作"。如果正向操作不可逆(比如已发短信、已扣积分),SAGA 就无能为力。

官方文档值得反复读的两处:Seata 官方文档、Apache Seata GitHub 仓库。源码里 io.seata.rm.datasource.undo 包下的 AbstractUndoExecutor 及其子类是理解 AT 回滚的最佳入口。


系列预告 :单机并发和分布式事务都聊完了,下一篇我们转向 分布式锁的三种实现(Redis / ZooKeeper / 数据库)在源码层面的取舍,重点拆 Redisson 看门狗续期和 RedLock 的争议。感兴趣可以关注,我们下篇见。

相关推荐
晨小安1 小时前
ThreadLocal及其内存泄漏问题深度解析
java·jvm·tomcat·哈希表
m0_587383002 小时前
深圳24小时自助健身房解决方案实战:从系统架构到部署指南
java·人工智能·spring boot·spring·系统架构·需求分析
郑州光合科技余经理2 小时前
同城电商系统:库存变更怎么同步到订单
java·开发语言·前端·后端·uni-app·php·ai编程
无序的浪3 小时前
测试博客-基于微服务的在线判题系统
java·spring cloud·docker·微服务·测试·在线判题
xfan_me3 小时前
维修保养记录精准版 API 对接实战指南
java·大数据·python
Keven-zhou3 小时前
不用先学前端框架,Java后端也能独立交付项目?飞算JavaAI实测
java
海上小飞龙3 小时前
改一个数,右边全得重算,这题怎么扛住两万次查询
java·c++·python
Leo.yuan3 小时前
从“看全局“到“评成效“:央国企穿透式监管六步链路,哪些厂商能真正闭环
java·大数据·人工智能
小马哥程序开发4 小时前
[点赞收藏免费领取 · 项目源码]57105基于Spring Boot的充电桩管理系统的设计与实现
java·spring boot·源码·课程设计·毕设·大作业·课设