业务场景(并发篇)--秒杀场景下如何防止超卖

2022-03-04 13:48:04 浏览数 (1)

1、超卖现象

  • 在同一时间如果有多个用户进行查询库存,那么他们得到的库存数据是一样的,都能够进行下单操作,这样必然就出现了超卖现象
  • 同一个用户在有库存的时候,连续发出多个请求,多个请求同时存在,于是生成多个订单

2、秒杀需解决的问题

如何在有限的商品数量的限制下如何保证抢购到商品的用户数不能大于商品数量,也就是不能出现超卖的问题;还有就是抢购时会出现大量用户的访问,如何提高用户体验效果也是一个问题,也就是要解决秒杀系统的性能问题

3、解决超卖方案

3.1 在数据库层面解决

(1)、在查询商品库存时加排他锁,执行如下语句

代码语言:javascript复制
select * from goods for where goods_id=#{id} for update

在事务中线程A通过select * from goods for where goods_id=#{id} for update语句给goods_id为#{id}的数据行上了锁。那么其他线程此时可以使用select语句读取数据,但是如果也使用select for update语句加锁,或者使用update,add,delete都会阻塞,直到线程A将事务提交(或者回滚),其他线程中的某个线程排在线程A后的线程才能获取到锁

(2)、更新数据库减库存的时候,进行库存限制条件

代码语言:javascript复制
update goods set stock = stock - 1 where goods_id = #{id} and stock >0

这种通过数据库加锁来解决的方案,性能不是很好,在高并发的情况下,还可能存在因为获取不到数据库连接或者因为超时等待而报错。

3.2 利用分布式锁

同一个锁key,同一时间只能有一个客户端拿到锁,其他客户端会陷入无限的等待来尝试获取那个锁,只有获取到锁的客户端才能执行下面的业务逻辑

这种方案的缺点是同一个商品在多用户同时下单的情况下,会基于分布式锁串行化处理,导致没法同时处理同一个商品的大量下单的请求

3.3 利用分布式锁 分段缓存

把数据分成很多个段,每个段是一个单独的锁,所以多个线程过来并发修改数据的时候,可以并发的修改不同段的数据

假设场景:假如你现在商品有100个库存,在redis存放5个库存key,形如

代码语言:javascript复制
key1=goods-01,value=20;
代码语言:javascript复制
key2=goods-02,value=20;
代码语言:javascript复制

key3=goods-03,value=20

用户下单时对用户id进行%5计算,看落在哪个redis的key上,就去取哪个,这样每次就能够处理5个进程请求

这种方案可以解决同一个商品在多用户同时下单的情况,但有个坑需要解决:当某段锁的库存不足,一定要实现自动释放锁然后换下一个分段库存再次尝试加锁处理,此种方案复杂比较高

3.4 利用redis的incr、decr的原子性 异步队列

实现思路

  • 1、在系统初始化时,将商品的库存数量加载到redis缓存中
  • 2、接收到秒杀请求时,在redis中进行预减库存(利用redis decr的原子性),当redis中的库存不足时,直接返回秒杀失败,否则继续进行第3步;
  • 3、将请求放入异步队列中,返回正在排队中;
  • 4、服务端异步队列将请求出队(哪些请求可以出队,可以根据业务来判定,比如:判断对应用户是否已经秒杀过对应商品,防止重复秒杀),出队成功的请求可以生成秒杀订单,减少数据库库存(在扣减库存的sql如下
代码语言:javascript复制
update goods set stock = stock - 1 where goods_id = #{id} and stock >0

),返回秒杀订单详情

  • 5、用户在客户端申请秒杀请求后,进行轮询,查看是否秒杀成功,秒杀成功则进入秒杀订单详情,否则秒杀失败

这种方案的缺点:由于是通过异步队列写入数据库中,可能存在数据不一致,其次引用多个组件复杂度比较高

4.参考链接

秒杀系统优化以及解决超卖问题

https://bit.ly/3e7kDVu

电商超卖现象的解决思路

https://bit.ly/2XnYUlz

0 人点赞