redis秒杀
Redis原子性原理
摘要:
一、Redis是单进程单线程的网络模型,用的是epoll,poll,select网络模型,这些网络模型都是单线程处理网络请求
二、Redis的单线程处理全部的客户端链接请求,命令读写请求。(有些任务好比rdb和aof等操做是fork子进程处理的,不会影响redis主线程处理客户端的命令)
三、Redis提供的全部API操做,相对于服务端方面都是one by one执行的,命令是一个接着一个执行的,不存在并行执行的状况。
四、Redis客户端就可能会出现高并发出现错误的读写数据,下面咱们举个电商秒杀的例子来说解一下。
Redis在并发中的表现
Redis的API是原子性的操做,那么多个命令在并发中也是原子性的吗?
看看下面这段代码:
<?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 '已经抢光了哦';
虽然llen和lpush2个操做若是单独执行是具有原子性的,然而上面这个业务2个命令组合起来才算是完成一个业务,可是2个命令组合起来就不具有原子性,全部在两个命令之间其余客户端会出现读写脏数据的状况。
在抢购进行到必定程度,假如如今已经有9我的抢购成功,又来了3个用户同时抢购,三个用户假设有前后顺序,可是在第一个用户lpush到redis以前,其余的两个用户其实已经获取push以前的长度9了,这时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 '恭喜您!抢到了哦';
为了检测实际效果,我使用jmeter工具模拟100、200、1000个用户并发进行抢购,通过大量的测试,最终抢购成功的用户始终为10,没有出现“超抢/超卖”
----------------