找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

4130

积分

0

好友

545

主题
发表于 3 天前 | 查看: 15| 回复: 0

今天我们来深入探讨一下阿里巴巴 Seata 框架在 1.5.1 版本中,是如何通过创新的机制解决 TCC 模式长期存在的三大顽疾:幂等、悬挂和空回滚的。

1 TCC 模式回顾

TCC 是最经典的分布式事务解决方案之一,它将一个分布式事务拆分为两个阶段执行:

  1. Try 阶段:尝试执行业务,完成所有资源的检查和预留。
  2. Confirm/Cancel 阶段:根据 Try 阶段的结果,进行最终的提交(Confirm)或回滚(Cancel)。

以一个电商下单场景为例,涉及订单、库存、账户三个服务。在 Try 阶段,我们并非直接扣减库存和金额,而是将它们“预留”或“冻结”在一个中间状态。如果所有服务的 Try 操作都成功,则进入 Confirm 阶段,将预留的资源正式扣除。如果任何一个服务的 Try 操作失败,则进入 Cancel 阶段,释放所有预留的资源。

关键点:Try 阶段的操作必须是提交本地事务的。例如,冻结账户金额,必须真实地从客户账户中将这笔钱扣除并转入一个中间态。如果不这样做,在 Confirm 阶段可能因为账户余额不足而导致问题。

1.1 Try-Confirm 流程

Try 阶段预留资源,Confirm 阶段扣除资源。流程如下图所示:

Seata TCC模式 Try-Confirm 两阶段提交流程图

1.2 Try-Cancel 流程

Try 阶段预留资源时,若某个服务(如扣减库存)失败,则全局事务回滚,在 Cancel 阶段释放所有已预留的资源。流程如下图所示:

Seata TCC模式 Try-Cancel 两阶段回滚流程图

2 TCC 模式的优势

TCC 模式最显著的优点是高性能。因为它在 Try 阶段就真实提交了本地事务,将资源锁定在业务层面(中间态),而非数据库层面,避免了长时间的资源阻塞等待。

此外,TCC 模式还可以进行深度优化:

2.1 异步提交

Try 阶段成功后,可以不立即执行 Confirm/Cancel,而是认为全局事务已结束,通过后台定时任务异步地完成最终资源扣除或释放,这能极大提升系统的吞吐量。

2.2 同库模式优化

在标准 TCC 模型中,涉及三个角色:

  • TM (Transaction Manager):管理全局事务的开启、提交或回滚。
  • RM (Resource Manager):管理分支事务,负责执行实际的 Try、Confirm、Cancel 操作。
  • TC (Transaction Coordinator):协调全局事务和分支事务的状态。

标准流程中,RM 与 TC 之间需要进行多次 RPC 调用来注册和报告状态。Seata 的“同库模式”对此进行了优化,将分支事务状态记录在本地,从而减少了约 50% 的 RPC 调用,性能得到显著提升。

Seata 官网 TCC 标准模式架构图

Seata TCC 同库模式优化后架构图

3 RM 端代码示例

以一个库存服务为例,其 TCC 接口的 Java 代码通常如下所示:

@LocalTCC
public interface StorageService {

    /**
     * 扣减库存 (Try阶段)
     * @param xid 全局xid
     * @param productId 产品id
     * @param count 数量
     * @return
     */
    @TwoPhaseBusinessAction(name = "storageApi", commitMethod = "commit", rollbackMethod = "rollback", useTCCFence = true)
    boolean decrease(String xid, Long productId, Integer count);

    /**
     * 提交事务 (Confirm阶段)
     * @param actionContext
     * @return
     */
    boolean commit(BusinessActionContext actionContext);

    /**
     * 回滚事务 (Cancel阶段)
     * @param actionContext
     * @return
     */
    boolean rollback(BusinessActionContext actionContext);
}

通过 @LocalTCC 注解,RM 在初始化时会向 TC 注册这个分支事务。在 Try 方法(decrease)上的 @TwoPhaseBusinessAction 注解定义了该分支事务的 resourceId、提交方法和回滚方法。其中 useTCCFence = true 属性是 Seata 1.5.1 解决三大问题的关键,我们下一节详细讲解。

4 Seata 1.5.1 的解决方案

在 Seata 1.5.1 版本之前,幂等、悬挂和空回滚需要开发者自行在业务代码中处理,逻辑复杂且容易出错。新版本引入了一张核心表 tcc_fence_log,通过记录事务状态来系统性地解决这些问题。上文提到的 useTCCFence 属性就是用来启用这套防悬挂机制的开关,默认是 false

tcc_fence_log 表的建表语句(MySQL 语法)如下:

CREATE TABLE IF NOT EXISTS `tcc_fence_log`
(
    `xid`           VARCHAR(128)  NOT NULL COMMENT 'global id',
    `branch_id`     BIGINT        NOT NULL COMMENT 'branch id',
    `action_name`   VARCHAR(64)   NOT NULL COMMENT 'action name',
    `status`        TINYINT       NOT NULL COMMENT 'status(tried:1;committed:2;rollbacked:3;suspended:4)',
    `gmt_create`    DATETIME(3)   NOT NULL COMMENT 'create time',
    `gmt_modified`  DATETIME(3)   NOT NULL COMMENT 'update time',
    PRIMARY KEY (`xid`, `branch_id`),
    KEY `idx_gmt_modified` (`gmt_modified`),
    KEY `idx_status` (`status`)
) ENGINE = InnoDB
DEFAULT CHARSET = utf8mb4;

4.1 解决幂等问题

问题场景:在 Confirm/Cancel 阶段,网络波动可能导致 TC 未收到 RM 的响应,从而触发重试。如果 RM 的 Confirm/Cancel 操作不是幂等的,重试就会导致资源被重复扣减或释放。

Seata 解决方案
在 Confirm 阶段,TCCFenceHandler.commitFence 方法会先查询 tcc_fence_log 表,检查当前分支事务的状态。

  • 如果状态已经是 STATUS_COMMITTED(已提交),则直接返回成功,不再执行业务提交逻辑。
  • 如果状态是 STATUS_TRIED(已尝试),则将其更新为 STATUS_COMMITTED,并执行真正的业务提交方法。

这样,无论 TC 重试多少次,RM 端都只会成功提交一次。Cancel 阶段的幂等逻辑与此类似。

核心判断逻辑代码简化示意:

public static boolean commitFence(...) {
    // ... 获取数据库连接
    TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
    if (tccFenceDO == null) {
        // 记录不存在,抛出异常
    }
    // 关键幂等判断
    if (TCCFenceConstant.STATUS_COMMITTED == tccFenceDO.getStatus()) {
        LOGGER.info(“Branch transaction has already committed before. idempotency rejected...”);
        return true; // 已提交,直接返回成功
    }
    // ... 其他状态判断
    // 更新状态并执行业务方法
    return updateStatusAndInvokeTargetMethod(..., TCCFenceConstant.STATUS_COMMITTED, ...);
}

4.2 解决空回滚问题

问题场景:在 分布式事务 中,某个 RM 分支的 Try 操作可能因为网络超时或节点宕机根本没有执行,但全局事务已决定回滚。此时,TC 会向该 RM 发起 Cancel 指令。如果 RM 直接执行 Cancel 逻辑,就会“空回滚”一个从未预留过的资源。

TCC 模式空回滚问题示意图:账户服务1节点故障导致全局回滚

Seata 解决方案

  • Try 阶段:执行成功时,会向 tcc_fence_log 表插入一条状态为 STATUS_TRIED 的记录。
  • Cancel 阶段:执行 rollbackFence 方法时,先查询 tcc_fence_log 表。
    • 如果没有找到对应记录,说明 Try 阶段根本未执行,此时直接返回成功,并不执行业务回滚逻辑,从而避免了空回滚。
    • 如果找到了记录且状态为 STATUS_TRIED,则正常执行业务回滚。

核心逻辑代码简化:

public static boolean rollbackFence(...) {
    // ... 获取数据库连接
    TCCFenceDO tccFenceDO = TCC_FENCE_DAO.queryTCCFenceDO(conn, xid, branchId);
    // 空回滚处理
    if (tccFenceDO == null) {
        // 记录不存在,说明try没执行,不进行回滚操作
        return true;
    } else {
        // ... 其他状态判断(如已回滚则幂等返回)
    }
    // 正常更新状态并执行业务回滚
    return updateStatusAndInvokeTargetMethod(..., TCCFenceConstant.STATUS_ROLLBACKED, ...);
}

4.3 解决悬挂问题

问题场景:这是最棘手的问题。由于网络阻塞,Cancel 指令可能先于 Try 指令到达 RM。RM 执行了 Cancel(由于空回滚逻辑,可能什么都没做),之后迟到的 Try 指令才到达并成功执行,预留了资源。此时全局事务已结束,这些预留的资源将永远无法被释放。

TCC 模式悬挂问题示意图:Rollback命令先于Try命令到达

Seata 解决方案
结合空回滚和幂等的逻辑,形成了一个闭环防御:

  1. Cancel 先到:执行 rollbackFence 方法,查询 tcc_fence_log 表,发现没有记录(因为 Try 未执行)。此时,它不仅不执行业务回滚,还会插入一条状态为 STATUS_SUSPENDED(悬挂) 的记录。
  2. Try 后到:执行 prepareFence 方法,尝试插入状态为 STATUS_TRIED 的记录。由于 (xid, branch_id) 是主键,而 STATUS_SUSPENDED 记录已存在,会导致主键冲突插入失败。Seata 捕获到这个异常后,会识别出这是悬挂场景,从而阻止 Try 业务执行,并清理日志。

这样,无论指令顺序如何,都保证了“Try 成功执行”和“全局事务回滚”这两个事件是互斥的,从而防止了资源悬挂。

相关代码逻辑:

// Cancel阶段先到,插入防悬挂记录
if (tccFenceDO == null) {
    boolean result = insertTCCFenceLog(conn, xid, branchId, actionName, TCCFenceConstant.STATUS_SUSPENDED);
    return true; // 插入悬挂记录后直接返回,不执行业务回滚
}

// Try阶段后到,插入记录时发生主键冲突
catch (TCCFenceException e) {
    if (e.getErrcode() == FrameworkErrorCode.DuplicateKeyException) {
        LOGGER.error(“Branch transaction has already rollbacked before,prepare fence failed...”);
        addToLogCleanQueue(xid, branchId); // 识别为悬挂,加入清理队列
    }
    // ... 抛出异常,Try业务不会被执行
}

重要细节queryTCCFenceDO 方法在查询 SQL 中使用了 SELECT ... FOR UPDATE。这确保了在 Cancel 阶段查询并可能插入 STATUS_SUSPENDED 记录的这个过程是原子的,防止在并发场景下,Try 和 Cancel 同时判断无记录而都去插入,从而破坏防悬挂逻辑。

5 总结与思考

TCC 模式因其高性能特点,在要求严格的金融、电商等场景中应用广泛,但幂等、悬挂和空回滚一直是其落地过程中必须谨慎处理的“暗礁”。Seata 框架在 1.5.1 版本中,通过引入 tcc_fence_log 事务控制表,以一种优雅且系统性的方式完美解决了这三大经典难题,将复杂的分布式事务问题收敛到了框架内部,极大地减轻了开发者的心智负担。

此外,Seata 框架通过代理数据源,确保了对 tcc_fence_log 表的操作与 RM 的业务数据库操作处于同一个本地事务中,实现了“同生共死”,保证了事务日志与业务状态的一致性。

对于正在使用或考虑使用 TCC 模式解决 分布式事务 的团队来说,深入理解 Seata 的这套机制,不仅能帮助我们更好地使用这个强大的工具,也能深化我们对分布式事务本质的认识。如果你想了解更多关于 Java 分布式系统的实战经验与深度解析,欢迎访问 云栈社区 与更多开发者交流探讨。




上一篇:Nacos 3.0 API设计与安全性改进解析
下一篇:Nacos权限绕过漏洞(CVE-2021-29441)分析与升级修复方案
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-3-10 11:37 , Processed in 0.452466 second(s), 43 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表