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

3471

积分

0

好友

481

主题
发表于 7 天前 | 查看: 31| 回复: 0

只要谈到微服务架构,分布式事务的一致性就是个无法回避的核心议题。传统的本地事务(@Transactional)在单体应用中游刃有余,可一旦跨服务调用,事务边界便立即失效,数据一致性难以保障。

像经典的 2PC(两阶段提交) 方案,虽然理论严谨,但在高并发生产环境下常常面临困境:同步阻塞导致吞吐量下降、协调者单点风险、长事务严重占用资源。因此,在高可用与高并发的场景下,补偿型事务模式(TCC / Saga) 展现出了更高的工程价值。

其中,TCC(Try-Confirm-Cancel) 是一种对业务侵入较深但一致性极强的方案,特别适用于支付扣款、库存冻结、核心订单状态流转等对数据准确性要求极高的场景。然而,TCC真正的挑战往往不在于理解“三段式”调用逻辑,而在于如何妥善处理幂等性、空回滚和悬挂这三大经典难题。在云栈社区等开发者社区中,TCC的实现细节与“坑点”也是经常被讨论的话题。

本文将以库存扣减为例,基于 Seata 框架与 Spring Boot,深入讲解如何落地一个健壮的TCC事务,并彻底解决上述三大问题。

TCC 模式核心思想

TCC将一个完整的分布式事务拆分为三个可管理的阶段:

阶段 职责 特点
Try 资源检查与预留 允许失败
Confirm 提交业务操作 理论上应永不失败(尽力而为)
Cancel 释放(回滚)预留资源 必须可靠执行

库存扣减为例:

  • Try:冻结库存(可用库存 available 减少,冻结库存 frozen 增加)。
  • Confirm:确认扣减,减少冻结库存(frozen 减少)。
  • Cancel:取消操作,解冻库存(available 增加,frozen 减少)。

TCC 的三大经典问题与挑战

1. 幂等性 (Idempotency)

Confirm或Cancel接口可能因为网络抖动、协调器重试、服务超时等原因被多次调用。必须保证多次调用产生的结果与一次调用完全相同

2. 空回滚 (Empty Rollback)

在分布式环境下,可能出现Cancel请求先于Try请求到达的情况(例如Try阶段超时,协调器直接触发回滚)。此时,Cancel操作需要能够识别出“Try未执行”,并直接成功返回,而不能去执行实际的资源回滚逻辑。

3. 悬挂 (Hanging)

与空回滚相反,悬挂是指Try请求在Cancel请求之后才到达并执行。这会导致资源被预留(冻结),但后续再也没有Confirm或Cancel来处置它,造成资源“悬挂”。

解决方案的核心是在Try执行前,检查是否已有对应的Cancel记录。

技术选型:为何选择 Seata TCC?

主流TCC框架对比:

框架 特点
Seata 支持 AT、TCC、Saga、XA 多种模式,生态成熟,社区活跃
Hmily 专精TCC模式,性能表现较好
tcc-transaction 实现较早,但近年来更新较少

本文选择 Seata TCC,主要优势在于它与Spring Boot集成非常简单,支持多种事务模式便于后续架构演进,并且拥有活跃的社区支持。

环境准备与配置

1. 启动 Seata Server

首先,你需要下载并启动Seata的服务端(TC,事务协调者)。

sh seata-server.sh -p 8091 -h 127.0.0.1

2. 引入依赖

在参与分布式事务的Spring Boot服务中,添加Seata依赖。

<dependency>
    <groupId>io.seata</groupId>
    <artifactId>seata-spring-boot-starter</artifactId>
    <version>1.5.2</version>
</dependency>

3. 配置文件 (application.yml)

配置Seata客户端,指向刚才启动的TC服务器。

seata:
  enabled: true
  application-id: ${spring.application.name}
  tx-service-group: my_test_tx_group
  service:
    vgroup-mapping:
      my_test_tx_group: default
    grouplist:
      default: 127.0.0.1:8091
  registry:
    type: file
  config:
    type: file

TCC 接口定义(库存服务示例)

首先,定义一个TCC接口。Seata通过注解来识别。

@LocalTCC
public interface InventoryTccService {

    @TwoPhaseBusinessAction(
        name = "inventoryTccAction",
        commitMethod = "confirm",
        rollbackMethod = "cancel"
    )
    boolean tryDecrease(
        @BusinessActionContextParameter(paramName = "productId") Long productId,
        @BusinessActionContextParameter(paramName = "count") Integer count
    );

    boolean confirm(BusinessActionContext context);

    boolean cancel(BusinessActionContext context);
}

幂等控制的核心:事务日志表

这是解决所有问题的基石。我们需要一张表来记录每个分支事务(Try/Confirm/Cancel)的执行状态。

CREATE TABLE tcc_transaction_log (
  tx_id VARCHAR(128) PRIMARY KEY,
  phase TINYINT COMMENT '阶段:1-Try, 2-Confirm, 3-Cancel',
  status TINYINT COMMENT '状态:0-进行中, 1-成功',
  gmt_create DATETIME,
  gmt_modified DATETIME
);

tx_id 建议由 全局事务ID(XID):分支事务ID(BranchId) 组成,确保全局唯一。

Try 阶段实现(防悬挂 + 防重复)

这是逻辑最复杂的一步,需要同时处理悬挂和幂等。

@Override
public boolean tryDecrease(Long productId, Integer count) {

    String xid = RootContext.getXID();
    String branchId = BusinessActionContextUtil.getContext().getBranchId();
    String txId = xid + ":" + branchId;

    // ① 防悬挂:检查是否已有Cancel记录
    if (tccLogMapper.exists(txId, Phase.CANCEL)) {
        log.info("分支事务 {} 已执行Cancel,Try阶段拒绝执行,防止悬挂", txId);
        return true; // 返回成功,让事务继续,后续Confirm/Cancel会处理
    }

    // ② 幂等控制:尝试插入Try阶段日志
    try {
        tccLogMapper.insert(txId, Phase.TRY, Status.PROCESSING);
    } catch (DuplicateKeyException e) {
        // 唯一键冲突,说明Try已执行过,幂等返回
        log.info("分支事务 {} Try阶段已执行,幂等返回", txId);
        return true;
    }

    // ③ 执行业务:冻结库存
    int ret = inventoryMapper.freezeStock(productId, count);
    if (ret <= 0) {
        throw new RuntimeException("库存冻结失败,可能库存不足");
    }

    // ④ 更新日志状态为成功
    tccLogMapper.markSuccess(txId, Phase.TRY);
    return true;
}

Confirm 阶段实现(幂等控制)

Confirm逻辑相对简单,核心是保证幂等。

@Override
public boolean confirm(BusinessActionContext context) {
    Long productId = Long.valueOf(context.getActionContext("productId").toString());
    Integer count = Integer.valueOf(context.getActionContext("count").toString());
    String txId = buildTxId(context); // 构建同上

    // 幂等判断:Confirm是否已成功执行过?
    if (tccLogMapper.existsSuccess(txId, Phase.CONFIRM)) {
        return true;
    }

    // 执行业务:确认扣减冻结的库存
    inventoryMapper.confirmDecrease(productId, count);

    // 记录Confirm成功日志
    tccLogMapper.markSuccess(txId, Phase.CONFIRM);
    return true;
}

Cancel 阶段实现(幂等 + 空回滚)

Cancel需要处理空回滚和幂等。

@Override
public boolean cancel(BusinessActionContext context) {
    Long productId = Long.valueOf(context.getActionContext("productId").toString());
    Integer count = Integer.valueOf(context.getActionContext("count").toString());
    String txId = buildTxId(context);

    // ① 幂等判断
    if (tccLogMapper.existsSuccess(txId, Phase.CANCEL)) {
        return true;
    }

    // ② 空回滚判断:Try阶段是否成功执行了?
    if (!tccLogMapper.existsSuccess(txId, Phase.TRY)) {
        log.info("分支事务 {} 为空回滚,Try未执行,记录Cancel日志后直接返回", txId);
        tccLogMapper.markSuccess(txId, Phase.CANCEL); // 重要!记录空回滚日志,防止后续Try悬挂
        return true;
    }

    // ③ 执行业务:解冻库存(正常回滚)
    inventoryMapper.cancelFreeze(productId, count);

    // 记录Cancel成功日志
    tccLogMapper.markSuccess(txId, Phase.CANCEL);
    return true;
}

事务发起方(订单服务)

在事务发起方(例如订单服务),使用 @GlobalTransactional 注解开启全局事务。

@Service
public class OrderServiceImpl {
    @Autowired
    private InventoryTccService inventoryTccService;
    // ... 其他TCC服务

    @PostMapping("/createOrder")
    @GlobalTransactional // 开启Seata全局事务
    public String createOrder(OrderRequest request) {
        // 1. 本地创建订单(状态为“处理中”)
        orderMapper.createTryingOrder(...);
        // 2. 调用库存服务Try接口
        inventoryTccService.tryDecrease(request.getProductId(), request.getCount());
        // 3. 可以继续调用其他服务的TCC Try...
        // 如果所有Try成功,Seata会自动调用各服务的Confirm;如果任一Try失败,则自动调用各服务的Cancel。
        return "订单创建处理中";
    }
}

生产环境必须验证的异常场景

上线前,务必模拟测试以下场景:

测试场景 验证目标
Try 阶段失败 确保所有已执行Try的服务,其Cancel被正确调用,资源释放。
Confirm 阶段重试 确保重复调用Confirm不会导致库存被重复扣减(幂等)。
Cancel 阶段重试 确保重复调用Cancel不会导致库存被重复解冻(幂等)。
空回滚 模拟Try超时后Cancel先到,确保不会误执行业务回滚逻辑。
悬挂 模拟Cancel先执行后,Try请求才到达,确保Try被拒绝执行。

生产最佳实践要点

  1. 幂等必须是“业务级”的:不要依赖不可靠的中间状态(如Redis锁、JVM内存标记)。必须通过数据库唯一约束(如事务日志表)、乐观锁或状态机来实现。
  2. Confirm/Cancel 必须设计为“无副作用”:这两个接口要尽可能做到可重试、不抛业务异常、保证最终能成功。这是TCC模式能够最终一致的基础。
  3. Try 阶段应尽量是短事务:避免在Try中进行长时间的RPC调用或复杂计算,以减少资源锁定时长,提升系统吞吐量。
  4. 建立完善的监控与补偿机制:利用Seata Dashboard监控事务状态;设置事务超时告警;对于极少数长时间未完成的事务,要有人工或自动的补偿任务介入。
  5. 合理设计事务日志表:至少包含xid, branch_id, phase, status, gmt_modified等核心字段,并考虑数据归档策略。

总结

TCC远不止是简单的“Try、Confirm、Cancel”三次调用,它是一套完整的分布式一致性工程体系。其落地的关键,在于精细的业务补偿设计、坚实的幂等控制机制以及对空回滚、悬挂等边界场景的全面防护。

当你的系统涉及资金、库存、核心订单等对一致性要求极高的业务时,TCC模式依然是经过大量实践验证的、最可靠的选择之一。希望本文的实战解析,能帮助你在分布式系统的复杂世界里,更稳健地驾驭数据的一致性。




上一篇:OpenClaw 创始人播客爆料:Meta 与 OpenAI 竞逐,AI 智能体或将重塑 80% 应用生态
下一篇:ScreenPipe:15MB离线AI助手,用自然语言搜索你的电脑活动记录
您需要登录后才可以回帖 登录 | 立即注册

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

GMT+8, 2026-2-23 08:59 , Processed in 0.526956 second(s), 41 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

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