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

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容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
目录
相关文章
|
6月前
|
监控 NoSQL Java
Redis之高并发超卖问题解决方案
在高并发的秒杀抢购场景中,常常会面临一个称为“超卖”(Over-Selling)的问题。超卖指的是同一件商品被售出的数量超过了实际库存数量,导致库存出现负数。这是由于多个用户同时发起抢购请求,而系统未能有效地控制库存的并发访问。
456 0
|
数据采集 并行计算 Java
【文末送书】Python高并发编程:探索异步IO和多线程并发
【文末送书】Python高并发编程:探索异步IO和多线程并发
237 0
|
缓存 NoSQL Redis
Redis高并发场景下秒杀超卖解决
Redis高并发场景下秒杀超卖解决
403 0
|
4月前
|
监控 应用服务中间件 nginx
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
高并发架构设计三大利器:缓存、限流和降级问题之Nginx的并发连接数计数的问题如何解决
|
4月前
|
设计模式 安全 NoSQL
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
Java面试题:设计一个线程安全的单例模式,并解释其内存占用和垃圾回收机制;使用生产者消费者模式实现一个并发安全的队列;设计一个支持高并发的分布式锁
65 0
|
4月前
|
设计模式 存储 缓存
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
Java面试题:结合建造者模式与内存优化,设计一个可扩展的高性能对象创建框架?利用多线程工具类与并发框架,实现一个高并发的分布式任务调度系统?设计一个高性能的实时事件通知系统
52 0
|
6月前
|
缓存 架构师 算法
高并发系统简单玩!Alibaba全新出品亿级并发设计速成笔记真香
如何提升系统性能,设计出一个靠谱的系统是每一个架构师或者正在往架构师方向进阶的同僚们都需要考虑的问题。公司所处的行业,业务场景决定了你设计的系统演进过程,不过万变不离其宗,系统设计和优化的思想都是相通的(当然如果你刚入行没多久,目前肯定还不需要苦恼这种问题,但是工作用不到,不代表面试不问)。
|
6月前
|
消息中间件 Java 程序员
阿里巴巴高并发架构到底多牛逼?是如何抗住淘宝双11亿级并发量?
众所周知,在Java的知识体系中,并发编程是非常重要的一环,也是面试的必问题,一个好的Java程序员是必须对并发编程这块有所了解的。
|
NoSQL 关系型数据库 MySQL
聊聊高并发下超卖,少卖的解决方案
聊聊高并发下超卖,少卖的解决方案
381 0
|
存储 SQL 算法
MySQL面试精选:阿里双十一高并发扣减库存就一行SQL语句搞定,Nice!!!
MySQL面试精选:阿里双十一高并发扣减库存就一行SQL语句搞定,Nice!!!

热门文章

最新文章

下一篇
无影云桌面