要理解队列,试想银行排队,对每条队伍来说,每次只能服务一个人,这个人到达队伍最前时才能被服务。这就是“先进先出”策略。

在程序中,队列也是类似的。应用程序向队列中添加一个 “任务”,这个任务就是一段告诉应用程序执行特定行为的代码。然后一些其他的结构,通常是“队列工作器”,需要负责一次又一次的将任务从队列中拉出并执行特定行为。队列工作器可以删除任务,携带一个延迟把任务加到队列,或者将其标记为已成功处理。

Laravel 可以轻松地使用 Redis, beanstalkd, Amazon’s Simple Queue Service (SQS), or 一个数据表 为队列服务。 你还可以同步驱动,使你的任务不使用队列正常运行,也可以对要被废弃的任务使用 null 驱动,这两种情况常被用于本地开发和测试环境。

为什么使用队列

对于移除一个笨重缓慢的同步调用,队列使之变得很容易。最常见的应用是发送 email ,发送邮件是缓慢的,你不会希望你的用户等待邮件发送成功后才接受响应,相反你希望用户点击了 “发送email” 的队列任务后,可以继续其他的工作。 你可能会说,你不需要节约用户的时间,但是你可能会遇到类似 cron job 或者 有很多工作要做的 webhook ,与其让它们一次性全部运行(可能超时),还不如选择将其各个部分放入队列中,并让队列工作器依次处理它们。

此外,如果您要处理的繁重任务已经超出了你的服务器的处理能力,则你可以增加多个队列工作器来处理队列,其速度比正常的应用程序服务器本身要快。

基本队列配置

Laravel 队列的配置位于 config/queue.php 文件,它允许你设置多个默认配置,并决定哪一个作为默认配置。这也是你配置 SQS redis beanstalkd 认证信心的地方。

Laravel Forge 和 简单的 redis 队列

Laravel Forge 是由 Laravel 的创建者Taylor Otwell 提供的托管管理服务,它使使用 Redis 作为驱动的队列变得很容易。你创建的每一个服务都自动配置了 Redis ,因此,如果你访问任何 Forge 控制台,你只需转到 Queue 标签,然后点击 “Start Worker”,你就可以使用 Redis 作为你的队列驱动;你只需保留默认设置,不需要做任何其他的操作。

队列任务

记得银行的例子吗?每个在队伍里的人,在程序中来讲,就是一个任务。根据环境,队列任务可以采用多种形状,例如,数据的数组或简单的字符串。在 Laravle 中,它们每个都是信息的集合,其中包含了任务名称,预加载的数据,到目前为止已处理该作业的尝试次数以及一些其他简单的元数据。

但是,您在与Laravel进行交互时无需担心任何这些。 Laravel提供了一种称为Job的结构,该结构旨在封装单个任务(可以命令您的应用程序执行的行为),并允许将其添加到队列或从队列中拉出。还有一些简单的帮助程序,可以轻松地将Artisan命令和邮件排队。

让我们从一个示例开始,在该示例中,每当用户使用SaaS应用程序修改他们的计划时,您希望重新运行一些有关整体利润的计算。

创建一个任务

Artisan 命令:

  1. php artisan make:job CrunchReports

默认代码如下:

<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;

class CrunchReports implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    /**
     * Create a new job instance.
     *
     * @return void
     */
    public function __construct()
    {
        //
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        //
    }
}

这个模板有两个方法:__construct() ,用于将数据绑定到任务中;handle() , 任务的逻辑代码应该放在这里,这也是也是用于注入依赖项的方法。

traits 和 interface 为类提供了添加到队列和与队列交互的能力。 Dispatchable 为它提供了自己进行分派的方法; Queueable允许您指定 Laravel 如何将此作业推送到队列; InteractsWithQueue允许每个作业在处理时控制其与队列的关系,包括删除或重新排队;和SerializesModels使工作能够对 Eloquent 模型进行序列化和反序列化。

因为序列化整个 Eloquent 对象太难了,所以当一个 job 进入队列时, SerializesModels traits 只序列了绑定到 Eloquent 对象上的主键。者意味着,当 job 执行时,需要根据主键,去查找一个新的模型实例,不是曾经在队列中的状态。

eg:

<?php

namespace App\Jobs;

use App\ReportGenerator;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;

class CrunchReports implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    protected $user;

    // 创建 队列 job 时候 期待被注入一个 user
    public function __construct($user)
    {
        $this->user = $user;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle(ReportGenerator $generator)
    {
        $generator->generateReportsForUser($this->user);

        Log::info('Generated reports.');
    }
}

以上例子中 laravel 将读取 typehints 自动注入这些依赖。

发布一个任务到队列

有很多方法可以调度任务,包括每个控制器的方法和全局 dispatch() 帮助方法。但是 laravel 5.5 之后,我们有了一个更简单的首选方法:每个任务自己调用 dispatch() 方法。

要调度任务,你只需要创建一个任务实例,然后用整个实例调用 dispatch() 方法,传入必要的参数即可。

eg:

$user = auth()->user();
$daysToCrunch = 7;
\App\Jobs\CrunchReports::dispatch($user, $dayToCrunch);

要精确的调度一个任务,你可以控制三种设置:连接,队列, 延迟。

自定义连接 如果您同时有多个队列连接,您可以通过在 dispatch() 方法后链接 onConnection() 方法来自定义连接:

DoThingJob::dispatch()->onConnection('redis');

自定义队列 队列服务器内,你可以指定你要发布的任务到哪一个队列。例如,你可以根据重要性来命名队列,分为一个 low 和一个 high 队列。
你可以使用 onQueue() 方法来指定发布任务到哪一个队列:

DoThingJob::dispatch()->onQueue('high');

自定义延迟 你可以使用 delay() 方法指定 queue worker 在处理你的工作前要等待的时间,它接收一个延迟的秒数,或者一个 DateTime/Carbon 实例:

// Delays five minutes before releasing the job to queue workers
$delay = now()->addMinutes(5);
DoThingJob::dispatch()->delay($delay);

Running a Queue Worker

Queue worker 是什么?它怎么工作的呢?在 Laravel 中,它是一个永远运行的 Artisan 命令行,它的职责是从队列中拉出来一个任务,然后运行他们:

php artisan queue:work

这个命令行是一个监听队列的守护进程;当队列中有一些任务时,它会拉取第一个任务,处理它,删除它,然后移动到下一个任务。如果没有任务了,它将 ”睡“ 一小会(可配置的时间),然后再检查是否有新的任务。

你做一些自定义:--timeout 代表队列监听者停止执行的秒数; --sleep 代表当没有任务时,队列监听者 ”睡“的秒数;--tries 代表每个任务在被删除前应该被尝试多少次;queue:work 后的第一个参数 代表应该监听那个连接;--queue 代表应该监听那个队列。如下:

php artisan queue:work redis --timeout=60 --sleep=15 --tries=3 --queue=high,medium

处理错误

异常

当一个异常被抛出,队列监听者将会释放这个任务返回到队列中。这个任务会被重新发布,一遍一遍的执行,直到执行成功,或者,触发队列监听者的最大允许尝试执行次数。

限制尝试的次数

最大允许尝试次数,使用 --tries 设置,通过 queue:listenqueue:work 命令行工具设置。

无限尝试的危险 如果你没设置 --tries 或者设置为 0,那么队列监听者就被允许无限制的尝试。这意味着,当有任何任务无法完成的情况(例如任务依赖于已经删除的文章)发生时,你的app就慢慢的停止运行,因为它在一直尝试。

有时,你想检查一个任务已经尝试了几次了,在任务上使用 attempts()即可:

public function handle(){
    // ...
    if($this->attempts()>3){

    }
}

处理失败的任务

一旦一个任务超过了它被允许的最大尝试次数,则被认为是 ”失败“ 的任务。在执行其他任何操作之前(即使你要做的只是限制任务的尝试次数),你都需要创建 ”failed jobs“ 数据表。

执行如下命令:

php artisan queue:failed-table
php artisan migrate

任何超过最大允许次数的任务都会被扔到这里。但是,你可以对失败的任务做很多事情。

首先,你可以对任务本身定义 failed() 方法,当任务失败时,将会被触发。

class CrunchReports implements ShouldQueue
{
    // ...

    public function failed(){
        // Do whatever you want, like notify an admin
    }
}

接着,你可以为失败的任务注册一个全局的处理程序。放到引导程序的任何位置——如果你不知道放在什么地方,那就放到 AppServiceProvider 中的 boot() 方法中吧。

// 某个服务提供者
use Illuminate\Support\Facades\Queue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\ServiceProvider;

class SomeServiceProvider extends ServiceProvider{

    // ...

    public function boot(){
        Queue::failing(function (JobFailed $event) {
            // $event->connectionName
            // $event->job
            // $event->exception
        });
    }

}

还有一套 Artisan 工具可以用于于失败的任务交互。

php artisan queue:failed

以上命令会列出失败的任务,如下:

+——+——————+————-+———————————+——————————-+
| ID | Connection | Queue | Class | Failed At |
+——+——————+————-+———————————+——————————-+
| 9 | database | default | App\Jobs\AlwaysFails | 2018-08-26 03:42:55 |
+——+——————+————-+———————————+——————————-+

以上你可以抓取单个 id 并重新尝试,使用如下命令:

php artisan queue:retry 9

你也可以重试所有:

php artisan queue:retry all

你可以删除某个失败任务:

php artisan queue:forget 6

当然你可以删除所有失败任务:

php artisan queue:flush

控制队列

有时你需要在处理任务的过程中,添加一些条件,这些条件可能会释放任务到队列稍后重试,或者永久删除该任务。

要释放任务到队列,使用 release() 方法即可:

public function handle(){
    if($condition){
        $this->release($numberOfSecondsToDelayBeforeRetrying);
    }
}

如果你想在处理任务期间将它删除,你只需在任何地方 return 即可。这就是向队列发出的信号,表明该任务已得到适当处理不需要返回到队列了。 如下:

public function handle(){
    if($jobShouldBeDeleted){
        return ;
    }
}

支持其他功能的队列

队列的主要用法是推一个任务到队列,但是你也可以使用邮件队列,通过使用 Mail::queue 功能。

Laravel Horizon

Laravel Horizon 是一个 laravel 提供的工具,且没有和 Laravel 核心捆绑。

Laravel Horizon 可以深入了解Redis排队作业的状态。您可以查看哪些作业失败,正在排队的作业数量以及它们的运行速度,甚至可以在任何队列超载或发生故障时获得通知。

安装很简单,感兴趣可以尝试, 这是文档