Spring@Transactional事务提交时机与ReentrantLock秒杀防超卖问题咨询
问题解答
1. Spring @Transactional注解的事务提交时机
Spring声明式事务基于AOP动态代理实现,采用环绕通知对标注了@Transactional的方法进行增强,执行流程固定为:
- 代理对象拦截方法调用,方法执行前开启事务
- 执行开发者编写的目标方法的全部业务逻辑
- 目标方法执行完成且无异常时,方法执行后提交事务;若抛出未捕获的回滚异常,则执行回滚逻辑
也就是说,事务提交动作一定是在你编写的目标方法的所有代码(包括你写的lock.unlock())执行完毕之后才会触发,不可能在方法结束前完成提交。
2. 秒杀场景锁与事务结合的问题分析
2.1 核心疑问解答
你当前的写法下,事务一定不会在unlock执行前提交,且还有一个隐藏问题:@Transactional标注的方法事务开启是在代理拦截阶段触发,也就是事务开启动作发生在你执行lock.lock()之前,完全不满足你要求的「事务必须在lock之后开启」的前提。
2.2 超卖问题确实会发生
你描述的异常流程是完全成立的:
- 线程1进入方法,获取到锁,执行库存校验、扣减逻辑后调用
unlock释放锁,此时线程1的事务尚未提交 - 线程2在
unlock后立刻拿到锁,开启执行逻辑,由于MySQL RR隔离级别下的快照读机制,线程2的事务无法读取到线程1未提交的库存扣减结果,校验库存时会认为库存仍然充足,继续执行扣减 - 两个线程的事务先后提交,最终库存扣减两次,出现超卖
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
相关产品推荐
相关产品推荐

