分布式事务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 它到底做了什么ATAuto 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 三段式TCCTry-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表示这是本地 TCCBusinessActionContext会携带 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 适合长流程、跨多个服务、无法长时间持有资源的场景比如订单履约、跨系统对账。代价是没有隔离性中间状态对外可见补偿逻辑必须由业务自己保证正确。五、三种模式怎么选一张决策表维度ATTCCSAGA侵入性低加注解高写三个方法中写状态机 补偿隔离性读未提交全局锁由 Try 的预留程度决定无性能中全局锁有竞争高高适用事务长度短短长补偿正确性框架自动业务保证业务保证典型场景常规 CRUD 跨服务资金、库存等强约束履约、审批流一句话取舍能用 AT 就用 ATAT 覆盖不了复杂 SQL、强隔离、非事务资源再上 TCC流程长且补偿天然可逆就用 SAGA。六、什么时候别用 / 别踩的坑别用 AT 处理资金。读未提交的隔离性 全局锁重试在资金场景下风险不可控。资金请用 TCC 或本地消息表。别在 AT 事务里做 RPC 之外的副作用。发 MQ、调第三方支付、写 Redis这些不在undo_log覆盖范围内回滚不会撤销它们。别忽略undo_log的清理。如果 TC 长时间不可用undo_log会堆积需要配置log_status和定时清理策略否则表会膨胀。别把全局锁当成数据库锁。它跨服务串行热点数据比如秒杀库存在 AT 下会变成全局串行瓶颈。TCC 的 Cancel 必须能空回滚、必须幂等、必须防悬挂这三条缺一不可否则线上一定出数据不一致。SAGA 的补偿不是回滚是反向业务操作。如果正向操作不可逆比如已发短信、已扣积分SAGA 就无能为力。官方文档值得反复读的两处Seata 官方文档、Apache Seata GitHub 仓库。源码里io.seata.rm.datasource.undo包下的AbstractUndoExecutor及其子类是理解 AT 回滚的最佳入口。系列预告单机并发和分布式事务都聊完了下一篇我们转向分布式锁的三种实现Redis / ZooKeeper / 数据库在源码层面的取舍重点拆 Redisson 看门狗续期和 RedLock 的争议。感兴趣可以关注我们下篇见。