库存超卖问题分析
库存超卖问题其本质就是多个线程操作共享数据产生的线程安全问题,即当一个线程在执行操作共享数据的多条代码的过程中,其他线程也参与了进来,导致了线程安全问题的产生。例如:线程1发送请求,查询库存,发现库存大于1,在还没来及扣除库存时,线程2甚至线程3等发送请求,发现这个数量也是大于1,那么这多个线程都会去扣除库存,最终多个线程都去扣除库存,此时就会出现库存的超卖问题。
![image-20230707172612925](https://i-blog.csdnimg.cn/blog_migrate/b732c9c36ef424a3e872b66820cc0049.png)
-
多线程安全问题常见的解决方案就是加锁
-
悲观锁:认为线程安全问题一定会发生,因此在操作数据之前先获取锁,确保线程串行执行,synchronized、lock都属于悲观锁,属于同步锁,让线程串行执行,优点是简单粗暴,缺点是性能一般 -
乐观锁:认为线程安全问题不一定会发生,只是在更新数据时,判断有没有其他线程对数据做了修改,优点是性能好,缺点是存在成功率问题 -
如果没有修改,则认为是安全的,该线程进行数据更新 -
如果已经被其他线程修改,则发生了线程安全问题,此时可以重试或异常
-
-
-
使用乐观锁解决库存超卖问题
-
乐观锁的关键是判断之前查询得到的数据是否被修改过,常见的方式有两种
-
版本号法
-
在数据库等中设置一个版本号字段,每次操作数据会对版本号进行加一操作,当每次读取数据时,读出版本号字段 ,在修改数据的时候,需要判断读出的版本号和数据库是否一致,如果一致,则表明在自己操作该数据的过程中,没有其他线程操作该数据,如果版本号不一致,则表明数据以及被别人修改过了。
image-20230710111424101
-
-
CAS
-
在扣减库存时,将现在库存和之前查询的库存做对比,如果一样,则说明没有其他线程在中间修改过库存数据,则认为是线程安全的,如果不一样,则说明有线程在中间修改过库存数据。
image-20230710143222845
-
-
-
乐观锁解决超卖问题
-
使用在扣减库存时,将现在库存和之前查询的库存做对比,如果一样,则表明没有人在此中间修改过库存,则认定线程安全,扣除库存
@Override
public Long seckillVoucher(Long voucherId) {
// 查询秒杀优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断秒杀是否开始和结束
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//如果当前时间 在开始时间之后 再结束时间之前 则表明秒杀能进行
LocalDateTime localDateTime = LocalDateTime.now();
if ( localDateTime.isBefore(beginTime) || localDateTime.isAfter(endTime) ){
return null;
}
//获取库存量
Integer stock = seckillVoucher.getStock();
if (ObjectUtil.isNull(stock) || ObjectUtil.isNotNull(stock) && stock.intValue() <= 0){
return null;
}
//扣减库存 将现在库存和之前查询的库存做对比,如果一样,则表明没有人在此中间修改过库存,则认定线程安全,扣除库存
boolean update = seckillVoucherService.update(
new LambdaUpdateWrapper<SeckillVoucher>().setSql("stock = stock - 1").eq(SeckillVoucher::getVoucherId, voucherId)
.eq(SeckillVoucher::getStock, stock)
);
if (!update){
return null;
}
//创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//创建订单ID
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//获取用户ID
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
// 代金券id
voucherOrder.setVoucherId(voucherId);
this.save(voucherOrder);
return orderId;
}该方式通过测试能够发现,失败率很高,这主要是由于当多个人都拿到库存时,只有一个能够扣减成功,其他的由于.eq(SeckillVoucher::getStock, stock)无法完成扣减。
其实在超卖问题中,我们需要保证的是在没用库存的情况下,不能再进行库存扣减,所有需要保证的是库存大于0,所有可以设置.gt(SeckillVoucher::getStock, 0)
@Override
public Long seckillVoucher(Long voucherId) {
// 查询秒杀优惠券信息
SeckillVoucher seckillVoucher = seckillVoucherService.getById(voucherId);
//判断秒杀是否开始和结束
LocalDateTime beginTime = seckillVoucher.getBeginTime();
LocalDateTime endTime = seckillVoucher.getEndTime();
//如果当前时间 在开始时间之后 再结束时间之前 则表明秒杀能进行
LocalDateTime localDateTime = LocalDateTime.now();
if ( localDateTime.isBefore(beginTime) || localDateTime.isAfter(endTime) ){
return null;
}
//获取库存量
Integer stock = seckillVoucher.getStock();
if (ObjectUtil.isNull(stock) || ObjectUtil.isNotNull(stock) && stock.intValue() <= 0){
return null;
}
//扣减库存 将现在库存和之前查询的库存做对比,如果一样,则表明没有人在此中间修改过库存,则认定线程安全,扣除库存
boolean update = seckillVoucherService.update(
new LambdaUpdateWrapper<SeckillVoucher>().setSql("stock = stock - 1").eq(SeckillVoucher::getVoucherId, voucherId)
.gt(SeckillVoucher::getStock, 0)
);
if (!update){
return null;
}
//创建订单
VoucherOrder voucherOrder = new VoucherOrder();
//创建订单ID
long orderId = redisIdWorker.nextId("order");
voucherOrder.setId(orderId);
//获取用户ID
Long id = UserHolder.getUser().getId();
voucherOrder.setUserId(id);
// 代金券id
voucherOrder.setVoucherId(voucherId);
this.save(voucherOrder);
return orderId;
}
本文由 mdnice 多平台发布