分布式事务深度:TCC空回滚、悬挂与Saga补偿风暴的根因与解法

大家好,我是小耶,写功课只是为了我踩过的坑,你们别再踩了!

分布式事务有一个诡异的定律:测试环境跑一百遍都没事,上线第一分钟就翻车。

TCC的代码在测试环境跑了几百遍,Try、Confirm、Cancel三个接口都写得漂漂亮亮。上线之后,账对不上了。查了半天日志才发现------Cancel执行了,但Try根本没执行过。

这就是空回滚。今天把TCC和Saga的三个核心陷阱彻底拆开讲清楚。

先搞懂几个词:

TCC(Try-Confirm-Cancel) :分布式事务的一种实现模式。Try做资源预留,Confirm确认提交,Cancel取消回滚。相比2PC的同步阻塞,TCC不长时间持有锁,性能更好。

空回滚:Cancel执行了,但Try根本没执行过。

悬挂:Try在Cancel之后才到达,资源被冻结但没有对应的Confirm或Cancel来处理。

事务记录表:TCC解决空回滚和悬挂的核心工具。Try执行前先插入一条记录,Cancel执行前先查这条记录是否存在。

补偿风暴:Saga模式下,补偿操作级联触发,导致系统被补偿请求淹没。

一、TCC空回滚:Cancel执行了,但Try从没来过

时序成因

空回滚的触发链路是这样的:

  1. 调用方发起Try请求

  2. Try请求在网络中拥塞,超时了

  3. 调用方认为Try失败,触发Cancel

  4. Cancel执行,但Try根本没有执行过

  5. Try请求最终到达了------但Cancel已经执行完了

为什么会出问题?

Cancel的逻辑是"释放Try阶段预留的资源"。但如果Try根本没有执行,就没有资源被预留。Cancel执行时,会尝试"解冻"一笔不存在的冻结金额------如果代码写得不严谨,可能直接把账户余额给改了,造成数据不一致。

解决方案:事务记录表

核心思路是:Try执行前先记录,Cancel执行前先检查。

bash 复制代码
CREATE TABLE tcc_transaction (
    tx_id VARCHAR(64) PRIMARY KEY COMMENT '事务ID',
    status VARCHAR(16) NOT NULL COMMENT 'INIT/TRIED/CONFIRMED/CANCELLED',
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);
bash 复制代码
// Try:先标记,再冻结
public void try(String txId) {
    insert(txId, "INIT");
    freezeAmount(100);
    updateStatus(txId, "TRIED");
}

// Cancel:先检查Try是否执行过
public void cancel(String txId) {
    String status = queryStatus(txId);
    if (status == null) {
        // 空回滚:Try从未执行过,记录并返回成功
        insert(txId, "CANCELLED");
        return;
    }
    if ("CANCELLED".equals(status)) {
        // 幂等:已经回滚过了
        return;
    }
    unfreezeAmount(100);
    updateStatus(txId, "CANCELLED");
}

关键点:Cancel识别到空回滚后,不能直接返回成功------必须先记录一条CANCELLED记录。否则悬挂问题会紧随其后。

二、TCC悬挂:Cancel已经执行完了,Try才姗姗来迟

时序成因

悬挂的触发链路:

  1. Try请求在网络中拥塞,超时了

  2. 调用方触发Cancel,Cancel执行成功

  3. Try请求最终到达,开始执行业务操作

  4. 资源被冻结了,但事务管理器认为这个事务已经结束,不会再发Confirm或Cancel

  5. 冻结的资源永远无法释放

空回滚和悬挂是一对孪生问题。 空回滚是"Cancel先到了",悬挂是"Try后到了"。两者都源于网络延迟导致的乱序。

解决方案

在Cancel空回滚返回成功之前,先记录该事务已回滚。Try接口在执行前检查这条记录:

bash 复制代码
// Try:检查是否已经被Cancel过
public void try(String txId) {
    String status = queryStatus(txId);
    if ("CANCELLED".equals(status)) {
        // 悬挂:Cancel已经执行过了,拒绝执行Try
        throw new RuntimeException("事务已被取消,拒绝执行Try");
    }
    insert(txId, "INIT");
    freezeAmount(100);
    updateStatus(txId, "TRIED");
}

空回滚和悬挂的完整防御需要三个检查:

场景 检查时机 检查内容 处理方式
空回滚 Cancel执行前 Try是否执行过 未执行过 → 记录CANCELLED,返回成功
悬挂 Try执行前 是否已被Cancel 已被Cancel → 拒绝执行Try
幂等 Confirm/Cancel执行前 是否已执行过 已执行过 → 直接返回成功

三、幂等:Confirm和Cancel可能被重复调用

TCC的三个接口(Try/Confirm/Cancel)都可能被重复调用。原因有两个:网络重试机制、事务管理器故障恢复后的重试。

解决方案:Confirm和Cancel执行前先查询事务状态。如果状态已经是CONFIRMED或CANCELLED,直接返回成功。

bash 复制代码
public void confirm(String txId) {
    String status = queryStatus(txId);
    if ("CONFIRMED".equals(status)) {
        return; // 幂等
    }
    commitAmount(100);
    updateStatus(txId, "CONFIRMED");
}

四、Saga补偿风暴:补偿链越长,风险越大

Saga模式和TCC不同。TCC是"预留+确认",Saga是"直接执行+反向补偿"。每个本地事务执行后立即提交,失败时按反向顺序执行补偿操作。

补偿风暴的触发链路

Saga流程是:LT1 → LT2 → LT3 → ... → LTn。如果LTn失败,需要按反向顺序执行:CTn → CTn-1 → ... → CT2 → CT1。

补偿风暴发生在补偿操作本身也失败时。如果CTn-1补偿失败,事务管理器会重试。如果重试多次仍然失败,可能需要人工介入。更严重的是,如果补偿操作触发了新的Saga流程(比如补偿操作本身是一个跨服务调用),补偿链会级联扩散。

一个真实的场景:电商订单流程涉及下单、扣库存、扣积分、发优惠券、通知物流。如果第5步失败,需要依次补偿前4步。但如果"退优惠券"这个补偿操作本身也失败了(比如优惠券服务不可用),补偿链就卡住了。此时如果第5步的补偿触发了一个新的Saga流程,整个系统可能被补偿请求淹没。

解决方案

方案一:补偿操作加限流。 给补偿操作设置独立的线程池和限流阈值,避免补偿请求挤占正常业务资源。

方案二:补偿操作加退避。 补偿失败后不立即重试,而是等待一段时间后再试。重试间隔逐次递增。

方案三:补偿操作独立幂等。 补偿操作必须幂等------重复执行结果一致。这样即使重试多次,也不会造成数据错误。

方案四:设置补偿超时和人工介入阈值。 补偿操作超过一定次数后,转为人工处理,避免系统被无限重试拖垮。

五、小结

TCC的空回滚、悬挂、幂等三个问题有一个共同的根因:网络延迟导致的时序乱序 。空回滚是Cancel先到了,悬挂是Try后到了,幂等是重复请求。解决方案的核心是事务记录表------Try执行前记录,Cancel执行前检查,Confirm/Cancel执行前查询状态。Saga的补偿风暴则源于补偿链的级联效应,需要通过限流、退避、独立幂等和人工介入阈值来控制。

小耶在手,SQL 不愁

还有什么想了解的,欢迎留言!小耶一定知无不言言无不尽......我们下次见~

相关推荐
天行健,君子而铎1 小时前
知形-数据库风险监测系统:非侵入式、可溯源、差异化的数据库安全方案
网络·数据库·安全
执笔为剑1 小时前
2.1.2-2、V8R3迁移到V8R6-迁移介绍
数据库
hiahiahia1231 小时前
Message 和对话
服务器·前端·数据库
QEasycloud1 小时前
BOM接口抛转失败钉钉通知策略实战教程:MySQL异常日志到钉钉消息的端到端配置
android·mysql·钉钉
又尔D.2 小时前
MySQL下载安装
数据库·mysql
Navicat中国2 小时前
Navicat 数据脱敏打造安全可用的仿真数据环境
数据库·数据安全·navicat·数据脱敏·合规
夜雪一千2 小时前
MySQL 删除 JSON 类型的数据实战
mysql·adb·json
Elastic 中国社区官方博客2 小时前
Elastic 线下 Meetup 将于 2026 年 12 月 19 号下午在西安举行
大数据·数据库·elasticsearch·搜索引擎·ai·数据分析·全文检索