环境准备:复现超卖现象
1、环境基本准备:新建一个count表,写几条模拟数据,做一条库存为1的数据
2、新建springboot项目,用mybatis-generator快速生成所需代码mybatis-generator插件实现代码自动生成_p&f°的博客-CSDN博客
3、自己在mapper层和xml文件中写一个扣减库存的方法
//根据id找到对应商品,扣减库存
int updateCountAfterBuyPro(@Param("id") Integer id, @Param("buyCount") Integer buyCount, @Param("updateTime") Date updateTime);
<update id="updateCountAfterBuyPro" parameterType="java.lang.Integer">
update count
set count = count - #{buyCount}
where id = #{id, jdbcType=INTEGER}
</update>
4、写一个server类,模拟扣减库存,生成订单操作
/**
* @auther xpf
* @date 2022/6/2 10:40
* @description 超卖现象解决
*/
@Service
public class OrderServer {
@Resource
private CountMapper countMapper;
// @Transactional(rollbackFor = Exception.class)
public void updateCount(int id, int buyCount){
//1、扣库存
Count selectProduct = countMapper.selectByPrimaryKey(id);
if (selectProduct == null){
throw new RuntimeException("找不到对应的商品");
}
Integer productCount = selectProduct.getCount();
System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
if (productCount < buyCount){
throw new RuntimeException("库存不足无法购买");
}
Date date = new Date();
countMapper.updateCountAfterBuyPro(id, productCount, date);
//2、生成订单快照(模拟)
//3、生成订单详情表(模拟)
}
}
5、写一个测试类,假设多人同时访问
@SpringBootTest
class DistributeLockApplicationTests {
@Autowired
private OrderServer orderServer;
@Test
void overCountTest() throws InterruptedException {
CountDownLatch cdl = new CountDownLatch(5);
CyclicBarrier cyclicBarrier = new CyclicBarrier(5);
ExecutorService es = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
es.execute(()->{
try {
//让线程同时到达
cyclicBarrier.await();
orderServer.updateCount(1, 1);
System.err.println(Thread.currentThread().getName() + "线程执行完毕");
} catch (Exception e) {
e.printStackTrace();
}finally {
cdl.countDown();
}
});
}
cdl.await();
es.shutdown();
}
}
6、运行程序,查看结果
可以看到,五个线程同时到达,同时获取到数据库id为1的商品的库存为1,然后都去更新库存
然后从数据库看到库存变为-4,不符合日常逻辑,超卖现象就产生了!
一、基于 synchronized 的方法锁(最原始的锁)
1、用方法锁在 updateCount 加上一个 synchronized 关键字。
2、将库存改为1,测试发现,当第一个线程获取到方法锁之后,其他线程只能等待线程释放锁,才能进入方法,可以解决超卖问题,数据库库存也是正常的0
题外:解决@Transactional加上事务后,还是会产生超卖问题
3、但是,一般情况下,下单步骤,在第一步扣减完库存后,会生成订单快照和生成订单详情表,那么这个时候方法往往会加上 @Transactional 事务回滚机制。
在加完事务回滚后,继续将库存改为1,测试发现,还是会产生超卖现象。
数据库库存字段为 -1
分析输出结果:
找到问题,原来是线程1先获得方法锁,进入方法后,扣减库存,但是由于开启了事务,在线程1释放锁后,提交事务之前,库存还是为1。此时,线程4获得方法锁,读取到还未提交的库存数量为1。然后线程1提交完毕,库存 1-1 = 0。线程4继续执行,剩余库存 0 - 1 = -1,然后提交。其他线程因为线程1提交完就已经是0了,所以抛出异常。
4、解决方案。
分析可以发现,事务提交是在锁之外的,所以会出现这种情况。那么把事务也锁起来,在释放锁之前,确保事务已经提交即可!
所以需要使用手动控制事务,而不用spring自带的。将原来的
@Transactional(rollbackFor = Exception.class)注释掉,修改代码如下
@Service
public class OrderServer {
@Resource
private CountMapper countMapper;
//平台事务管理器
@Autowired
private PlatformTransactionManager platformTransactionManager;
//事务的定义
@Autowired
private TransactionDefinition transactionDefinition;
// @Transactional(rollbackFor = Exception.class)
public synchronized void updateCount(int id, int buyCount){
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
//1、扣库存
Count selectProduct = countMapper.selectByPrimaryKey(id);
if (selectProduct == null){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("找不到对应的商品");
}
Integer productCount = selectProduct.getCount();
System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
if (productCount < buyCount){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("库存不足无法购买");
}
Date date = new Date();
countMapper.updateCountAfterBuyPro(id, productCount, date);
//2、生成订单快照(模拟)
//3、生成订单详情表(模拟)
platformTransactionManager.commit(transaction);
}
}
5、将数据库库存恢复为1,测试通过,数据库库存正常为0,结果如下。
二、基于 synchronized 的块锁(最原始的锁)
对象锁:
synchronized (this){
}
或者
Object object = new Object();
synchronized (object){
//业务代码 }
类锁:
//括号里写的是 本类.class
synchronized (OrderServer.class){
}
对象锁和类锁的区别:类锁只可能有一个,而对象锁可能可以通过new创建多个,还是会产生并发执行情况。
使用快锁修改代码后结果:
@Service
public class OrderServer {
@Resource
private CountMapper countMapper;
//平台事务管理器
@Autowired
private PlatformTransactionManager platformTransactionManager;
//事务的定义
@Autowired
private TransactionDefinition transactionDefinition;
// @Transactional(rollbackFor = Exception.class)
public synchronized void updateCount(int id, int buyCount){
synchronized (OrderServer.class){
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
//1、扣库存
Count selectProduct = countMapper.selectByPrimaryKey(id);
if (selectProduct == null){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("找不到对应的商品");
}
Integer productCount = selectProduct.getCount();
System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
if (productCount < buyCount){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("库存不足无法购买");
}
Date date = new Date();
countMapper.updateCountAfterBuyPro(id, productCount, date);
//2、生成订单快照(模拟)
//3、生成订单详情表(模拟)
platformTransactionManager.commit(transaction);
}
}
}
执行结果符合预期。
三、使用可重入锁 ReentranLock (jdk1.5以后并发包中的锁)
setver 代码修改如下:
@Service
public class OrderServer {
@Resource
private CountMapper countMapper;
//平台事务管理器
@Autowired
private PlatformTransactionManager platformTransactionManager;
//事务的定义
@Autowired
private TransactionDefinition transactionDefinition;
// @Transactional(rollbackFor = Exception.class)
public synchronized void updateCount(int id, int buyCount){
ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
TransactionStatus transaction = platformTransactionManager.getTransaction(transactionDefinition);
//1、扣库存
Count selectProduct = countMapper.selectByPrimaryKey(id);
if (selectProduct == null){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("找不到对应的商品");
}
Integer productCount = selectProduct.getCount();
System.out.println(Thread.currentThread().getName() + "获取的库存值是:" + productCount);
if (productCount < buyCount){
platformTransactionManager.rollback(transaction);
throw new RuntimeException("库存不足无法购买");
}
Date date = new Date();
countMapper.updateCountAfterBuyPro(id, productCount, date);
//2、生成订单快照(模拟)
//3、生成订单详情表(模拟)
platformTransactionManager.commit(transaction);
}catch (Exception e){
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
结果符合预期。