只要谈到微服务架构,分布式事务的一致性就是个无法回避的核心议题。传统的本地事务(@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被拒绝执行。 |
生产最佳实践要点
- 幂等必须是“业务级”的:不要依赖不可靠的中间状态(如Redis锁、JVM内存标记)。必须通过数据库唯一约束(如事务日志表)、乐观锁或状态机来实现。
- Confirm/Cancel 必须设计为“无副作用”:这两个接口要尽可能做到可重试、不抛业务异常、保证最终能成功。这是TCC模式能够最终一致的基础。
- Try 阶段应尽量是短事务:避免在Try中进行长时间的RPC调用或复杂计算,以减少资源锁定时长,提升系统吞吐量。
- 建立完善的监控与补偿机制:利用Seata Dashboard监控事务状态;设置事务超时告警;对于极少数长时间未完成的事务,要有人工或自动的补偿任务介入。
- 合理设计事务日志表:至少包含
xid, branch_id, phase, status, gmt_modified等核心字段,并考虑数据归档策略。
总结
TCC远不止是简单的“Try、Confirm、Cancel”三次调用,它是一套完整的分布式一致性工程体系。其落地的关键,在于精细的业务补偿设计、坚实的幂等控制机制以及对空回滚、悬挂等边界场景的全面防护。
当你的系统涉及资金、库存、核心订单等对一致性要求极高的业务时,TCC模式依然是经过大量实践验证的、最可靠的选择之一。希望本文的实战解析,能帮助你在分布式系统的复杂世界里,更稳健地驾驭数据的一致性。