laravel异步事件中`shouldQueue`存在的问题

测试异步队列事件shouldQueue存在的问题,方法如下:

  • shouldQueue添加判断条件,值小于5继续操作
  • handle中阻塞操作,并添加条件

代码如下:

<?php

namespace App\Listeners\Test;

use App\Events\Test\RollEvent;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Support\Facades\Redis;

class CapterBEventListener implements ShouldQueue
{
    use InteractsWithQueue;

    public function handle(RollEvent $event)
    {
        $redis = Redis::class;
        $num   = $redis::incr('capter_test');

        echo '执行:',$num,PHP_EOL;
        $redis::expire('capter_test', 3600);

        sleep(10);
        echo '结束',PHP_EOL;
    }

    public function shouldQueue(RollEvent $event) 
    {
        $redis = Redis::class;
        $num   = $redis::exists('capter_test') ? (int)$redis::get('capter_test') : 0;

        return $num < 5;
    }
}

测试如下:

  • 执行结果跑了10遍
  • 预期应该是跑4遍后停止
    $ php artisan tinker
    >>> for($i = 0; $i < 10; $i++) event(new App\Events\Test\RollEvent(5))

我想可能有人会说,那么你把判断条件放到handle中不就好了吗?例如:

    public function handle(RollEvent $event)
    {
        $redis = Redis::class;
        $num   = $redis::exists('capter_test') ? (int)$redis::get('capter_test') : 0;

        if ($num < 5) 
        {
            // doit...
        }
    }

那么不就回到主题了么,shouldQueue方法不是很鸡肋么?还有存在的意义么?同步事件又不支持,异步事件又存在问题?

上面我用redis是为了便于举例,同理,比如操作mysql数据库,或者发起异步请求,同样会出现类似的问题

《L01 基础入门》
我们将带你从零开发一个项目并部署到线上,本课程教授 Web 开发中专业、实用的技能,如 Git 工作流、Laravel Mix 前端工作流等。
《G01 Go 实战入门》
从零开始带你一步步开发一个 Go 博客项目,让你在最短的时间内学会使用 Go 进行编码。项目结构很大程度上参考了 Laravel。
最佳答案

你大概对 shouldQueue 方法有些误解,shouldQueue 是决定是否将任务放入队列。

在你执行循环

for($i = 0; $i < 10; $i++) event(new App\Events\Test\RollEvent(5))

的时候,这10个次时间是同一时间触发的,并不会因为你在 handle() 方法中添加了 sleep() 就会阻塞,sleep() 只会在队列执行时阻塞,但因为是异步,所以这里是没意义的。

事件的触发流程大概是这样:

  1. for 循环触发10个事件
  2. 当 i 等于 0 时 shouldQueue 条件成立,队列被执行
  3. 当 i 等于 1 时,前一个的队列刚刚被分发出去,还没执行完,redis获取到的值还是 0 ,条件成立
  4. 以此类推,所以 10 次循环条件都成立。

怎么来验证这一点呢? 在你的代码中添加日志打印

public function shouldQueue(RollEvent $event) 
{
    $redis = Redis::class;
    $num   = $redis::exists('capter_test') ? (int)$redis::get('capter_test') : 0;

    Log::info($num);

    return $num < 5;
}

你会发现 10 次日志的值全部是 0,一瞬间就被打印出来了,但此时你的队列还没执行完。

所以如果你要添加阻塞,应该在 shouldQueuefor 中去添加,而不是在 handle 中添加阻塞,最好是在 for 中添加,这样不会影响 shouldQueue 的逻辑。

for($i = 0; $i < 10; $i++) 
{
    sleep(10);
    event(new App\Events\Test\RollEvent(5))
}

但这样会造成请求阻塞,如果不想请求阻塞,就在 handle 中去动态判断。


结论:

shouldQueue 方法中,不应该以队列中不断变化的值去作为判断,因为队列是异步,而 shouldQueue 是在队列分发之前同步执行的,速度要快于队列,很可能会造成误判。

shouldQueue 方法是没问题的,我们在怀疑一个事情的时候应该自己先仔细求证,不要着急下结论。

1年前 评论
MArtian (作者) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前
讨论数量: 12

你大概对 shouldQueue 方法有些误解,shouldQueue 是决定是否将任务放入队列。

在你执行循环

for($i = 0; $i < 10; $i++) event(new App\Events\Test\RollEvent(5))

的时候,这10个次时间是同一时间触发的,并不会因为你在 handle() 方法中添加了 sleep() 就会阻塞,sleep() 只会在队列执行时阻塞,但因为是异步,所以这里是没意义的。

事件的触发流程大概是这样:

  1. for 循环触发10个事件
  2. 当 i 等于 0 时 shouldQueue 条件成立,队列被执行
  3. 当 i 等于 1 时,前一个的队列刚刚被分发出去,还没执行完,redis获取到的值还是 0 ,条件成立
  4. 以此类推,所以 10 次循环条件都成立。

怎么来验证这一点呢? 在你的代码中添加日志打印

public function shouldQueue(RollEvent $event) 
{
    $redis = Redis::class;
    $num   = $redis::exists('capter_test') ? (int)$redis::get('capter_test') : 0;

    Log::info($num);

    return $num < 5;
}

你会发现 10 次日志的值全部是 0,一瞬间就被打印出来了,但此时你的队列还没执行完。

所以如果你要添加阻塞,应该在 shouldQueuefor 中去添加,而不是在 handle 中添加阻塞,最好是在 for 中添加,这样不会影响 shouldQueue 的逻辑。

for($i = 0; $i < 10; $i++) 
{
    sleep(10);
    event(new App\Events\Test\RollEvent(5))
}

但这样会造成请求阻塞,如果不想请求阻塞,就在 handle 中去动态判断。


结论:

shouldQueue 方法中,不应该以队列中不断变化的值去作为判断,因为队列是异步,而 shouldQueue 是在队列分发之前同步执行的,速度要快于队列,很可能会造成误判。

shouldQueue 方法是没问题的,我们在怀疑一个事情的时候应该自己先仔细求证,不要着急下结论。

1年前 评论
MArtian (作者) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前

你大概对 shouldQueue 方法有些误解,shouldQueue 是决定是否将任务放入队列。

在你执行循环

for($i = 0; $i < 10; $i++) event(new App\Events\Test\RollEvent(5))

的时候,这10个次时间是同一时间触发的,并不会因为你在 handle() 方法中添加了 sleep() 就会阻塞,sleep() 只会在队列执行时阻塞,但因为是异步,所以这里是没意义的。

事件的触发流程大概是这样:

  1. for 循环触发10个事件
  2. 当 i 等于 0 时 shouldQueue 条件成立,队列被执行
  3. 当 i 等于 1 时,前一个的队列刚刚被分发出去,还没执行完,redis获取到的值还是 0 ,条件成立
  4. 以此类推,所以 10 次循环条件都成立。

怎么来验证这一点呢? 在你的代码中添加日志打印

public function shouldQueue(RollEvent $event) 
{
    $redis = Redis::class;
    $num   = $redis::exists('capter_test') ? (int)$redis::get('capter_test') : 0;

    Log::info($num);

    return $num < 5;
}

你会发现 10 次日志的值全部是 0,一瞬间就被打印出来了,但此时你的队列还没执行完。

所以如果你要添加阻塞,应该在 shouldQueuefor 中去添加,而不是在 handle 中添加阻塞,最好是在 for 中添加,这样不会影响 shouldQueue 的逻辑。

for($i = 0; $i < 10; $i++) 
{
    sleep(10);
    event(new App\Events\Test\RollEvent(5))
}

但这样会造成请求阻塞,如果不想请求阻塞,就在 handle 中去动态判断。


结论:

shouldQueue 方法中,不应该以队列中不断变化的值去作为判断,因为队列是异步,而 shouldQueue 是在队列分发之前同步执行的,速度要快于队列,很可能会造成误判。

shouldQueue 方法是没问题的,我们在怀疑一个事情的时候应该自己先仔细求证,不要着急下结论。

1年前 评论
MArtian (作者) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
levi (楼主) 1年前
MArtian (作者) 1年前
levi (楼主) 1年前

今天写业务的时候回想起这个问题,发现其实是不存在的,准确一点来说在单个队列中是不存在的。


为什么这么说,先说Laravel异步事件加入队列的生命周期,最少包含三个阶段(可能有更多):

  1. 触发事件event
  2. 通过事件侦听器的shouldQueue来判断是否将异步事件加入队列,并将event对象序列化
  3. 按照先后顺序执行队列handle

说明:

  • 其中步骤1,这个阶段出现高并发时是不确定的,只要触发就会执行,并且将所有触发的事件按照堆栈的形式组成一个list
  • 而从步骤2到步骤3,按照先进先出的原则,从list中先pop第一组先进入的event
  • 之后从弹出的这组event中,按照事件侦听器的顺序,从上至下依次执行
  • 结束之后,继续从listpop下一组event循环执行

结论:

  • 无论高并发如何没有规律,没有顺序的去触发事件
  • 队列的前提始终是一个队列,执行的顺序会one by one,可以用redisLists数据类型来理解
  • 也就不存在第一组eventhandle执行之前,后面的event开始进入shouldQueue来判断要不要加入队列,更不会重复去执行handle
  • 也就是说,后面的event在执行shouldQueue,判断要不要加入队列时,前一组event肯定已经执行完handle

衍生问题:

  • 这个结论仅限于单个队列,或者多个不同级别的队列,比如说有2条队列,一条负责task,一条负责order,各司其职,互不干扰,这样是绝对没有问题的
  • 但是如果存在多个队列同时处理相同的异步事件时,可能就需要考虑分布式锁了,亦或者是mysql的排它锁,但是这个复杂程度就高很多了,目前我的需求还没这么高,暂且不做考虑,等以后真的需要这方面的需求的时候我再考虑
  • 上面推导出来的结论都是建立在异步事件执行成功的前提下,当业务中handle抛出异常,那么当前队列会结束,并将其重新加入队列末尾,最后执行,直至成功或失败最大程度

末尾补充:
对衍生问题最后一点,异步事件处理过程中抛出异常的解决办法

  • 在高并发下执行相同操作,当前队列抛出异常并不影响整个流程,无非是哪个操作先执行而已,所以这个情况不是问题
  • 在高并发下执行不同操作,这个时候通常要根据业务来考虑handle如何执行,具体情况具体操作,举个简单的例子,当下架商品的同时,有个买家抢先一步下了个单,那么只要在下架商品的handle中下架商品,并根据最终情况来统计最终库存即可
1年前 评论

也就是说,后面的 event 在执行 shouldQueue,判断要不要加入队列时,前一组 event 肯定已经执行完 handle 了

这个你确定吗?如果真的是按照 list 的情况触发 shouldQueue,假如有100个 handle 要执行,那这里不就发生阻塞了?因为 shouldQueue 是和请求同步执行的。

我认为 shouldQueue 方法和 handle 并不发生联系,也不会等待前一个 event 的 handle 结束再来判断,而是 event 触发时同步触发 shouldQueue,shouldQueue 也不用去理队列的执行情况,只根据当下的判断去决定是否应该分发队列。

如果你觉得你的结论是正确的,可以贴出你的伪代码,再来讨论一下,我对这个话题也很感兴趣。

1年前 评论
levi (楼主) 1年前
MArtian (作者) 1年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!