线上订单数据错乱,我花了2小时排查,发现问题竟在代码里

6 阅读1分钟

一、问题背景

线上用户反馈订单数据不对,对方查询我们的订单状态接口时,同一个订单的不同乘机人,给了不同的状态。

排查发现:事务没生效,数据只更新了一半

二、问题代码

@Service public class OrderServiceImpl implements OrderService {

  @Override
  public void createOrder(OrderDTO dto) {
      // 1. 创建订单
      orderMapper.insert(order);

      // 2. 更新库存 - 事务失效的位置
      // 同一个类内部方法调用,绕过了代理!
      boolean success = updateStock(dto.getSkuId());
      if (!success) {
          throw new RuntimeException("库存不足");
      }
  }

  // 这个方法上的@Transactional不起作用!
  @Transactional(rollbackFor = Exception.class)
  public boolean updateStock(Long skuId) {
      // 更新库存
      stockMapper.deduct(skuId);
      return true;
  }

}

三、问题根因

用图解释: 正常调用(经过代理):

OrderServiceImpl.createOrder()
    → 代理对象(事务开启)
    → 目标对象方法
    → 代理对象(事务提交)
    ✅ 事务生效

内部调用(绕过代理):

OrderServiceImpl.createOrder()
    → this.updateStock()  // 直接调目标对象
    → 事务注解无效
    ❌ 事务失效

关键点:

  • Spring 事务基于 AOP 动态代理(ProxyOrderService)
  • this.xxx() 是直接调用目标对象,不是代理对象
  • 所以 @Transactional 注解被跳过

四、解决方案

方案 1:注入自己 @Service public class OrderServiceImpl implements OrderService {

  @Autowired
  private OrderService self;  // 注入代理对象

  @Override
  public void createOrder(OrderDTO dto) {
      orderMapper.insert(order);

      // 用代理对象调用,走事务
      boolean success = self.updateStock(dto.getSkuId());
      if (!success) {
          throw new RuntimeException("库存不足");
      }
  }

  @Transactional(rollbackFor = Exception.class)
  public boolean updateStock(Long skuId) {
      stockMapper.deduct(skuId);
      return true;
  }

}

方案 2:类内部方法抽取到另一个 Bean @Service public class StockService {

  @Transactional(rollbackFor = Exception.class)
  public void deductStock(Long skuId) {
      stockMapper.deduct(skuId);
  }

}

@Service public class OrderServiceImpl implements OrderService {

  @Autowired
  private StockService stockService;

  @Override
  public void createOrder(OrderDTO dto) {
      orderMapper.insert(order);
      stockService.deductStock(dto.getSkuId());
  }

}

方案 3:手动编程式事务(不推荐,最复杂)

五、总结

  1. Spring 事务基于动态代理
  2. 类内部方法调用会绕过代理
  3. 最佳实践:涉及事务的方法,调用方不要在同一类内
  4. 自注入或拆分到独立 Bean 是标准解法