当你在电商下单时,钱到底怎么扣?用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当作银弹到处使用
  • 忽略幂等性和防悬挂控制
  • 没有配套的监控和报警系统
相关推荐
JH30731 小时前
【SpringBoot】SpringBoot中使用AOP实现日志记录功能
java·spring boot·后端
anqi272 小时前
在sheel中运行Spark
大数据·开发语言·分布式·后端·spark
程序员小刚2 小时前
基于SpringBoot + Vue 的作业管理系统
vue.js·spring boot·后端
问道飞鱼3 小时前
【Springboot知识】Springboot计划任务Schedule详解
java·spring boot·后端·schedule
o0o0o0D4 小时前
jmeter 执行顺序和组件作用域
后端
神仙别闹4 小时前
基于ASP.NET+MySQL实现待办任务清单系统
后端·mysql·asp.net
程序员buddha4 小时前
【Spring Boot】Spring Boot + Thymeleaf搭建mvc项目
spring boot·后端·mvc
okok__TXF6 小时前
spring详解-循环依赖的解决
java·后端·spring
二十雨辰7 小时前
[学成在线]23-面试题总结
java·后端
神马都会亿点点的毛毛张8 小时前
【SpringBoot教程】SpringBoot自定义注解与AOP实现切面日志
java·spring boot·后端·spring·spring aop·aspectj