• 简单实现redis实现高并发下的抢购/秒杀功能(转)


    简述

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

    常规写法:

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

    这里我就只谈redis的解决方案

    我们先来看以下php代码是否能正确解决超抢/卖的问题:

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

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

     $len = $redis->llen('order:1');  
    
     if($len >= $num)
       return '已经抢光了哦';

    在抢购进行到一定程度,假如现在已经有9个人抢购成功,又来了3个用户同时抢购,这时if条件将会被绕过(条件同时被满足了),这三个用户都能抢购成功。而实际上只剩下一件库存可以抢了。
    在高并发下,很多看似不大可能是问题的,都成了实际产生的问题了。要解决“超抢/超卖”的问题,核心在于保证检查库存时的操作是依次执行的,再形象的说就是把“多线程”转成“单线程”。即使有很多用户同时到达,也是一个个检查并给与抢购资格,一旦库存抢尽,后面的用户就无法继续了。
    我们需要使用redis的原子操作来实现这个“单线程”。首先我们把库存存在goods_store:1这个列表中,假设有10件库存,就往列表中push10个数,这个数没有实际意义,仅仅只是代表一件库存。抢购开始后,每到来一个用户,就从goods_store:1中pop一个数,表示用户抢购成功。当列表为空时,表示已经被抢光了。因为列表的pop操作是原子的,即使有很多用户同时到达,也是依次执行的。抢购的示例代码如下:
    比如这里我先把库存(假设有10件)放入redis队列:

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

    好吧,抢购时间到了:

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

    注意:这里可以不必进行数据库操作,而是先存入队列,操作数据库的时间是跟用户无关的,所以应该立马返回让用户知道是否抢到,之后再对这个队列进行操作。

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

    问题

    上面虽然能够解决超卖的现象,但是却不能够防止超抢的情况发生,就是一个用户可以抢 到相同的多件商品

    尝试解决

    $data = $redis->lRange('order:1',0,-1);  //把抢到的用户存入到列表中
    if(!in_array($user_id,$data))
    {
        $count=$redis->lpop('goods_store:1');
        if(!$count)
          return '已经抢光了哦';
    
        $result = $redis->lpush('order:1',$user_id);
        if($result)
          return '恭喜您!抢到了哦';
    }
    else
    {
        return '已经抢购了哦';
    }

    上面这个代码在没有高并发情况下测试没有问题,但是如果高并发情况下呢。答案是不可以的。这跟上面的

     $len = $redis->llen('order:1');  
    
     if($len >= $num)
       return '已经抢光了哦';

    是一样道理的,所以行不通

    这时有人提出了

    $data = $redis->lRange('order:1',0,-1);  //把抢到的用户存入到列表中
    if(!in_array($user_id,$data))
    ...

    这两行代码不就是判断list中某个值是否存在吗?为何不直接调用list的exist函数判断,我刚开始也照着这样去查找。不过并没有找到这个内置函数。而我也自己写了一个函数判断是否存在list中,伪代码如下

    if(调用函数判断id是否存在list中)
     {
      $count=$redis->lpop('goods_store:1');
      if(!$count)
        return '已经抢光了哦';
      $result = $redis->lpush('order:1',$user_id);
      if($result)
        return '恭喜您!抢到了哦';
      }
     else
     {
        return '已经抢购了哦';
     }

    不过答案还是不行的,因为如果同时有两个相同id进入if判断,还是都会进入到if中。

    解决

    这时我将list转成了hash,将代码改成了下面

    //把所有用户都插入到这个队列中
    $wait_key = "user_wait:2";
    
    //真正抢到的用户信息队列
    $user_key = "user:1";
    
    //库存队列
    $store_key = "goods_store:1";
    
    $result =$redis->hset($wait_key, $user_id, $user_id);
        if($result)
        {
            $count = $redis->lpop($store_key);
            if (!$count)
                echo '已经抢光了哦'.$user_id;
            else
            {
                $result =$redis->hset($user_key, $user_id, $user_id);
                echo '恭喜您!抢到了哦'.$user_id;
            }
        }

    这时又有人说这样干嘛不可以用list,代码如下:

    //把所有用户都插入到这个队列中
    $wait_key = "user_wait:2";
    
    //真正抢到的用户信息队列
    $user_key = "user:1";
    
    //库存队列
    $store_key = "goods_store:1";
    $result =$redis->rPush($wait_key, $user_id);
        if($result)
        {
            $count = $redis->lpop($store_key);
            if (!$count)
                echo '已经抢光了哦'.$user_id;
            else
            {
                $result =$redis->rPush($user_key,  $user_id);
                echo '恭喜您!抢到了哦'.$user_id;
            }
        }

    对比

    list:

     hash:

    分析:list中的值是可以重复的,而hash里面的值是不可以重复的

    所以

    $result =$redis->rPush($wait_key, $user_id);

    $result =$redis->hset($wait_key, $user_id, $user_id);

    当高并发的的情况下,无论id是否相同,list的rpush返回结果都是1,而hash的hset只有不同的时候才返回1.这样就可以避免由于高并发而导致一个用户抢到多件同种商品

    测试结果

    先加入10个库存

    $user_id =  1;//当前抢购用户id
    
    $wait_key = "user_wait:2";
    
    $user_key = "user:1";
    
    $store_key = "goods_store:1";
    
        $result =$redis->hset($wait_key, $user_id, $user_id);
        if($result)
        {
            $count = $redis->lpop($store_key);
            if (!$count)
                echo '已经抢光了哦'.$user_id;
            else
            {
                $result =$redis->hset($user_key, $user_id, $user_id);
                echo '恭喜您!抢到了哦'.$user_id;
            }
        }
        else
        {
            echo '已经抢到了'.$user_id;
        }

    为了测试极限高并发情况下,我直接将用户Id设置为1

    我这里模拟1000个用户同时进入秒杀

     如果秒杀成功,应该是库存为9,而真正的抢购队列只有用户1

     使用jemter测试结果

    结果也是符合预期的

    现在来模拟真正不同id看看是否只是10个用户能抢到

    先补充仓库

     

     所有抢购用户

     

     真正抢购到

     至此,已经算是实现了预期

    转自:https://blog.csdn.net/qq_33862778/article/details/80651703

  • 相关阅读:
    event loop笔记
    webpack不同devtools打包对比
    windows 7系统搭建本地SVN服务器的过程
    Eclipse安装SVN插件
    总结eclipse中安装maven插件
    myeclipse building workspace如何禁止及提高myeclipse速度
    基于SAML的单点登录介绍
    使用 CAS 在 Tomcat 中实现单点登录
    SSO之CAS备忘
    Maven环境变量配置
  • 原文地址:https://www.cnblogs.com/shiliuye/p/13294593.html
Copyright © 2020-2023  润新知