当前位置: 首页 > news >正文

分布式事务中的空回滚与悬挂:原理、场景与解决方案

空回滚(Empty Rollback)和悬挂(Hanging)是 TCC 模式下最典型的两类异常问题,也会出现在 SAGA 等补偿型分布式事务中。我会用通俗的语言拆解这两个概念,结合具体场景说明成因,并给出可落地的解决方案。

一、空回滚(Empty Rollback)

1. 核心定义

当 TCC 的 Cancel 阶段被触发,但对应的 Try 阶段并未实际执行 时,执行 Cancel 操作就会产生“空回滚”——本质是对“不存在的资源”执行回滚补偿。

2. 典型场景(为什么会发生?)

以“扣减库存”的 TCC 为例:

  • 协调者(TM)向库存服务发送 Try 请求,但因网络超时/服务宕机,库存服务未收到 Try 请求(Try 未执行)。
  • 协调者判定 Try 失败,向库存服务发送 Cancel 请求,要求回滚“预占的库存”。
  • 但库存服务根本没有执行过 Try(没有预占任何库存),此时执行 Cancel 就是“空回滚”。

如果不处理空回滚,可能导致:

  • 补偿逻辑报错(如查询不到预占记录);
  • 重复执行 Cancel 时触发无效操作,影响系统稳定性。

3. 解决方案:记录事务状态,空回滚直接返回成功

核心思路:在参与者(RM)侧记录事务ID的执行状态,Cancel 阶段先校验状态,无 Try 记录则直接返回“成功”。

实战代码示例(伪代码)

// 1. 事务状态表(存储每个TCC事务的执行状态)
CREATE TABLE tcc_transaction (id BIGINT PRIMARY KEY AUTO_INCREMENT,tx_id VARCHAR(64) NOT NULL COMMENT '全局事务ID',branch_id VARCHAR(64) NOT NULL COMMENT '分支事务ID',status TINYINT NOT NULL COMMENT '0:未执行Try 1:Try成功 2:Cancel成功 3:Confirm成功',create_time DATETIME,update_time DATETIME,UNIQUE KEY uk_tx_branch (tx_id, branch_id)
);// 2. Try阶段:执行预占 + 记录状态
public boolean tryDeductStock(String txId, String branchId, Long skuId, Integer num) {// 1. 先检查事务状态:避免重复TryTccTransaction tx = getTxByTxIdAndBranchId(txId, branchId);if (tx != null && tx.getStatus() != 0) {return false; // 已有状态,拒绝重复执行}// 2. 执行预占库存(本地事务)try (Connection conn = getConn()) {conn.setAutoCommit(false);// 预占库存(冻结)updateStock(skuId, num, conn);// 记录Try执行状态(0→1)insertOrUpdateTxStatus(txId, branchId, 1, conn);conn.commit();return true;} catch (Exception e) {// 异常则回滚本地事务return false;}
}// 3. Cancel阶段:先校验状态,避免空回滚
public boolean cancelDeductStock(String txId, String branchId) {// 核心:先查事务状态TccTransaction tx = getTxByTxIdAndBranchId(txId, branchId);if (tx == null || tx.getStatus() == 0) {// 未执行Try,空回滚→直接返回成功// 同时记录状态为Cancel成功,避免重复空回滚insertOrUpdateTxStatus(txId, branchId, 2, getConn());return true;}if (tx.getStatus() == 2) {// 已执行过Cancel,幂等返回成功return true;}// 真正执行回滚(释放预占库存)try (Connection conn = getConn()) {conn.setAutoCommit(false);releaseStock(skuId, conn);updateTxStatus(txId, branchId, 2, conn);conn.commit();return true;} catch (Exception e) {return false;}
}

二、悬挂(Hanging)

1. 核心定义

“悬挂”是比空回滚更复杂的异常:Cancel 阶段先执行(空回滚),之后 Try 阶段才迟到执行
此时如果不处理,Try 执行成功后,协调者不会再触发 Cancel,导致“预占的资源”永远无法释放,数据不一致。

2. 典型场景

还是以库存扣减为例:

  1. 协调者发送 Try 请求 → 网络延迟,库存服务未收到(Try 未执行)。
  2. 协调者超时,判定 Try 失败 → 发送 Cancel 请求 → 库存服务执行空回滚(记录状态为 2:Cancel 成功)。
  3. 延迟的 Try 请求到达库存服务 → 此时如果执行 Try,会预占库存,但 Cancel 已经执行完,没有后续补偿 → 库存被永久冻结(悬挂)。

3. 解决方案:Try 阶段校验 Cancel 状态

核心思路:在 Try 阶段执行前,先检查事务状态,如果已经执行过 Cancel,则拒绝执行 Try,从根源避免悬挂。

实战代码补充(在上述 Try 方法中增强校验)

public boolean tryDeductStock(String txId, String branchId, Long skuId, Integer num) {// 1. 先检查事务状态:核心防悬挂逻辑TccTransaction tx = getTxByTxIdAndBranchId(txId, branchId);if (tx != null) {if (tx.getStatus() == 2) {// 已执行Cancel,拒绝执行Try→避免悬挂return false;}if (tx.getStatus() == 1 || tx.getStatus() == 3) {// Try/Confirm已执行,幂等返回成功return true;}}// 2. 执行预占库存 + 记录状态(同之前逻辑)try (Connection conn = getConn()) {conn.setAutoCommit(false);updateStock(skuId, num, conn);insertOrUpdateTxStatus(txId, branchId, 1, conn);conn.commit();return true;} catch (Exception e) {return false;}
}

三、通用解决方案总结(落地关键)

1. 核心手段

问题 核心解决思路 关键动作
空回滚 记录事务状态,Cancel 先校验状态 Cancel 时无 Try 记录 → 直接返回成功
悬挂 Try 阶段先校验 Cancel 状态 Try 时已执行 Cancel → 拒绝执行 Try
幂等性 基于 tx_id + branch_id 做唯一标识 所有阶段重复请求 → 直接返回上次执行结果

2. 框架层面的兜底(无需重复造轮子)

主流 TCC 框架(如 Seata、ByteTCC)已内置空回滚和悬挂的处理逻辑,核心是:

  • 框架自动维护分支事务状态表;
  • Try/Cancel/Confirm 阶段自动校验状态,避免空回滚和悬挂;
  • 提供幂等、重试的默认实现。

Seata TCC 防悬挂配置示例(极简)

@TccAction(name = "deductStockTcc", commitMethod = "confirm", rollbackMethod = "cancel")
public boolean tryDeductStock(String txId, Long skuId, Integer num) {// 业务逻辑:预占库存return true;
}public boolean confirm(String txId, Long skuId, Integer num) {// 确认:扣减实际库存return true;
}public boolean cancel(String txId, Long skuId, Integer num) {// 回滚:释放预占库存return true;
}
// Seata 自动处理:
// 1. Cancel 时无 Try 记录 → 空回滚返回成功;
// 2. Try 时已执行 Cancel → 拒绝执行,防悬挂;
// 3. 所有方法自动幂等。

总结

  1. 空回滚:Cancel 执行时 Try 未执行,核心解决思路是「Cancel 先查状态,无 Try 记录则直接返回成功」;
  2. 悬挂:Cancel 先执行,Try 后到,核心解决思路是「Try 先查 Cancel 状态,已 Cancel 则拒绝执行」;
  3. 落地时优先使用 Seata 等成熟框架,其内置了状态管理和异常处理,避免重复开发。
http://www.jsqmd.com/news/464213/

相关文章:

  • 解密Android14 QS面板事件传递链:从Tile点击到系统服务的完整流程剖析
  • Mozz TCAD丨LDMOS仿真优化与RESURF技术解析
  • IntelliJ IDEA 2021.3.3版本破解插件ja-netfilter-all的安装与激活指南
  • STM32无源蜂鸣器音乐盒:用PWM实现《小星星》完整曲谱(HAL库版)
  • RAG-03查询与检索模块
  • RAG-04响应与生成模块
  • 工业物联网实战:用Python+RS485温湿度传感器搭建环境监测系统
  • Python自动化实战:微信小程序每日签到脚本开发指南
  • Java PTA实战:面向对象设计中的Shape继承与多态应用
  • 五种高效工具对比:Shp 数据导入 PostGIS 的最佳实践指南
  • 反激式电源设计避坑指南:从MATLAB仿真看PID参数对输出电压稳定的影响
  • FastAPI + Uvicorn 实战:5分钟搭建高性能异步API(含性能优化技巧)
  • TVS二极管在高速信号保护中的关键参数与选型策略
  • 深入解析APB总线:从读写时序到高效验证实践
  • 深入解析I2S总线协议:数字音频接口的核心技术
  • NVIDIA校招笔试通关秘籍:Board Design Engineer必考的5大电路题型解析
  • UE5 Break Hit Result节点详解:从基础到高级用法(含避坑指南)
  • Zotero7必装插件:better-notes自定义学术笔记模板全攻略(附代码)
  • [阵列信号处理]近场DOA估计算法-2DMUSIC方法实战:从理论推导到MATLAB仿真
  • 车间巡检不踩坑!2026设备巡检APP口碑排行榜
  • 计算机毕业设计springboot基于springboot的社会公益平台 基于Spring Boot的志愿服务与公益资源管理系统 Spring Boot框架下的爱心捐赠与社区互助平台
  • DETR vs 传统目标检测:为什么Transformer正在改变游戏规则?
  • 微前端qiankun中子应用字体图标加载失败的深度解析与解决方案
  • C#与PLC通讯浮点数处理全攻略:ModbusTCP下的字节转换实战(含NModbus示例)
  • C++二维数组实战:5种常见矩阵操作保姆级教程(附完整代码)
  • 通义千问Max vs Long模型怎么选?实测对比长文本处理与多语言支持
  • 车载开发实战:CarLife、CarPlay、HiCar三大方案对比与选型指南
  • 计算机毕业设计springboot校园周边美食探索及分享平台 基于Spring Boot的大学城美食推荐与互动社区平台 Spring Boot驱动的校园周边饮食文化探索系统
  • 微信小程序跨平台兼容性避坑:当Android倔强拒绝你的HTTPS请求时
  • 从零上手WT588F02B:语音固件制作与开发板实战测试指南