一、什么是库存超卖?
库存超卖指的是商品的库存超过我们预计卖出的数量(比如双11可以用9.9抢购iphone手机,但是总共只有10台,如果没处理导致最终被抢购了100台、1000台,超过了预计卖出的数量,就是库存超卖问题)
一般我们在做商品库存扣减的时候,会先判断库存是否充足,如果充足的话就执行库存扣减操作,如果不充足的话则返回失败
但在高并发场景下,就可能出现下列问题:
多个线程同时查询库存的时候返回充足后,同时对库存进行扣减,导致出现库存超卖的问题
二、如何防止库存超卖?
防止库存超卖,本质上就是为了解决并发问题,要保证操作的原子性和有序性:
- 原子性:库存查询、库存扣减等操作,需要作为一个原子操作执行
- 有序性:多个并发线程需要排队执行
1、MySQL方案
我们可以使用MySQL的行锁进行update库存操作,如:
update product
set quantity = quantity - #{count}
where product_id = 111 and quantity >= #{count}
缺点:如果请求全部打到数据库,会出现请求阻塞的情况,最终可能搞垮数据库
2、Redis Lua方案
Redis作为一个内存数据库,本身的读写操作就很快,所以借助Redis的单线程机制+Lua脚本可以保证操作的原子性,如:
if tonumber(redis.call('get','product:111'))>0 then -- 库存查询
redis.call('decr', 'product:111') -- 库存扣减
return 1; -- 成功返回1
else
return 0; -- 失败返回0
return;
三、Q&A
1、为什么不使用Redis的setnx指令来防止库存超卖?
问题1:当我们用setnx命令时,可以保证只能有一个请求执行成功,但是如果这个请求后续执行异常了呢?那么会导致这个分布式锁不能被释放
解决方案:所以我们一般用set命令代替setnx命令,并设置好过期时间
问题2:但是由于这个获取锁的操作和库存扣减操作不是原子的,如果获取锁成功了,由于库存扣减时由于某些原因执行时间超过了锁的过期时间,导致锁被自动释放,所以可能这个锁会被其他线程获取,最终导致超卖
解决方案://TODO 锁续命
总结:Redis的setnx指令在高并发的场景下,锁的竞争会变多,导致性能下降。另外由于获取锁的操作和库存扣减的操作不是原子性,可能会导致超卖
// 获取锁推荐使用set的方式
String result = jedis.set(lockKey, requestId, "NX", "EX", expireTime);
String result = jedis.setnx(lockKey, requestId); //如线程死掉,其他线程无法获取到锁
// 释放锁,非原子操作,可能会释放其他线程刚加上的锁
if (requestId.equals(jedis.get(lockKey))) {
jedis.del(lockKey);
}