如何正确使用redis队列处理php秒杀并发问题?

NightBear
  • 401

百度之后,找到这样的思路:
需要一个排队队列和抢购结果队列及库存队列。高并发情况,先将用户进入排队队列,用一个线程循环处理从排队队列取出一个用户,判断用户是否已在抢购结果队列,如果在,则已抢购,否则未抢购,库存减1,写数据库,将用户入结果队列。

‘先将用户进入排队队列,用一个线程循环处理从排队队列取出一个用户’
到底如何:"用一个线程循环处理",我就不明白该如何下手了,啥时候开启这个"线程"?

是不是每次用户进行抽奖,访问主程序就立即启动这个"线程"进行循环处理队列数据?

还是说,用户点击抽奖按钮,触发的是执行入队操作,
然后,在服务器会有一个以cli方式启动的独立进程,在长时间订阅监听队列的情况,如果有数据就执行用户队列的出列,然后再走下单操作?

入队和出列,应该是两个不同的程序调用的吧?那如何在用户一访问进行抽奖,就能顺利实现入队和出列呢,如何在代码层面安排好这些调用动作的呢?

回复
阅读 14.1k
9 个回答
黒之染
  • 3.1k
✓ 已被采纳

以下方案建议不要用了,rabbitMQ安装方便又好用,比redis做队列直接多了,还不用考虑大部分的问题。

"用一个线程循环处理",我就不明白该如何下手了,啥时候开启这个"线程"?

  • 我的方案是,先写好一个专门处理队列中的数据的程序,用定时任务(linux的crontab)每分钟调用一次此程序。

此程序使用blPop或者brPop堵塞获取list的数据(这两个方法的区别可看redis文档得知),要堵塞的原因是,万一这一分钟没有数据,过了30秒后数据进来了,就要再等30秒才能处理。堵塞的最大时间我设了59秒。同时为了兼容大量数据的情况,此程序会循环从list读数据,每次读数据用的都是堵塞方式,所以每次堵塞的时间都会根据当前程序运行的时长动态改变。理论上如果业务不复杂,这个程序运行一次不会超过60秒,也就达到要求了,如果超过了60秒,也会自动新增线程来执行下一次循环。

你接下来的问题有点不理解,你是了解大的流程的,我说一下这个流程里的细节吧

我是用redis的list的,其它答案里有提到用锁,如果是你这个方案,完全不需要用锁,因为list已经为你解决了并发问题。

只要用户点了“抢”,你就把用户的信息lPush或者rPush进一个list,这时会返回一个int,就是告诉你这个操作之后,list里有多少条数据了,这个int是线程安全的,即使再高的并发,也不会造成这个int对于这个用户来说已经过时,所以你可以判断这个int有没有超过库存,如果超过了,直接告诉前端这个用户错过秒杀了,如果否,则让前端等待抢购结果。(此时并不需要把超过库存的用户从list里删除。库存数建议在秒杀前查询出来放到redis中,之后也不要修改redis的库存数,因为这个库存数是专门用于跟list长度做对比的)

接下来的步骤就根据不同的业务需求了,如果接下来要用户填写补充信息,则最简单了:写一个接口接收用户补充的信息,查询list中用户排第几,跟库存对比一下他是不是真的秒杀到了,然后做入库操作。

如果接下来没有让用户操作的需要了,则跟上面回答你第一个疑问那样,写一个堵塞轮询的接口。

Redis队列可以设置队列长度,当一个用户连接到服务器,Redis队列长度+1,注意队列的数据结构是先进先出的进行操作。当队列长度达到你设定的长度时候,禁止Redis入队操作。

高并发时可以使用锁控制抢购或者抽奖,获取到锁了就可以进行购买等行为,获取不到锁就立刻返回,像你说的抢购的人都加入抢购队列,那抢购就有顺序性了,其本身并无顺序性,不能等先来的先抢购,抢购的人有上千万,要造一个上千万的队列吗,再说队列处理到最新加入队列的,那客户不知要等多久了

$ttl = 4;
$random = mt_rand(1,1000).'-'.gettimeofday(true).'-'.mt_rand(1,1000);

$lock = fasle;
while (!$lock) {
    $lock = $redis->set('lock', $random, array('nx', 'ex' => $ttl));
}

if ($redis->get('goods.num') <= 0) {
    echo ("秒杀已经结束");
    //删除锁
    if ($redis->get('lock') == $random) {
        $redis->del('lock');
    }
    return false;
}

$redis->decr('goods.num');
echo ("秒杀成功");
//删除锁
if ($redis->get('lock') == $random) {
    $redis->del('lock');
}
return true;

来源 http://coffeephp.com/articles...

落日花园
  • 2
新手上路,请多包涵

我也很想知道这个答案

deer13
  • 2
新手上路,请多包涵

秒杀不需要这么复杂吧,只需要有一个唯一锁,能进行原子增加,然后根据回馈的抢购数来判断是否超过了最大量,超过了就提示没有数量就好了吧

如果是1秒钟post成百上千次的话,我的想法是:

  1. 先做插入,包含毫秒级时间戳,或者直接用自增ID
  2. 然后排序判断插入的是第几个,如果超过总数量,则抢购失败,更改抢购为已抢完,
用一个线程循环处理",我就不明白该如何下手了,啥时候开启这个"线程"
  1. 这个在秒杀开启前开始执行就行,执行过程贯穿整个秒杀过程,可以是几个进程也可以是一个进程一直跑,这是出队的过程。入队的话就是PHP惯有模式,每次一个请求进来自动启动进程,往队列扔数据。
  2. 然后我们要明白这个队列的意义是什么,redis的意义是什么。redis是用来扛并发用的,通过一个计数器,先查询还有库存就执行入队,库存扣完了就直接return。redis扛了一层后进队列数据已经量级小很多了,队列的作用本质是帮DB扛并发用的,使得DB事务执行全部串行化,避免锁的争抢降低DB性能。
enyccc
  • 7

我是这么干的。
比如你有1000个秒杀商品
设计就这样,

大家到等待秒杀的页面,点击秒杀按钮,然后ajax请求一个api接口,这个接口的核心代码就是

$number = $redis->incr($key);
if ($number > 1000) {
    return '抢光了';
} else {
    $_SESSION['flag'] = 1;
    return redirect('跳转到下一个页面');
}

这样抢到的人就可以在下个页面慢吞吞的填写资料,没抢到的人就可以在当前页面死心。

宣传栏