【并发】高并发下库存超卖问题如何解决?

本文涉及的产品
云数据库 Redis 版,社区版 2GB
推荐场景:
搭建游戏排行榜
简介: 【并发】高并发下库存超卖问题如何解决?

出现场景

在并发的场景下,比如商城售卖商品中,一件商品的销售数量>库存数量的问题,称为超卖问题。主要原因是在并发场景下,请求几乎同时到达,对库存资源进行竞争,由于没有适当的并发控制策略导致的错误。

例如简单的下单操作,通常我们会按照如下写法

public ServerResponse createOrder(Integer userId, Integer shippingId){
    // 执行查询sql select amount form store where postID = 12345;
    // 判断是否大于0然后执行更新操作 update store set amount = amount - quantity where postID = 12345;
}

由于如上的写法在应用层没有任何并发控制,如果 postID 为12345的商品库存为1件,此时有两个请求到达,先后执行了查询sql,则通过MySQL读取库存时,会加共享锁,因此都能获取到商品库存为1件,然后又分别执行更新操作,MySQL会将两个更新操作串行化执行,依次成功减库存,因此库存数量变成-1。

以下是几种比较主流的解决方案

解决方案

数据库设置字段为无符号型

数据库设置字段为无符号型

当并发超卖时为负数直接报异常

通过捕获异常提示已经售空。

悲观锁

悲观锁主要用于保护数据的完整性。当多个事务并发执行时,某个事务对数据应用加锁,则其他事务只能等该事务执行完了,才能进行对该数据进行修改操作。

update goods set num = num - 1 WHERE id = 1001 and num > 0

假设现在商品只剩下一件了,此时数据库中 num = 1;

但有 100 个线程同时读取到了这个 num = 1,所以 100 个线程都开始减库存了。

但你最终会发觉,其实只有一个线程减库存成功,其他 99 个线程全部失败。update操作会自动加排它锁

需要注意的是,FOR UPDATE 生效需要同时满足两个条件时才生效:

  • 数据库的引擎为 innoDB
  • 操作位于事务块中(BEGIN/COMMIT)

悲观锁采用的是「先获取锁再访问」的策略,来保障数据的安全。但是加锁策略,依赖数据库实现,会增加数据库的负担,对于并发很高的场景并不会使用悲观锁,会导致其他事务都会发生阻塞,造成大量的事务发生积压拖垮整个系统。

乐观锁

select version from goods WHERE id= 1001
update goods set num = num - 1, version = version + 1 WHERE id= 1001 AND num > 0 AND version = @version(上面查到的version);

这种方式采用了版本号的方式,其实也就是 CAS 的原理。


假设此时 version = 100, num = 1; 100 个线程进入到了这里,同时他们 select 出来版本号都是 version = 100。

然后直接 update 的时候,只有其中一个先 update 了,同时更新了版本号。

那么其他 99 个在更新的时候,会发觉 version 并不等于上次 select 的 version,就说明 version 被其他线程修改过了。那么我就放弃这次 update。


使用乐观锁需修改数据库的事务隔离级别:

使用乐观锁的时候,如果一个事务修改了库存并提交了事务,那其他的事务应该可以读取到修改后的数据值,所以不能使用可重复读的隔离级别,应该修改为读取已提交(Read committed)

缺点:虽然防止了超卖,但是会导致很多线程更新库存失败了,所以在我们这种设计下,1000个人真要是同时发起购买,可能只有100个幸运儿能够买到东西。

通过redis队列解决

秒杀的商品id作为键,库存作为redis中的list,提前加入redis缓存中,多少件商品就入队列多少个1,高并发请求到达时依次在队列中排序获取库存,能够获得库存则继续执行下单逻辑,否则库存不足抢不到。但是这种方式下,每个请求只能购买一件商品。

// 假设有1000个商品,商品id为goods_123
Jedis jedis = new Jedis("127.0.0.1", 6379);
for(int i = 0; i < 1000; i++){
  jedis.rpush("goods_123", 1);
}

抢购下单逻辑如下

Jedis jedis = new Jedis("127.0.0.1", 6379);
int result = 0;
if(result = jedis.lpop("goods_123") > 0){
  // 有库存
}else{
  // 库存不足
}

优点:实现简单,不需要在单独加锁(无论是悲观锁还是乐观锁)。

缺点:队列的长度是有限的,必须控制好,不然请求会越积越多。当库存非常大时, 会占用非常多的内存,每个请求只能购买一件商品。

分布式锁+分段缓存

借鉴ConcurrenthashMap分段锁的机制,把100个商品,分在3个段上,key为分段名字,value为库存数量。用户下单时对用户id进行%3计算,看落在哪个redis的key上,就去取哪个。


如key1=product-01,value1=33;key2=product-02,value2=33;key3=product-03,value3=33;

其实会有几个问题:


用户想买34件的时候,要去两个片查

一个片上卖完了为0,又要去另外一个片查

取余方式计算每一片数量,除不尽时,让最后一片补,如100/3=33.33。

缺点:方案复杂,有遗留问题

优点:比单纯的分布式锁,性能要好

(推荐方案)Redis原子操作(Redis incr)+乐观锁

先查询redis中是否有库存信息,如果没有就去数据库查,这样就可以减少访问数据库的次数。

获取到后把数值填入redis,以商品id为key,数量为value。

还需要设置redis对应这个key的超时时间,以防所有商品库存数据都在redis中。


1.比较下单数量的大小,如果够就做后续逻辑。

2.执行redis客户端的increment,参数为负数,则做减法。

有的人会不做第一步查询直接减,其实这样不太好,因为当库存为1时,很多做减3,或者减30情况,其实都是不够,这样就白减。

扣减数据库的库存,这个时候就不需要再select查询,直接乐观锁update,把库存字段值减1 。

做完扣库存就在订单系统做下单。

样例场景:

假设两个用户在第一步查询得到库存等于10,A用户走到第二步扣10件,同时一秒内B用户走到第二部扣3件。

因为redis单线程处理,若A用户线程先执行redis语句,那么现在库存等于0,B就只能失败,就不会去更新数据库了。

 public void order(OrderReq req) {
        String key = "product:" + req.getProductId();
        // 第一步:先检查 库存是否充足
        Integer num = (Integer) redisTemplate.get(key);
          if (num == null){
          // 去查数据库的数据
          // 并且把数据库的库存set进redis,注意使用NX参数表示只有当没有redis中没有这个key的时候才set库存数量到redis
          //注意要设置序列化方式为StringRedisSerializer,不然不能把value做加减操作
          // 同时设置超时时间,因为不能让redis存着所有商品的库存数,以免占用内存。
           if (count >=0) {
            //设置有效期十分钟
            redisTemplate.expire(key, 60*10+随机数防止雪崩, TimeUnit.SECONDS);
        }
          // 减少经常访问数据库,因为磁盘比内存访问速度要慢
        }
        if (num < req.getNum()) {
            logger.info("库存不足");
        }
        // 第二步:减少库存
        long value = redisTemplate.increment(key, -req.getNum().longValue());
        // 库存充足
        if (value >= 0) {
            logger.info("成功购买");
            // update 数据库中商品库存和订单系统下单,订单的状态为待支付
            // 也可以使用最终一致性的方式,更新库存成功后,发送mq,等待订单创建生成回调。
            boolean res= updateProduct(req);
              if (res)
                createOrder(req);
        } else {
            // 减了后小小于0 ,如两个人同时买这个商品,导致A人第一步时看到还有10个库存,但是B人买9个先处理完逻辑,
            // 导致B人的线程10-9=1, A人的线程1-10=-9,则现在需要增加刚刚减去的库存,让别人可以买1个
            redisTemplate.increment(key, req.getNum().longValue());
            logger.info("恢复redis库存");
        }
    }

updateProduct方法中执行的sql如下:

update Product set count = count - 购买数量 where id = 商品id and count - 购买数量 >= 0 and version = 查到的version;

虽然redis已经防止了超卖,但是数据库层面,也要使用乐观锁防止超卖,以防redis崩溃时无法使用或者不需要redis处理时,因为不一定全部商品都用redis。

LUA脚本保持库存原子性

扣减redis的库存时,最好用lua脚本处理,因为如果剩余1个时,用户买100个,这个时候其实会先把key increase -100就会变负99。

所以用lua脚本先查询数量剩余多少,是否够减100后,再去减100。替换“库存不足”那个判断到incre的那几行代码。

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
目录
相关文章
|
4月前
|
监控 NoSQL Java
Redis之高并发超卖问题解决方案
在高并发的秒杀抢购场景中,常常会面临一个称为“超卖”(Over-Selling)的问题。超卖指的是同一件商品被售出的数量超过了实际库存数量,导致库存出现负数。这是由于多个用户同时发起抢购请求,而系统未能有效地控制库存的并发访问。
150 0
|
8月前
|
缓存 NoSQL Redis
Redis高并发场景下秒杀超卖解决
Redis高并发场景下秒杀超卖解决
274 0
|
7月前
|
NoSQL 关系型数据库 MySQL
聊聊高并发下超卖,少卖的解决方案
聊聊高并发下超卖,少卖的解决方案
166 0
|
10月前
|
存储 SQL 算法
MySQL面试精选:阿里双十一高并发扣减库存就一行SQL语句搞定,Nice!!!
MySQL面试精选:阿里双十一高并发扣减库存就一行SQL语句搞定,Nice!!!
|
SQL 存储 关系型数据库
从零搭建基于SpringBoot的秒杀系统(七):高并发导致超卖问题分析处理
在没有高并发的环境下,做到现在已经算是一个比较完善的后端逻辑了,但是如果同时有1000个请求或者更多请求的时候,就会产生很多问题,包括秒杀最怕的超卖。想一下,秒杀活动本来就是不赚钱甚至是亏钱的活动,如果超卖了,发货就代表亏本,不发货直接影响信用。因此绝不能出现超卖的情况。
|
双11 数据库 黑灰产治理
高并发秒杀系统如何实现正确的扣减库存?
高并发秒杀系统如何实现正确的扣减库存?
389 1
高并发秒杀系统如何实现正确的扣减库存?
|
双11 数据库 黑灰产治理
高并发秒杀系统如何实现正确的扣减库存?
在【精通高并发系列】的《实践出真知:全网最强秒杀系统架构解密!!》一文中,冰河详细的阐述了高并发秒杀系统的架构设计,也简单提到了如何扣减商品的库存。
1684 0
高并发秒杀系统如何实现正确的扣减库存?
|
SQL 缓存 前端开发
|
PHP 数据库 消息中间件
PHP解决抢购、秒杀、抢楼、抽奖等阻塞式高并发库存防控超量的思路方法
如今在电商行业里,秒杀抢购活动已经是商家常用促销手段。但是库存数量有限,而同时下单人数超过了库存量,就会导致商品超卖甚至库存变负数的问题。 又比如:抢购火车票、论坛抢楼、抽奖乃至爆红微博评论等也会引发阻塞式高并发问题。如果不做任何措施可能在高瞬间造成服务器瘫痪,如何解决这个问题呢?
2098 0
|
缓存 关系型数据库 MySQL
MySQL处理高并发,防止库存超卖
一般电子商务网站都会遇到如团购、秒杀、特价之类的活动,而这样的活动有一个共同的特点就是访问量激增、上千甚至上万人抢购一个商品。然而,作为活动商品,库存肯定是很有限的,如何控制库存不让出现超买,以防止造成不必要的损失是众多电子商务网站程序员头疼的问题,这同时也是最基本的问题。
5066 0

热门文章

最新文章