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

摘要:
简要描述闪购/秒杀是当今常见的应用场景。如何解决高并发竞争下的超级快照问题?在高度并行的发展中,许多看似不可能的问题变成了实际问题。我们需要使用Redis的原子操作来实现这个“单线程”。当列表为空时,表示已被抢劫。为了测试实际效果,我使用jmeter工具模拟了100、500和1000个并发用户进行抢购。经过大量测试后,成功用户的数量始终为10,没有出现“超卖”。

简述

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

常规写法:

查询出对应商品的库存,看是否大于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:

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

 hash:

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

分析: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个库存

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

$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个用户同时进入秒杀

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

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

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

 使用jemter测试结果

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

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

结果也是符合预期的

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

先补充仓库

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

 所有抢购用户

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

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

 真正抢购到

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

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

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

免责声明:文章转载自《简单实现redis实现高并发下的抢购/秒杀功能(转)》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇centos7使用dnf命令时出现ImportError: No module named _conf错误qml(Qt Quick)做界面下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

redis 在 php 中的应用(key篇)

本文为我阅读了redis参考手册之后结合博友的博客编写,注意 php_redis 和 redis-cli 的区别(主要是返回值类型和参数用法) 目录: KEY(键) DEL EXISTS EXPIRE EXPIREAT keys MOVE PERSIST TTL RANDOMKEY RENAME RENAMENX TYPE SORT KEY(...

Linux下常用redis指令

set get 取值赋值,key值区分大小写 mset mget多个的取值和赋值 del 删除一个或多个key 查看key值是否存在,有就返回1,没有就是0 设置一个有存活时间的key值,设置成功返回1 显示key值剩余存活时间 keys显示符合指定条件的key 创建相关数据类型,并使用keys显示对应的数据类型 创建hash类型的数据...

使用redis作为消息队列的用法

背景 最近项目有个需求需要动态更新规则,当时脑中想到的第一个方案是利用zk的监听机制,管理人员更新完规则将状态写入zk,集群中的机器监听zk的状态,当有状态变更后,集群中的机器开始拉取最新的配置。但由于公司技术选型,没有专门搭建zk集群,因此也不可能为这一个小需求去搭建zk集群。图为使用zk监听状态变化的流程。 最后只好退而求其次,想到了使用redis的...

设置Django生产环境系统重启后的自动启动项

前面,作者已经介绍了把Django部署到生产环境中的主要方法,现在我们来看一下如何设置项目开机启动。 在把Django项目部署到生产环境中时,我们前面使用安装包和源码安装了Nginx、uwsgi、redis等,这些应用安装后,不会使用系统默认服务一样来快速启停服务,需要到对应的安装目录下才能启动应用。如果服务因为某些原因重启,上述应用不能自动启用,实际生产...

深入理解Spring Redis的使用 (八)、Spring Redis实现 注解 自动缓存

项目中有些业务方法希望在有缓存的时候直接从缓存获取,不再执行方法,来提高吞吐率。而且这种情况有很多。如果为每一个方法都写一段if else的代码,导致耦合非常大,不方便后期的修改。 思来想去,决定使用自动注解+Spring AOP来实现。 直接贴代码。 自定义注解类: package com.ns.annotation; import java.lang...

Redis性能篇(三)Redis关键系统配置:如何应对Redis变慢

Redis被广泛使用的一个很重要的原因是它的高性能。因此我们必要要重视所有可能影响Redis性能的因素、机制以及应对方案。影响Redis性能的五大方面的潜在因素,分别是: Redis内部的阻塞式操作 CPU核和NUMA架构的影响 Redis关键系统配置 Redis内存碎片 Redis缓冲区 在前面的2讲中,学习了会导致Redis变慢的潜在阻塞点以及相应...