使用@Transactional仍出现并发下单超卖问题如何解决?
核心问题分析
你当前的超卖问题和事务隔离级别无关,是逻辑设计层面的缺陷:
- 库存校验和扣减是拆分的非原子操作,属于典型的先查后改并发问题:两个并发请求同时查询到库存充足,后续分别执行扣减操作,就会出现超卖。
- InnoDB引擎默认的普通查询是快照读,读取的是事务启动时的库存快照,不是数据库当前的真实库存,即便调整隔离级别也无法避免校验结果过时。
- 库存扣减逻辑是在应用层做计算后再写入数据库,操作本身不具备原子性,并发场景下会出现覆盖更新。
解决方案
方案1:数据库原子扣减(最通用、高可靠,适配中小并发场景)
将库存校验和扣减合并为单个原子SQL操作,依赖数据库行锁保证并发安全,不用额外加锁逻辑:
- 首先在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代表库存不足。
- 调整业务执行顺序,先扣库存再创建订单,避免无效的订单回滚操作:
@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的场景)
如果你的库存校验逻辑不止判断数量,还有其他业务规则,需要先查询库存再做校验,可以给查询加悲观写锁,避免其他事务同时修改同一条商品记录:
- 给GoodsRepository的findById方法加锁注解:
@Lock(LockModeType.PESSIMISTIC_WRITE) Optional<Goods> findById(Long id);
该注解会在查询时自动给数据行加排他锁,其他事务要修改该行必须等当前事务提交。
- 保持原有查询-修改逻辑即可,锁会保证同一时间只有一个事务能操作同一条商品的库存,不会出现并发修改。
高并发场景优化
如果业务并发量超过数据库性能瓶颈,可以采用分布式锁(Redis SETNX)或者Redis预扣库存方案,先在Redis层完成库存校验和扣减,再异步同步到数据库,同时增加超时未支付自动回滚库存的机制即可。
注意事项
- 多商品扣减一定要按照固定顺序(比如商品ID升序)执行,避免不同请求加锁顺序相反导致死锁。
- 需要配套超时订单自动取消逻辑,避免用户下单后不支付导致库存被长期占用。
- 库存扣减失败会触发事务自动回滚,不需要手动处理已扣减的库存。
内容的提问来源于stack exchange,提问作者Fedor Nikolaev
相关产品推荐
相关产品推荐

