You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

使用@Transactional仍出现并发下单超卖问题如何解决?

核心问题分析

你当前的超卖问题和事务隔离级别无关,是逻辑设计层面的缺陷:

  • 库存校验和扣减是拆分的非原子操作,属于典型的先查后改并发问题:两个并发请求同时查询到库存充足,后续分别执行扣减操作,就会出现超卖。
  • InnoDB引擎默认的普通查询是快照读,读取的是事务启动时的库存快照,不是数据库当前的真实库存,即便调整隔离级别也无法避免校验结果过时。
  • 库存扣减逻辑是在应用层做计算后再写入数据库,操作本身不具备原子性,并发场景下会出现覆盖更新。

解决方案

方案1:数据库原子扣减(最通用、高可靠,适配中小并发场景)

将库存校验和扣减合并为单个原子SQL操作,依赖数据库行锁保证并发安全,不用额外加锁逻辑:

  1. 首先在GoodsRepository中新增自定义扣减方法:
@Modifying
@Query(value = "UPDATE goods SET available = available - :quantity WHERE id = :goodsId AND available >= :quantity", nativeQuery = true)
int deductStock(@Param("goodsId") Long goodsId, @Param("quantity") Long quantity);

这个SQL执行后返回的影响行数为1代表扣减成功,为0代表库存不足。

  1. 调整业务执行顺序,先扣库存再创建订单,避免无效的订单回滚操作:
@Transactional(isolation = Isolation.READ_COMMITTED, propagation = Propagation.REQUIRES_NEW)
public boolean createOrder(User user) {
    final Map<Long, Long> records = cartRecords.getRecords();
    // 按商品ID升序扣减,避免不同并发请求加锁顺序不一致导致死锁
    List<Long> sortedGoodsIds = records.keySet().stream().sorted().toList();
    
    // 先扣所有商品库存
    for (Long id : sortedGoodsIds) {
        int effectRows = goodsRepository.deductStock(id, records.get(id));
        if (effectRows == 0) {
            // 任意商品扣减失败,事务自动回滚所有已扣库存
            return false;
        }
    }
    
    // 库存扣减全部成功后再创建订单、写入订单商品关联
    Long sum = 0L;
    for (Long id : records.keySet()) {
        Goods goods = goodsRepository.findById(id).get();
        sum += goods.getPrice() * records.get(id);
    }
    Date dateNow = new Date();
    Order order = new Order(dateNow, sum, true, user);
    orderRepository.save(order);

    for (Long id : records.keySet()) {
        addOrderGoods(new OrderGoods(goodsRepository.findById(id).get(), order, records.get(id)));
    }
    // 原有的checkAvailable方法可以废弃,原子扣减已经包含了库存校验逻辑
    return true;
}

方案2:悲观锁方案(适合校验逻辑复杂,无法合并为单SQL的场景)

如果你的库存校验逻辑不止判断数量,还有其他业务规则,需要先查询库存再做校验,可以给查询加悲观写锁,避免其他事务同时修改同一条商品记录:

  1. 给GoodsRepository的findById方法加锁注解:
@Lock(LockModeType.PESSIMISTIC_WRITE)
Optional<Goods> findById(Long id);

该注解会在查询时自动给数据行加排他锁,其他事务要修改该行必须等当前事务提交。

  1. 保持原有查询-修改逻辑即可,锁会保证同一时间只有一个事务能操作同一条商品的库存,不会出现并发修改。

高并发场景优化

如果业务并发量超过数据库性能瓶颈,可以采用分布式锁(Redis SETNX)或者Redis预扣库存方案,先在Redis层完成库存校验和扣减,再异步同步到数据库,同时增加超时未支付自动回滚库存的机制即可。

注意事项

  • 多商品扣减一定要按照固定顺序(比如商品ID升序)执行,避免不同请求加锁顺序相反导致死锁。
  • 需要配套超时订单自动取消逻辑,避免用户下单后不支付导致库存被长期占用。
  • 库存扣减失败会触发事务自动回滚,不需要手动处理已扣减的库存。

内容的提问来源于stack exchange,提问作者Fedor Nikolaev

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.28 22:45:05