当你在电商下单时,钱到底怎么扣?用TCC模式解决分布式事务难题

前言:一个订单背后的技术修罗场

假设你正在开发一个电商系统,用户下单时需要同时完成:

  1. 创建订单
  2. 扣减库存
  3. 冻结优惠券
  4. 扣减账户余额

这四个操作分布在不同的微服务中,如何保证它们要么全部成功,要么全部回滚?这就是分布式事务要解决的核心问题。今天我们要讨论的TCC模式,正是解决这类问题的经典方案。

一、分布式事务的常见解法

1.1 传统方案的局限性

我们先看几种常见方案的对比:

方案 原理 适用场景 缺点
2PC 事务协调器统一决策 数据库层面事务 同步阻塞、性能差
本地消息表 消息日志+定时任务补偿 最终一致性场景 实现复杂、数据可能不一致
Saga 反向操作补偿 长事务场景 业务侵入性强、补偿逻辑难写
TCC 业务资源预留+确认/取消 高并发场景 需要业务改造

1.2 TCC的独特优势

TCC(Try-Confirm-Cancel)通过业务层面的资源预留,解决了传统方案在以下场景的痛点:

  • 跨多个业务系统的长事务
  • 需要快速释放资源的场景
  • 对数据一致性要求高的金融交易

二、TCC模式的核心原理

2.1 三个阶段拆解事务

阶段解析:

  1. Try:预留业务资源(如冻结库存、预扣金额)
  2. Confirm:确认执行业务操作(真正扣减资源)
  3. 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 必须实现的三大保障

  1. 幂等性控制:每个阶段都要支持重复调用

    java 复制代码
    // 通过状态判断实现幂等
    if (order.getStatus() != OrderStatus.TRYING) {
        throw new IllegalStateException("订单状态异常");
    }
  2. 空回滚防护:防止未执行Try却收到Cancel

    java 复制代码
    public boolean cancelLockStock(String skuId) {
        LockRecord record = lockRecordDao.selectBySku(skuId);
        if (record == null) {
            // 记录异常日志但不阻断流程
            log.warn("空回滚警告:skuId={}", skuId);
            return true;
        }
        // 正常处理逻辑...
    }
  3. 防悬挂控制:Cancel先于Try到达

    java 复制代码
    public 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 最适合的场景

  1. 需要强一致性的金融交易
  2. 库存、优惠券等资源敏感操作
  3. 跨多个业务系统的长流程操作

6.2 需要避开的坑

  1. 业务改造成本高:每个操作都要实现三个接口
  2. 开发复杂度陡增:要考虑各种异常情况
  3. 不适合简单事务:简单的本地事务不要用TCC

七、总结:TCC的正确打开方式

正确姿势

  • 先评估业务场景是否真的需要
  • 设计阶段明确资源预留方式
  • 实现完善的异常处理机制
  • 配合监控系统实时跟踪事务状态

常见误区

  • 把TCC当作银弹到处使用
  • 忽略幂等性和防悬挂控制
  • 没有配套的监控和报警系统
相关推荐
开始学java5 分钟前
继承树追溯
后端
何中应10 分钟前
分布式事务的两种解决方案
java·分布式·后端
SimonKing1 小时前
无需重启!动态修改日志级别的神技,运维开发都哭了
java·后端·程序员
架构精进之路1 小时前
多智能体系统不是银弹
后端·架构·aigc
涡能增压发动积2 小时前
MySQL数据库为何逐渐黯淡,PostgreSQL为何能新王登基
人工智能·后端
架构精进之路2 小时前
多智能体系统架构解析
后端·架构·ai编程
Java中文社群2 小时前
重磅!Ollama发布UI界面,告别命令窗口!
java·人工智能·后端
程序员清风2 小时前
程序员代码有Bug别怕,人生亦是如此!
java·后端·面试
就是帅我不改3 小时前
告别996!高可用低耦合架构揭秘:SpringBoot + RabbitMQ 让订单系统不再崩
java·后端·面试