概念
超卖问题:在秒杀系统设计中,超卖是一个经典、常见的问题,任何商品都会有数量上限,如何避免成功下订单买到商品的人数不超过商品数量的上限,这是每个抢购活动都要面临的难点。
可重入锁:一种支持重进入的锁机制。重进入是指一个线程在持有锁的情况下,可以再次获取相同的锁而不会被阻塞。避免了死锁的发生,同时也提高了代码的简洁性和可读性。支持公平性设置,使得等待时间最长的线程优先获取锁。
ReentrantLock
Spring框架提供了一个可重入锁的实现,即ReentrantLock
。这是一个标准的Java并发工具类,Spring对其进行了封装,使其更易于在Spring管理的bean中使用。
下面是一个使用Spring的ReentrantLock
的简单例子:
1 | import org.springframework.beans.factory.annotation.Autowired; |
模拟场景
场景为:你在网吧上网,发现机子时间快结束了,你随便找了一部精彩的电影看,路过的兄弟都觉得你看的这部电影很精彩,纷纷给你的机器续时长,站在你后面一起欣赏。
假设每次续费会增加一个小时,有100个兄弟续费。
实现
为了模拟并发充值问题,设计以下表格用于测试。
1 | CREATE TABLE `device` ( |
出现超卖问题的代码
业务代码
update方法就是查询和更新业务
1 |
|
使用ReentrantLock解决超卖问题
业务层加入ReentrantLock代码
1 | Lock lock = new ReentrantLock(); |
数据库查看执行结果
事务导致超卖问题
业务代码
执行结果
解决事务超卖问题
为什么加入事务就导致锁失效了呢?
答案是事务边界问题
使用@Transactional 注解来管理事务,但锁的获取和释放并没有放在事务边界之内。这意味着如果在事务提交之前锁就被释放了,其他线程可能在当前事务结束之前修改相同的数据,这会导致数据不一致。
如果你在这些新线程中进行数据库操作,它们将运行在各自独立的事务中(如果配置了事务的话),或者根本没有事务管理。这可能会导致数据不一致,因为原始线程的事务可能会回滚,而新线程中的操作可能已经提交了。
因此,如果你需要在有@Transactional注解的方法中进行多线程操作,并且希望这些操作在同一个事务中进行,你需要手动管理这些线程的事务边界,或者重新考虑你的设计,以确保事务的一致性和完整性。
缩小事务边界
业务层调整
执行结果
总结
处理并发和超卖问题时,理解并合理运用锁机制和事务管理至关重要。
通过将锁操作置于事务边界内,可以有效防止数据不一致,确保系统的稳定性和可靠性。
解决方案概述
乐观锁:通过版本号或时间戳检查数据是否已被其他事务修改,适用于读多写少的场景。
悲观锁:预先锁定数据直至事务完成,适合写操作频繁或数据竞争激烈的场景。
分布式锁:如Redisson,确保分布式系统中数据的一致性,适用于跨节点的数据同步。
代码级锁:利用synchronized或ReentrantLock等机制,控制线程间的访问顺序,防止并发冲突
事务边界的重要性
关键点:确保锁的获取和释放严格位于事务边界内,避免数据在事务未完成前被其他线程修改。
实践:使用try-finally结构包裹锁的获取和释放,确保即使发生异常,锁也能正确释放,维护数据完整性。