提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
秒杀场景下的原子性扣减是怎么来的
提示:以下是本篇文章正文内容,下面案例可供参考
一、库存是怎么样扣减的
对于一个商品库存的扣减流程如下:
这是秒杀场景下的库存扣减,需要考虑的是库存的预扣减和回退库存以及库存不足和超卖问题。
二、不同扣减的实现方案
1.数据库扣减
普通的数据库扣减就是写Sql语句:update product set stock = stock - 1 where id = 1;
但是这种需要在扣减之前呢对库存做个判断来检查是否充足,即:
int stock = mapper.getStockById(1);
if (stock > 0) {
int count = mapper.updateStock(1);
if (count > 0) {
addOrder(1);
}
}
但是这段代码的问题------查询和更新操作不是原子性的,如果并发场景下,都查询到了库存大于0,都执行扣减,就会超卖。如果加synchronized关键字对性能影响太大了,所以引出基于数据库的乐观锁,即修改sql语句:
update product set stock = stock - 1 where id = product and stock > 0。但是由于redis热门,且不能频繁访问数据库,所以扣减这可以挪到redis中。
2.Reids扣减
上面提到了扣减时需要将判断库存是否充足和扣减作为一个原子性操作,那么引出redis的incr。
decr保证了什么操作的原子性呢?
就是说保证了执行库存扣减时的原子性,因为库存扣减背后也分为:拿库存,-1,写回库存这三件事。不是原子性的话 这三步可能会被别的事务或线程给打断。
伪代码:
boolean exist = redidClient.query(productid, userId);
if (exist) {
return -1;
}
int stock = redisClient.queryStock(productId);
if (stock <= 0) {
return 0;
}
redisClient.incrby(productId, -1);
redisClient.add(productId, userId);
return 1;
流程如下:
先判断用户是否参与过秒杀活动,参与过就退出;再判断现在还有没有了,没有就退出;如果有库存,就开始扣减,然后将参与秒杀活动的记录存起来。但是注意,此时只是原子性扣减,并没有解决超卖,因为查库存(queryStock)和更新库存(incrby)是非原子性操作,在并发情况下,多个用户都发现有库存,都进行原子性扣减,所以造成超卖。同样的加synchronized话可以解决超卖,但是影响性能,每次查询都要竞争那把锁。
解决方案(判断预扣减后库存是否为负数):
boolean exist = redisClient.query(productId, userId);
if (exist) {
return -1;
}
if (redisClient.incrby(productId, -1) < 0) {
return 0;
}
redisClient.add(productId, userId);
return 1;
解释:
redisClient.incrby(productId, -1) < 0;这个判断先进行预扣减,扣减完后大于0说明库存充足,扣减完后等于0说明正好就剩一个,扣减完后为负数那么说明已经没库存了,预扣减失败.如果预扣减失败的话(为负数)还需要把库存设为0:
如果这里不设置为0的话,并发场景下,预计库存很多负数值负的太多,回退时就比较麻烦。就得采用lua脚本来解决。
总结
以上内容大部分来自博客:高并发秒杀细节_redis滑块锁-CSDN博客。
这里仅作为我个人学习总结使用。