• redis实现高并发下的抢购/秒杀功能


    抢购/秒杀是如今很常见的一个应用场景,那么高并发竞争下如何解决超抢(或超卖库存不足为负数的问题)呢?

    常规写法:

    查询出对应商品的库存,看是否大于0,然后执行生成订单等操作,但是在判断库存是否大于0处,如果在高并发下就会有问题,导致库存量出现负数

    这里我就只谈redis的解决方案吧...
    我们先来看以下代码(这里我以laravel为例吧)是否能正确解决超抢/卖的问题:

    <?php
     
     $num = 10;   //系统库存量
     $user_id =  Session::get('user_id');//当前抢购用户id
     $len = Redis::llen('order:1');  //检查库存,order:1 定义为健名
     if($len >= $num)
       return '已经抢光了哦';
    
    $result = Redis::lpush('order:1',$user_id);  //把抢到的用户存入到列表中
    if($result)
      return '恭喜您!抢到了哦';
    

    如果代码正常运行,按照预期理解的是列表order:1中最多只能存储10个用户的id,因为库存只有10个。
    然而,但是,在使用jmeter工具模拟多用户并发请求时,最后发现order:1中总是超过5个用户,也就是出现了“超抢/超卖”。
    分析问题就出在这一段代码:

    $len = Redis::llen('order:1');  //检查库存,order:1 定义为健名
    if($len >= $num)
       return '已经抢光了哦';
    

    在抢购进行到一定程度,假如现在已经有9个人抢购成功,又来了3个用户同时抢购,这时if条件将会被绕过(条件同时被满足了),这三个用户都能抢购成功。而实际上只剩下一件库存可以抢了。

    在高并发下,很多看似不大可能是问题的,都成了实际产生的问题了。要解决“超抢/超卖”的问题,核心在于保证检查库存时的操作是依次执行的,再形象的说就是把“多线程”转成“单线程”。即使有很多用户同时到达,也是一个个检查并给与抢购资格,一旦库存抢尽,后面的用户就无法继续了。

    我们需要使用redis的原子操作来实现这个“单线程”。首先我们把库存存在goods_store:1这个列表中,假设有10件库存,就往列表中push10个数,这个数没有实际意义,仅仅只是代表一件库存。抢购开始后,每到来一个用户,就从goods_store:1中pop一个数,表示用户抢购成功。当列表为空时,表示已经被抢光了。因为列表的pop操作是原子的,即使有很多用户同时到达,也是依次执行的。抢购的示例代码如下:
    比如这里我先把库存(可用库存,这里我强调下哈,一般都是商品详情页抢购,后来者进来看到的库存可能不再是后台系统配置的10个库存数了)放入redis队列:

     $num=10; //库存
     $len=Redis::llen('goods_store:1'); //检查库存,goods_store:1 定义为健名
     $count = $num-$len; //实际库存-被抢购的库存 = 剩余可用库存
     for($i=0;$i<$count;$i++)
       Redis::lpush('goods_store:1',1);//往goods_store列表中,未抢购之前这里应该是默认滴push10个库存数了
    
     //echo Redis::llen('goods_store:1');//未抢购之前这里就是10了
    

    好吧,抢购时间到了:

     /* 模拟抢购操作,抢购前判断redis队列库存量 */
     $count=Redis::lpop('goods_store:1');//lpop是移除并返回列表的第一个元素。
     if(!$count)
        return '已经抢光了哦';
     /* 下面处理抢购成功流程 */
    DB::table('goods')->decrement('num', 1);//减少num库存字段
    

    用户抢购成功后,上面的我们也可以稍微优化下,比如我们可用将用户ID存入了order:1列表中。接下来我们可以引导这些用户去完成订单的其他步骤,到这里才涉及到与数据库的交互。最终只有很少的人走到这一步吧,也就解决的数据库的压力问题。
    我们再改下上面的代码:

    $user_id =  Session::get('user_id');//当前抢购用户id
    /* 模拟抢购操作,抢购前判断redis队列库存量 */
    $count=Redis::lpop('goods_store:1');
    if(!$count)
      return '已经抢光了哦';
    
    $result = Redis::lpush('order:1',$user_id);
    if($result)
      return '恭喜您!抢到了哦';
    
    

    不过这里还存在一个问题就是一个用户抢购多次,我们继续优化代码,将抢购成功的用户放入set中,并判断新抢购的用户是否已经在set中。若存在则返回已经抢购成功的提示。

    $user_id =  Session::get('user_id');//当前抢购用户id
    /* 模拟抢购操作,抢购前判断redis队列库存量 */
    $count=Redis::lpop('goods_store:1');
    if(!$count)
      return '已经抢光了哦!';
    $exist_user = Redis::sIsMember('order:1',$user_id);
    if($exist_user)
          return '已经抢购成功了哦!';
    $result = Redis::sAdd('order:1',$user_id);
    if($result)
      return '恭喜您!抢到了哦';
    

    为了检测实际效果,我使用jmeter工具模拟100、200、1000个用户并发进行抢购,经过大量的测试,最终抢购成功的用户始终为10,没有出现“超抢/超卖”。

    上面只是简单模拟高并发下的抢购思路,真实场景要比这复杂很多,比如双11活动远远比这更复杂多啦,很多注意的地方如抢购活动页面做成静态的,通过ajax调用接口等等。

  • 相关阅读:
    Laravel5.0学习--01 入门
    MySQL账户安全设置
    360路由器c301最新固件支持万能中继
    PsySH:PHP交互运行环境
    PHP-CS-Fixer:格式化你的PHP代码
    JVM 类加载机制详解
    Java虚拟机(JVM)概述
    聊一聊 Spring 中的线程安全性
    Java 里如何实现线程间通信
    Java 数据结构
  • 原文地址:https://www.cnblogs.com/caibaotimes/p/14419864.html
Copyright © 2020-2023  润新知