怎么用redis做秒杀支撑的demo

用redis做秒杀的库存扣除, 限制每个账号只能抢购一次, 这个简单的demo使用了string, hash, list三种基本类型.

  • 用string类型的int值来存储剩余库存, 并在抢购成功后减1

  • 用hash来存储"
    已抢购到"
    的会员的id(可以确保用户id作为field的唯一性). 注意: 这个hash的field对应的uid不一定抢购成功

  • 用list来保存真正抢购成功的会员id的列表, 作为后续处理订单的队列

用redis实现秒杀——打造高效稳定秒杀系统的必备方法

第一次写的时候, 尝试过使用string的bitmap来保存该会员是否抢购成功过, 但是这个在高并发时会出问题, 所以后来换成了唯一field的hash

2个文件:

  • init.php: 初始化库存, 统计数据, 抢购成功的会员列表等

  • buy.php: 抢购

初始化

ini.php:

$m_redis = new YourRedisClass();
//redis类很多, 可以自己写, 也可以用predis等
$m_redis->
set('
rush_stock'
, 20);
//int, 可抢购的商品总数
$m_redis->
set('
rush_success'
, 0);
//int, 成功的数量
$m_redis->
set('
rush_fail'
, 0);
//int, 失败的数量
$m_redis->
expire('
rush_queue_h'
, 0);
//hash, 已加入抢购队列的会员的hash记录表(field是唯一的, 可限制每个uid只有一次), 不一定抢购成功
$m_redis->
set('
rush_got_uid'
, '
'
);
//string, 抢购成功的会员uid记录, 只是为了能简单的显示抢到的会员.
$m_redis->
del('
rush_got_uid_l'
);
//list, 抢购成功的会员uid(方便抢购后的订单批次处理)
echo '
success, '
.date('
Y-m-d H:i:s'
);

执行本文件, 初始化数量.

redis-cli 下执行 "
mget rush_stock rush_fail rush_success rush_got_uid"
确认初始化数据

秒杀

判断的逻辑:

  • 库存是否为0, 库存>
    0则进入抢购队列

  • 抢购队列数据(hash)写入成功, 则准备扣减库存

  • 库存扣减成功(余数>
    =0)则抢购成功, 进入订单处理队列(list)目前是用string int存储库存, 也可以用list的item的个数来计数, 但是初始化时没有string类型来得简单.

buy.php

//随机生成会员id
$uid = rand(1,200);


$m_redis = new YourRedisClass();
//redis类很多, 可以自己写, 也可以用predis等

$key = '
rush_stock'
;

$q = $m_redis->
get($key);


//1. 先判断库存数量
//库存为0, 直接无法进入抢购队列
if($q <
1){
$m_redis->
incr('
rush_fail'
);
//记录失败的数量
die($uid.'
:OutOfStock'
);

}

//2. 判断该会员是否购买过 =>
是否进入过队列
$queued = $m_redis->
hSet('
rush_queue_h'
, $uid, $uid);
//这里只能判断是否进入了抢购的队列. 如果库存为0则无法进入. 进入了队列后才能抢购
if(!$queued){
$m_redis->
incr('
rush_fail'
);
//记录失败的数量
die($uid.'
:queue failed'
);

}

//让cpu飞一会
$n = rand(20000,100000);

for($i=0;
$i <
$n;
$i++){
$a = rand(1,20000);

$a = rand(1,30000);

$a = rand(1,40000);

$a = rand(1,50000);

$a = rand(1,60000);

$a = rand(1,70000);

$a = rand(1,80000);

$a = rand(1,90000);

}


//3. 扣减数量
$q = $m_redis->
decr($key, 1);
//扣减数量后会返回结果值
echo $q.'
left:'
;



////region 如果不判断操作后返回的结果,则可能会造成超发
//$m_redis->
incr('
q_success'
);
//记录成功的数量 ==>
这个是有bug的, 不可取
//die('
:success'
);

////endregion

if($q <
0){
$m_redis->
incr('
rush_fail'
);
//记录失败的数量
die($uid.'
:decrease fail'
);

}else{
//记录成功的数量
$m_redis->
incr('
rush_success'
);

//记录该会员已购买
$m_redis->
append('
rush_got_uid'
, $uid.'
,'
);
//字符串追加
$m_redis->
rPush('
rush_got_uid_l'
, $uid);
//list
die($uid.'
:success'
);

}

上面的代码中的hash保存的会员uid, 只是进入抢购队列的会员uid, 不一定抢购成功了, 那些根本没有进入抢购队列的, 也不会在这个hash中, 直接因为库存为0而被拒绝了.

AB压力测试: 做一个简单的500个并发并总计尝试2000次的请求(测试时, win10下600个并发Nginx就挂机了)

Apache路径bin>
ab -n 2000 -c 500 http://xxx.com/buy.php

redis-cli下执行 "
mget rush_stock rush_fail rush_success rush_got_uid"
确认结果, 通过 rush_stock 的值查看可能的超发的数量

执行 "
hvals rush_queue_h"
可查看进入抢购队列的用户id, 这个数量 >
= 抢购成功的用户数量

对于list队列的数据操作, 可以使用 BLPOP 命令, 这样可以实现FIFO的数据处理顺序.



秒杀是电商经常使用的促销方式,但随之而来的高并发和大流量也让秒杀系统的实现变得复杂,通过使用redis,可以轻松实现秒杀系统的高效稳定。接下来,我们将介绍如何用redis实现秒杀的demo。
一、 构建秒杀系统所需的数据库
要实现秒杀系统,需要建立两个数据库,一个用来存储秒杀商品库存(stock),一个用来存储成功抢购记录(record)。其中,redis的hash类型能很好地解决秒杀库存的快速更新。购买成功记录则用set作为存储类型,保证每个用户在抢购中只能购买一次。
二、 实现秒杀系统的队列
为了避免高并发情况下的资源竞争,应使用队列技术,让系统按照顺序依次执行请求。这里可以使用redis的list类型实现队列,将用户请求放入队列,然后从队列头部取出用户请求,如果当前库存大于0,则进行购买,否则加入失败记录集合,弹出队列,继续处理下一个请求。
三、 缓存预热与数据同步
预热缓存可以有效避免突然的高并发导致数据库连接数增加,使数据库崩掉的情况。数据同步则确保了各个节点之间秒杀系统数据的一致性。使用redis的publish/subscribe机制,非常简单地实现了数据的异步传输。
通过以上方法,我们能够很好地实现秒杀系统,达到系统内部的高并发处理与数据更新,使系统在高流量下仍然能够顺利运行。这些细节无不展现了redis在秒杀场景下的优秀表现,相信它将会为你的秒杀系统提供很大的帮助。