You need to enable JavaScript to run this app. Spring@Transactional事务提交时机与ReentrantLock秒杀防超卖问题咨询
优惠活动
大模型
产品
解决方案
定价
更多

Spring@Transactional事务提交时机与ReentrantLock秒杀防超卖问题咨询

问题解答

1. Spring @Transactional注解的事务提交时机

Spring声明式事务基于AOP动态代理实现,采用环绕通知对标注了@Transactional的方法进行增强,执行流程固定为:

  • 代理对象拦截方法调用,方法执行前开启事务
  • 执行开发者编写的目标方法的全部业务逻辑
  • 目标方法执行完成且无异常时,方法执行后提交事务;若抛出未捕获的回滚异常,则执行回滚逻辑

也就是说,事务提交动作一定是在你编写的目标方法的所有代码(包括你写的lock.unlock())执行完毕之后才会触发,不可能在方法结束前完成提交。

2. 秒杀场景锁与事务结合的问题分析

2.1 核心疑问解答

你当前的写法下,事务一定不会在unlock执行前提交,且还有一个隐藏问题:@Transactional标注的方法事务开启是在代理拦截阶段触发,也就是事务开启动作发生在你执行lock.lock()之前,完全不满足你要求的「事务必须在lock之后开启」的前提。

2.2 超卖问题确实会发生

你描述的异常流程是完全成立的:

  1. 线程1进入方法,获取到锁,执行库存校验、扣减逻辑后调用unlock释放锁,此时线程1的事务尚未提交
  2. 线程2在unlock后立刻拿到锁,开启执行逻辑,由于MySQL RR隔离级别下的快照读机制,线程2的事务无法读取到线程1未提交的库存扣减结果,校验库存时会认为库存仍然充足,继续执行扣减
  3. 两个线程的事务先后提交,最终库存扣减两次,出现超卖

2.3 修复方案

可以根据业务场景选择以下两种方案:

方案1:调整锁范围,包裹整个事务生命周期

保证加锁在事务开启之前,解锁在事务提交之后,常见实现方式:

  • 上层调用层加锁:比如在Controller层调用func方法之前加锁,调用完成后解锁,锁会自然包裹整个事务的开启、执行、提交流程
  • 改用编程式事务:在方法内部手动控制事务的开启和提交,示例代码如下:
@Service
public class ServiceOne{
    private Lock lock = new ReentrantLock(true);
    @Autowired
    private TransactionTemplate transactionTemplate;

    public Result func(long seckillId, long userId) {
        lock.lock();
        try {
            return transactionTemplate.execute(status -> {
                // Check stock
                // sale
                // Reduce stock
                return Result.success();
            });
        } finally {
            lock.unlock();
        }
    }
}

方案2:数据库层面兜底防超卖

即使锁机制出现问题,也可以通过数据库行锁避免超卖,将扣减库存的SQL改为带库存校验的条件更新:

UPDATE seckill_stock SET stock = stock - 1 WHERE seckill_id = #{seckillId} AND stock > 0

该语句会自动获取对应行的写锁,在提交前会阻塞其他线程的更新操作,且只有库存大于0时才会执行扣减,从数据库层面杜绝超卖。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.11 14:30:04