如何防止库存超卖?


一、什么是库存超卖?

库存超卖指的是商品的库存超过我们预计卖出的数量(比如双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);
}

文章作者: GaryLee
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 GaryLee !
  目录