分布式事务订单库存解决方案:让数据一致性无懈可击!🔄

73 阅读9分钟

标题: 分布式事务太难?TCC、Saga、本地消息表一网打尽!
副标题: 从理论到实践,订单库存一致性全攻略


🎬 开篇:一次灾难性的数据不一致

电商大促当天:

用户下单 -> 订单服务创建订单成功 ✅
         -> 库存服务扣减库存... 网络超时 ❌
         
结果:
- 订单创建成功(已生成订单号)
- 库存未扣减(还是原来的数量)
- 用户支付成功
- 仓库发不了货(库存数据不对)

客服:为什么我有订单但是库存没扣?😱
运营:数据对不上,怎么发货?😰
老板:这个月奖金全扣!💀

排查发现:
- 订单服务和库存服务是独立的微服务
- 使用了本地事务,无法保证一致性
- 没有任何补偿机制
- 数据不一致导致库存混乱

损失:
- 库存混乱:无法统计
- 财务对账失败:通宵加班
- 用户投诉:爆炸
- 技术债:3个月才修复完

教训:分布式系统必须解决事务一致性问题!

🤔 什么是分布式事务?

想象你和朋友之间转账:

  • 本地事务: 你银行账户-100,朋友账户+100(同一个银行,一次完成)
  • 分布式事务: 你的支付宝-100,朋友的微信+100(不同系统,需要协调)

分布式事务:跨多个服务/数据库的事务,要么都成功,要么都失败!


📚 知识地图

分布式事务解决方案
├── 💪 2PC/3PC(强一致性,性能差)
├── 🎯 TCC(Try-Confirm-Cancel)
├── 📜 Saga(长事务,补偿机制)
├── 📨 本地消息表(推荐!)
├── 🔔 事务消息(RocketMQ)
└── 📊 最大努力通知

🎯 方案1:TCC模式

🌰 生活中的例子

订酒店房间:

  • Try(尝试): 预订房间,锁定房间(房间标记为"已预订")
  • Confirm(确认): 支付成功,确认入住(房间分配给你)
  • Cancel(取消): 支付失败,取消预订(释放房间)

💻 技术实现

/**
 * TCC模式实现
 */

// ========== 订单服务 ==========

/**
 * 订单TCC接口
 */
public interface OrderTccService {
    
    /**
     * Try:创建预订单
     */
    @TwoPhaseBusinessAction(
        name = "orderTccService",
        commitMethod = "confirmOrder",
        rollbackMethod = "cancelOrder"
    )
    String tryCreateOrder(
        @BusinessActionContextParameter(paramName = "orderDTO") OrderDTO orderDTO
    );
    
    /**
     * Confirm:确认订单
     */
    boolean confirmOrder(BusinessActionContext context);
    
    /**
     * Cancel:取消订单
     */
    boolean cancelOrder(BusinessActionContext context);
}

/**
 * 订单TCC实现
 */
@Service
public class OrderTccServiceImpl implements OrderTccService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private StockTccService stockTccService;  // Dubbo远程调用
    
    /**
     * Try阶段:创建预订单
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public String tryCreateOrder(OrderDTO orderDTO) {
        log.info("【Try阶段】创建预订单:{}", orderDTO);
        
        // 1. 创建预订单(状态为TRYING)
        Order order = Order.builder()
            .orderNo(generateOrderNo())
            .userId(orderDTO.getUserId())
            .productId(orderDTO.getProductId())
            .quantity(orderDTO.getQuantity())
            .totalAmount(orderDTO.getTotalAmount())
            .status(OrderStatus.TRYING)  // ⚡ TRYING状态
            .createTime(LocalDateTime.now())
            .build();
        
        orderMapper.insert(order);
        
        // 2. 调用库存服务Try(预扣库存)
        boolean stockResult = stockTccService.tryDeductStock(
            orderDTO.getProductId(),
            orderDTO.getQuantity(),
            order.getOrderNo()
        );
        
        if (!stockResult) {
            throw new BusinessException("库存不足");
        }
        
        log.info("【Try阶段】预订单创建成功:orderNo={}", order.getOrderNo());
        
        return order.getOrderNo();
    }
    
    /**
     * Confirm阶段:确认订单
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean confirmOrder(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderDTO", OrderDTO.class)
            .getOrderNo();
        
        log.info("【Confirm阶段】确认订单:orderNo={}", orderNo);
        
        // 1. 查询预订单
        Order order = orderMapper.selectByOrderNo(orderNo);
        
        if (order == null) {
            log.warn("订单不存在:orderNo={}", orderNo);
            return true;  // 幂等性:已处理过
        }
        
        if (order.getStatus() == OrderStatus.CONFIRMED) {
            log.warn("订单已确认:orderNo={}", orderNo);
            return true;  // 幂等性
        }
        
        // 2. 更新订单状态为已确认
        order.setStatus(OrderStatus.CONFIRMED);
        order.setConfirmTime(LocalDateTime.now());
        orderMapper.updateById(order);
        
        log.info("【Confirm阶段】订单确认成功:orderNo={}", orderNo);
        
        return true;
    }
    
    /**
     * Cancel阶段:取消订单
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cancelOrder(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderDTO", OrderDTO.class)
            .getOrderNo();
        
        log.info("【Cancel阶段】取消订单:orderNo={}", orderNo);
        
        // 1. 查询预订单
        Order order = orderMapper.selectByOrderNo(orderNo);
        
        if (order == null) {
            log.warn("订单不存在:orderNo={}", orderNo);
            return true;  // 幂等性
        }
        
        if (order.getStatus() == OrderStatus.CANCELLED) {
            log.warn("订单已取消:orderNo={}", orderNo);
            return true;  // 幂等性
        }
        
        // 2. 更新订单状态为已取消
        order.setStatus(OrderStatus.CANCELLED);
        order.setCancelTime(LocalDateTime.now());
        orderMapper.updateById(order);
        
        log.info("【Cancel阶段】订单取消成功:orderNo={}", orderNo);
        
        return true;
    }
}

// ========== 库存服务 ==========

/**
 * 库存TCC接口
 */
public interface StockTccService {
    
    /**
     * Try:预扣库存
     */
    @TwoPhaseBusinessAction(
        name = "stockTccService",
        commitMethod = "confirmDeduct",
        rollbackMethod = "cancelDeduct"
    )
    boolean tryDeductStock(
        @BusinessActionContextParameter(paramName = "productId") Long productId,
        @BusinessActionContextParameter(paramName = "quantity") Integer quantity,
        @BusinessActionContextParameter(paramName = "orderNo") String orderNo
    );
    
    /**
     * Confirm:确认扣减
     */
    boolean confirmDeduct(BusinessActionContext context);
    
    /**
     * Cancel:取消扣减(回滚)
     */
    boolean cancelDeduct(BusinessActionContext context);
}

/**
 * 库存TCC实现
 */
@Service
public class StockTccServiceImpl implements StockTccService {
    
    @Autowired
    private StockMapper stockMapper;
    
    @Autowired
    private StockFreezeMapper freezeMapper;
    
    /**
     * Try阶段:冻结库存
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean tryDeductStock(Long productId, Integer quantity, String orderNo) {
        log.info("【Try阶段】冻结库存:productId={}, quantity={}, orderNo={}", 
            productId, quantity, orderNo);
        
        // 1. 检查是否已经冻结过(幂等性)
        StockFreeze existFreeze = freezeMapper.selectByOrderNo(orderNo);
        if (existFreeze != null) {
            log.warn("库存已冻结:orderNo={}", orderNo);
            return true;
        }
        
        // 2. 查询库存
        Stock stock = stockMapper.selectByProductId(productId);
        
        if (stock == null || stock.getAvailableStock() < quantity) {
            log.warn("库存不足:productId={}, available={}, need={}", 
                productId, stock != null ? stock.getAvailableStock() : 0, quantity);
            return false;
        }
        
        // 3. ⚡ 扣减可用库存,增加冻结库存
        int updated = stockMapper.freezeStock(productId, quantity);
        
        if (updated == 0) {
            log.error("冻结库存失败:productId={}", productId);
            return false;
        }
        
        // 4. 记录冻结记录
        StockFreeze freeze = StockFreeze.builder()
            .orderNo(orderNo)
            .productId(productId)
            .quantity(quantity)
            .status(FreezeStatus.TRYING)
            .createTime(LocalDateTime.now())
            .build();
        
        freezeMapper.insert(freeze);
        
        log.info("【Try阶段】库存冻结成功:orderNo={}", orderNo);
        
        return true;
    }
    
    /**
     * Confirm阶段:确认扣减
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean confirmDeduct(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderNo", String.class);
        
        log.info("【Confirm阶段】确认扣减库存:orderNo={}", orderNo);
        
        // 1. 查询冻结记录
        StockFreeze freeze = freezeMapper.selectByOrderNo(orderNo);
        
        if (freeze == null) {
            log.warn("冻结记录不存在:orderNo={}", orderNo);
            return true;
        }
        
        if (freeze.getStatus() == FreezeStatus.CONFIRMED) {
            log.warn("库存已确认扣减:orderNo={}", orderNo);
            return true;  // 幂等性
        }
        
        // 2. 更新冻结记录状态
        freeze.setStatus(FreezeStatus.CONFIRMED);
        freeze.setConfirmTime(LocalDateTime.now());
        freezeMapper.updateById(freeze);
        
        // 注意:库存已在Try阶段扣减,这里只需更新状态
        
        log.info("【Confirm阶段】库存扣减确认成功:orderNo={}", orderNo);
        
        return true;
    }
    
    /**
     * Cancel阶段:回滚扣减
     */
    @Override
    @Transactional(rollbackFor = Exception.class)
    public boolean cancelDeduct(BusinessActionContext context) {
        String orderNo = context.getActionContext("orderNo", String.class);
        Long productId = context.getActionContext("productId", Long.class);
        Integer quantity = context.getActionContext("quantity", Integer.class);
        
        log.info("【Cancel阶段】回滚库存:orderNo={}, productId={}, quantity={}", 
            orderNo, productId, quantity);
        
        // 1. 查询冻结记录
        StockFreeze freeze = freezeMapper.selectByOrderNo(orderNo);
        
        if (freeze == null) {
            log.warn("冻结记录不存在:orderNo={}", orderNo);
            return true;
        }
        
        if (freeze.getStatus() == FreezeStatus.CANCELLED) {
            log.warn("库存已回滚:orderNo={}", orderNo);
            return true;  // 幂等性
        }
        
        // 2. ⚡ 释放冻结库存
        int updated = stockMapper.unfreezeStock(productId, quantity);
        
        if (updated == 0) {
            log.error("释放库存失败:productId={}", productId);
            return false;
        }
        
        // 3. 更新冻结记录状态
        freeze.setStatus(FreezeStatus.CANCELLED);
        freeze.setCancelTime(LocalDateTime.now());
        freezeMapper.updateById(freeze);
        
        log.info("【Cancel阶段】库存回滚成功:orderNo={}", orderNo);
        
        return true;
    }
}

/**
 * Mapper实现
 */
@Mapper
public interface StockMapper {
    
    /**
     * 冻结库存
     */
    @Update("UPDATE stock " +
            "SET available_stock = available_stock - #{quantity}, " +
            "    frozen_stock = frozen_stock + #{quantity} " +
            "WHERE product_id = #{productId} " +
            "AND available_stock >= #{quantity}")
    int freezeStock(@Param("productId") Long productId, 
                   @Param("quantity") Integer quantity);
    
    /**
     * 解冻库存
     */
    @Update("UPDATE stock " +
            "SET available_stock = available_stock + #{quantity}, " +
            "    frozen_stock = frozen_stock - #{quantity} " +
            "WHERE product_id = #{productId}")
    int unfreezeStock(@Param("productId") Long productId, 
                     @Param("quantity") Integer quantity);
}

/**
 * 数据库表结构
 */
CREATE TABLE stock (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    product_id BIGINT NOT NULL,
    total_stock INT NOT NULL DEFAULT 0 COMMENT '总库存',
    available_stock INT NOT NULL DEFAULT 0 COMMENT '可用库存',
    frozen_stock INT NOT NULL DEFAULT 0 COMMENT '冻结库存',
    UNIQUE KEY uk_product_id (product_id)
) COMMENT='库存表';

CREATE TABLE stock_freeze (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    order_no VARCHAR(50) NOT NULL,
    product_id BIGINT NOT NULL,
    quantity INT NOT NULL,
    status VARCHAR(20) NOT NULL COMMENT 'TRYING/CONFIRMED/CANCELLED',
    create_time DATETIME NOT NULL,
    confirm_time DATETIME,
    cancel_time DATETIME,
    UNIQUE KEY uk_order_no (order_no),
    INDEX idx_product_id (product_id)
) COMMENT='库存冻结记录表';

/**
 * 优点:
 * ✅ 强一致性
 * ✅ 实时性好
 * ✅ 无需人工干预
 * 
 * 缺点:
 * ❌ 实现复杂(每个操作都要3个方法)
 * ❌ 性能开销大(需要3次调用)
 * ❌ 代码侵入性强
 * 
 * 适用场景:
 * ✅ 对一致性要求极高
 * ✅ 业务逻辑相对简单
 * ✅ 交易类场景(支付、转账)
 */

📨 方案2:本地消息表(推荐!)

🌰 生活中的例子

寄快递:

  • 写纸条: "帮我转账100元给张三"(本地消息)
  • 放进包裹: 和订单一起打包(同一个事务)
  • 快递送达: 对方收到包裹和纸条(消息送达)
  • 执行任务: 对方看到纸条,转账给张三(消费消息)

💻 技术实现

/**
 * 本地消息表方案
 */

// ========== 订单服务 ==========

/**
 * 本地消息表
 */
CREATE TABLE local_message (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_id VARCHAR(50) NOT NULL COMMENT '消息ID',
    message_type VARCHAR(50) NOT NULL COMMENT '消息类型',
    message_body TEXT NOT NULL COMMENT '消息内容',
    status VARCHAR(20) NOT NULL DEFAULT 'PENDING' COMMENT '状态:PENDING/SUCCESS/FAIL',
    retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
    max_retry INT NOT NULL DEFAULT 3 COMMENT '最大重试次数',
    next_retry_time DATETIME COMMENT '下次重试时间',
    create_time DATETIME NOT NULL,
    update_time DATETIME,
    UNIQUE KEY uk_message_id (message_id),
    INDEX idx_status_retry (status, next_retry_time)
) COMMENT='本地消息表';

/**
 * 订单服务实现
 */
@Service
public class OrderService {
    
    @Autowired
    private OrderMapper orderMapper;
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 创建订单
     */
    @Transactional(rollbackFor = Exception.class)
    public String createOrder(OrderDTO dto) {
        // 1. 创建订单
        Order order = Order.builder()
            .orderNo(generateOrderNo())
            .userId(dto.getUserId())
            .productId(dto.getProductId())
            .quantity(dto.getQuantity())
            .totalAmount(dto.getTotalAmount())
            .status(OrderStatus.PENDING)
            .createTime(LocalDateTime.now())
            .build();
        
        orderMapper.insert(order);
        
        // 2. ⚡ 创建本地消息(和订单在同一个事务中)
        LocalMessage message = LocalMessage.builder()
            .messageId(UUID.randomUUID().toString())
            .messageType("STOCK_DEDUCT")
            .messageBody(JSON.toJSONString(Map.of(
                "orderNo", order.getOrderNo(),
                "productId", dto.getProductId(),
                "quantity", dto.getQuantity()
            )))
            .status(MessageStatus.PENDING)
            .createTime(LocalDateTime.now())
            .build();
        
        messageMapper.insert(message);
        
        log.info("订单创建成功,已记录本地消息:orderNo={}, messageId={}", 
            order.getOrderNo(), message.getMessageId());
        
        return order.getOrderNo();
    }
}

/**
 * 定时任务:扫描并发送本地消息
 */
@Component
public class LocalMessageScheduler {
    
    @Autowired
    private LocalMessageMapper messageMapper;
    
    @Autowired
    private RabbitTemplate rabbitTemplate;
    
    /**
     * 每分钟扫描一次待发送的消息
     */
    @Scheduled(cron = "0 * * * * ?")
    public void scanAndSendMessages() {
        log.info("开始扫描本地消息");
        
        // 1. 查询待发送的消息
        List<LocalMessage> messages = messageMapper.selectPendingMessages(100);
        
        log.info("查询到{}条待发送消息", messages.size());
        
        // 2. 发送消息
        for (LocalMessage message : messages) {
            try {
                sendMessage(message);
            } catch (Exception e) {
                log.error("发送消息失败:messageId=" + message.getMessageId(), e);
                handleSendFail(message);
            }
        }
    }
    
    /**
     * 发送消息
     */
    private void sendMessage(LocalMessage message) {
        // 1. 发送到MQ
        rabbitTemplate.convertAndSend(
            "stock.exchange",
            "stock.deduct",
            message.getMessageBody()
        );
        
        // 2. 更新消息状态为成功
        message.setStatus(MessageStatus.SUCCESS);
        message.setUpdateTime(LocalDateTime.now());
        messageMapper.updateById(message);
        
        log.info("消息发送成功:messageId={}", message.getMessageId());
    }
    
    /**
     * 处理发送失败
     */
    private void handleSendFail(LocalMessage message) {
        message.setRetryCount(message.getRetryCount() + 1);
        
        if (message.getRetryCount() >= message.getMaxRetry()) {
            // 超过最大重试次数,标记为失败
            message.setStatus(MessageStatus.FAIL);
            log.error("消息发送失败,已达最大重试次数:messageId={}", 
                message.getMessageId());
            
            // 发送告警
            alertService.sendAlert("本地消息发送失败", message.getMessageId());
        } else {
            // 设置下次重试时间(指数退避)
            int delayMinutes = (int) Math.pow(2, message.getRetryCount());
            message.setNextRetryTime(
                LocalDateTime.now().plusMinutes(delayMinutes)
            );
        }
        
        message.setUpdateTime(LocalDateTime.now());
        messageMapper.updateById(message);
    }
}

// ========== 库存服务 ==========

/**
 * MQ消费者:扣减库存
 */
@Component
public class StockDeductConsumer {
    
    @Autowired
    private StockService stockService;
    
    @RabbitListener(queues = "stock.deduct.queue")
    public void handleStockDeduct(String messageBody) {
        log.info("收到扣减库存消息:{}", messageBody);
        
        try {
            // 1. 解析消息
            Map<String, Object> data = JSON.parseObject(messageBody, Map.class);
            String orderNo = (String) data.get("orderNo");
            Long productId = (Long) data.get("productId");
            Integer quantity = (Integer) data.get("quantity");
            
            // 2. 幂等性检查
            if (stockService.isAlreadyDeducted(orderNo)) {
                log.info("库存已扣减,跳过:orderNo={}", orderNo);
                return;
            }
            
            // 3. 扣减库存
            boolean success = stockService.deductStock(
                productId, 
                quantity, 
                orderNo
            );
            
            if (!success) {
                log.error("扣减库存失败:orderNo={}", orderNo);
                throw new BusinessException("库存不足");
            }
            
            log.info("库存扣减成功:orderNo={}", orderNo);
            
        } catch (Exception e) {
            log.error("处理扣减库存消息失败", e);
            // 重新抛出异常,触发消息重试
            throw new AmqpRejectAndDontRequeueException("处理失败", e);
        }
    }
}

/**
 * 库存服务实现
 */
@Service
public class StockService {
    
    @Autowired
    private StockMapper stockMapper;
    
    @Autowired
    private StockLogMapper logMapper;
    
    /**
     * 扣减库存
     */
    @Transactional(rollbackFor = Exception.class)
    public boolean deductStock(Long productId, Integer quantity, String orderNo) {
        // 1. 扣减库存
        int updated = stockMapper.deductStock(productId, quantity);
        
        if (updated == 0) {
            log.warn("库存不足:productId={}, quantity={}", productId, quantity);
            return false;
        }
        
        // 2. 记录扣减日志(用于幂等性)
        StockLog log = StockLog.builder()
            .orderNo(orderNo)
            .productId(productId)
            .quantity(quantity)
            .type(StockLogType.DEDUCT)
            .createTime(LocalDateTime.now())
            .build();
        
        logMapper.insert(log);
        
        return true;
    }
    
    /**
     * 幂等性检查
     */
    public boolean isAlreadyDeducted(String orderNo) {
        StockLog log = logMapper.selectByOrderNo(orderNo);
        return log != null;
    }
}

/**
 * 优点:
 * ✅ 实现简单
 * ✅ 可靠性高(消息持久化)
 * ✅ 性能好(异步处理)
 * ✅ 业务侵入小
 * 
 * 缺点:
 * ⚠️ 最终一致性(有延迟)
 * ⚠️ 需要定时任务扫描
 * ⚠️ 需要幂等性设计
 * 
 * 适用场景:
 * ✅ 对实时性要求不高
 * ✅ 高并发场景
 * ✅ 微服务架构(推荐)⭐⭐⭐⭐⭐
 */

📊 方案对比总结

方案一致性性能复杂度实时性推荐度
2PC/3PC强一致⭐⭐
TCC强一致⭐⭐⭐
Saga最终一致⭐⭐⭐⭐
本地消息表最终一致⭐⭐⭐⭐⭐
事务消息最终一致⭐⭐⭐⭐⭐

✅ 最佳实践

生产环境推荐:本地消息表 or RocketMQ事务消息

设计原则:
□ 优先选择最终一致性方案
□ 根据业务特点选择合适方案
□ 重试机制必须幂等
□ 记录完整的日志便于排查
□ 监控消息堆积和失败率

容错处理:
□ 消息重试(指数退避)
□ 最大重试次数限制
□ 死信队列处理
□ 人工介入机制
□ 补偿接口

性能优化:
□ 异步处理(MQ)
□ 批量操作
□ 消息去重
□ 限流保护

监控告警:
□ 消息发送失败
□ 消息消费失败
□ 消息堆积
□ 库存不一致

🎉 总结

核心要点

分布式事务解决方案:

1️⃣ 强一致性场景 -> TCC
   - 金融转账
   - 支付场景
   - 实时性要求高

2️⃣ 最终一致性场景 -> 本地消息表/事务消息
   - 订单+库存
   - 积分+优惠券
   - 大部分业务场景(推荐)

3️⃣ 长事务场景 -> Saga
   - 工作流
   - 审批流程
   - 多步骤业务

4️⃣ 关键设计:
   - 幂等性
   - 补偿机制
   - 监控告警
   - 人工介入

记住:分布式事务的核心是"最终一致性+补偿机制"! 🔄


文档编写时间:2025年10月24日
作者:热爱分布式的事务工程师
版本:v1.0
愿你的数据始终一致! 🔄✨