前言:一个订单背后的技术修罗场
假设你正在开发一个电商系统,用户下单时需要同时完成:
- 创建订单
- 扣减库存
- 冻结优惠券
- 扣减账户余额
这四个操作分布在不同的微服务中,如何保证它们要么全部成功,要么全部回滚?这就是分布式事务要解决的核心问题。今天我们要讨论的TCC模式,正是解决这类问题的经典方案。
一、分布式事务的常见解法
1.1 传统方案的局限性
我们先看几种常见方案的对比:
方案 | 原理 | 适用场景 | 缺点 |
---|---|---|---|
2PC | 事务协调器统一决策 | 数据库层面事务 | 同步阻塞、性能差 |
本地消息表 | 消息日志+定时任务补偿 | 最终一致性场景 | 实现复杂、数据可能不一致 |
Saga | 反向操作补偿 | 长事务场景 | 业务侵入性强、补偿逻辑难写 |
TCC | 业务资源预留+确认/取消 | 高并发场景 | 需要业务改造 |
1.2 TCC的独特优势
TCC(Try-Confirm-Cancel)通过业务层面的资源预留,解决了传统方案在以下场景的痛点:
- 跨多个业务系统的长事务
- 需要快速释放资源的场景
- 对数据一致性要求高的金融交易
二、TCC模式的核心原理
2.1 三个阶段拆解事务

阶段解析:
- Try:预留业务资源(如冻结库存、预扣金额)
- Confirm:确认执行业务操作(真正扣减资源)
- Cancel:取消预留(释放冻结的资源)
2.2 举个真实的例子
假设用户下单购买Switch游戏机:
- Try阶段 :
- 订单服务:生成待支付订单
- 库存服务:冻结1台库存
- 账户服务:冻结用户账户2000元
- Confirm阶段 :
- 订单状态变更为已支付
- 实际扣减库存
- 实际扣除账户金额
- Cancel阶段 :
- 删除订单记录
- 释放冻结的库存
- 解冻账户金额
三、代码实现:从理论到实践
3.1 定义TCC接口
java
public interface OrderServiceTCC {
@Transactional
@RequestMappging("/try")
boolean tryCreateOrder(OrderDTO order);
@Transactional
@RequestMapping("/confirm")
boolean confirmCreateOrder(Long orderId);
@Transactional
@RequestMapping("/cancel")
boolean cancelCreateOrder(Long orderId);
}
3.2 Try阶段实现
java
@Service
public class OrderServiceImpl implements OrderServiceTCC {
// 预创建订单(状态为待确认)
@Override
public boolean tryCreateOrder(OrderDTO order) {
OrderEntity entity = new OrderEntity();
entity.setStatus(OrderStatus.TRYING);
entity.setAmount(order.getAmount());
orderMapper.insert(entity);
// 调用库存服务的try接口
inventoryService.tryLockStock(order.getSkuId(), order.getQuantity());
// 调用账户服务的try接口
accountService.tryFreezeBalance(order.getUserId(), order.getAmount());
return true;
}
}
3.3 Confirm阶段实现
java
@Override
public boolean confirmCreateOrder(Long orderId) {
OrderEntity order = orderMapper.selectById(orderId);
// 幂等性检查
if (order.getStatus() == OrderStatus.CONFIRMED) {
return true;
}
// 正式确认订单
order.setStatus(OrderStatus.CONFIRMED);
orderMapper.updateById(order);
// 实际扣减库存(RPC调用)
inventoryService.confirmLockStock(order.getSkuId());
// 实际扣款(RPC调用)
accountService.confirmDeduction(order.getUserId());
return true;
}
3.4 Cancel阶段实现
java
@Override
public boolean cancelCreateOrder(Long orderId) {
OrderEntity order = orderMapper.selectById(orderId);
// 幂等性检查
if (order.getStatus() == OrderStatus.CANCELLED) {
return true;
}
// 恢复订单状态
order.setStatus(OrderStatus.CANCELLED);
orderMapper.updateById(order);
// 释放库存(RPC调用)
inventoryService.cancelLockStock(order.getSkuId());
// 解冻金额(RPC调用)
accountService.cancelDeduction(order.getUserId());
return true;
}
四、异常处理:TCC的关键战场
4.1 典型异常场景处理

4.2 必须实现的三大保障
-
幂等性控制:每个阶段都要支持重复调用
java// 通过状态判断实现幂等 if (order.getStatus() != OrderStatus.TRYING) { throw new IllegalStateException("订单状态异常"); }
-
空回滚防护:防止未执行Try却收到Cancel
javapublic boolean cancelLockStock(String skuId) { LockRecord record = lockRecordDao.selectBySku(skuId); if (record == null) { // 记录异常日志但不阻断流程 log.warn("空回滚警告:skuId={}", skuId); return true; } // 正常处理逻辑... }
-
防悬挂控制:Cancel先于Try到达
javapublic boolean tryLockStock(String skuId, int quantity) { if (lockRecordDao.isCancelled(skuId)) { throw new TryAfterCancelException("禁止在Cancel后执行Try"); } // 正常处理逻辑... }
五、实战中的进阶技巧
5.1 超时控制策略
java
// 在Try阶段记录超时时间
order.setExpireTime(LocalDateTime.now().plusMinutes(15));
// 定时任务扫描过期订单
@Scheduled(cron = "0 */5 * * * ?")
public void handleTimeoutOrders() {
List<Order> orders = orderMapper.selectExpiredOrders();
orders.forEach(order -> {
// 自动触发Cancel操作
orderService.cancelCreateOrder(order.getId());
});
}
5.2 异步化改造
java
// 使用MQ异步执行Confirm
public void afterTrySuccess(Long orderId) {
rocketMQTemplate.sendAsync("ORDER_CONFIRM_TOPIC", orderId, new SendCallback() {
@Override
public void onSuccess(SendResult sendResult) {
log.info("Confirm消息发送成功");
}
@Override
public void onException(Throwable e) {
// 加入重试队列
retryQueue.add(orderId);
}
});
}
六、TCC的适用场景与局限性
6.1 最适合的场景
- 需要强一致性的金融交易
- 库存、优惠券等资源敏感操作
- 跨多个业务系统的长流程操作
6.2 需要避开的坑
- 业务改造成本高:每个操作都要实现三个接口
- 开发复杂度陡增:要考虑各种异常情况
- 不适合简单事务:简单的本地事务不要用TCC
七、总结:TCC的正确打开方式
正确姿势:
- 先评估业务场景是否真的需要
- 设计阶段明确资源预留方式
- 实现完善的异常处理机制
- 配合监控系统实时跟踪事务状态
常见误区:
- 把TCC当作银弹到处使用
- 忽略幂等性和防悬挂控制
- 没有配套的监控和报警系统