简介
如果你在你的项目中使用过 Laravel,你很可能也用过它的任务队列。任务队列为那些不需要立即执行的耗时任务(例如发送电子邮件或推送通知)提供了一个很好的方式,这将显著提高请求的响应时间。Laravel 支持多个存储平台,如 Redis、Amazon SQS,甚至是关系型数据库作为队列后端。但是,在本篇文章中,我将仅讨论 Redis 作为队列后端。另外,由于我们使用的是相对较旧的 Laravel 版本(v5.8),因此本文中的内容可能与最新版本的 Laravel 不同,但一般的实现应该不会有太大的区别。
Laravel 提供了一个非常简单的接口来与其队列系统进行交互。所有与框架一起提供的队列驱动程序的连接配置都存储在 config/queue.php
中。每个需要被转移到队列中的业务逻辑都封装在一个 Job
类中。当你运行 make:job
artisan 命令(例如 php artisan make:job SendWelcomeEmail
)时,你就创建了一个新的 job。现在你可以通过从控制器(或任何你觉得合适的地方)调用 SendWelcomeEmail::dispatch($user)
或使用 Laravel 的帮助方法 dispatch(new SendWelcomeEmail($user))
来将此 job 分派到队列中。一个独立的 worker 进程,通过运行 php artisan queue:work
来启动,将会接收并处理这个 job。在本文中,我们将讨论从你分派 job 到队列工作程序处理它的整个过程。所以,一起来跟随本文探究这个过程吧。
先决条件
由于我将讨论 Redis 作为队列后端,因此让我们先简要了解一下 Laravel 用来实现其队列系统的数据结构。
列表:Redis 中的列表是由插入顺序排序的字符串列表。你可以通过在列表的头部(左侧)或尾部(右侧)推送新元素来向列表中添加元素。LPUSH
命令将新元素添加到列表的头部,而 RPUSH
命令将其添加到列表的尾部。类似地,你可以使用 LPOP
从列表的头部删除元素,并使用 RPOP
从尾部删除元素。Laravel 将列表用作存储 job 的数据结构,新 job 通过 RPUSH
推到队列的尾部,worker 进程将从队列的头部使用 LPOP
获取要处理的 job,从而维护队列的 FIFO 结构。第一个进入队列的 job 将首先被处理。Redis 列表还有另一个特殊功能,即列表上的阻塞操作。当你在空列表上调用 LPOP
或 RPOP
时,你会得到 null。要查找列表中是否有任何新项目,你需要不断地运行其中一个命令。这会导致客户端和 Redis 服务器上的不必要的处理。因此,Redis 实现了称为 BLPOP
和 BRPOP
的命令,它们类似于 LPOP
和 RPOP
,但它们的区别在于这些命令将阻塞用户指定的时间,除非新元素被添加到列表中。例如,如果列表为空,则 BLPOP mylist 5
将阻塞 5 秒钟,但是只要列表中添加了新项,它就会返回该项。如果在 5 秒内没有添加任何项,则它将返回 null,就像它的非阻塞版本一样。
有序集合:Redis 中的集合是唯一的、不重复的字符串元素的集合。在有序集合中,集合中的每个元素还分配了一个称为 score 的浮点数,并根据与它们相关联的分数对元素进行排序。ZADD
命令将在集合中添加一个新项,ZRANGE
将按排序顺序输出集合的内容。我们还可以对分数进行操作,ZRANGEBYSCORE
将返回其分数落在命令中提供的范围内的元素,而 ZREMRANGEBYSCORE
将删除其分数落在给定范围内的所有元素。让我们看一些例子。在下面的例子中,我们将拥有一组科技公司,其成立年份为分数。
Redis 有序集合操作
有关 Redis 命令的更详细信息,请参阅其文档。当你分派延迟的 job 或重试失败的 job 时,Laravel 在 Redis 中内部存储这些 job 的有序集合。在接下来的几节中,我们将看到 Laravel 队列如何使用 Redis,以及在你分派 job 和处理 job 时发生了什么。
分派 Jobs
有两种主要的方式来分派 job,你可以调用 SendWelcomeEmail::dispatch($user)
或 dispatch(new SendWelcomeEmail($user))
。在第一种情况下,dispatch
方法提供在你的 job 类中包含的 Dispatchable
trait,而在第二种情况下,dispatch
方法是一个全局可用的帮助方法。它们都本质上做同样的事情,创建一个 Illuminate\Foundation\Bus\PendingDispatch
类的实例,将你的 job 类的实例注入其中作为参数。PendingDispatch
类并没有做太多的事情,它只是在底层的 job 类中设置队列、连接、延迟等信息。
分派 Jobs
有趣的部分发生在类的析构函数中。
在脚本的关闭序列期间,析构函数将创建Illuminate\Bus\Dispatcher
类的一个实例并调用其dispatch()
方法。在我们深入研究dispatch()
方法之前,让我们先看看Dispatcher
类的构造函数。
Dispatcher::__construct()
在此处,闭包$queueResolver
是由框架的DI容器注入的,它负责根据你在config/queue.php
文件中设置的配置获取适当的队列驱动程序。如果你感到好奇,绑定发生在Illuminate\Bus\BusServiceProvider
中,闭包在内部调用Illuminate\Queue\QueueuManager@connection()
方法。
Dispatcher::dispatch()
如果你的作业类实现了Illuminate\Contracts\Queue\ShouldQueue
接口,则Dispatcher
类中的dispatch()
方法将将作业类调度到队列后端,否则它将通过调用dispatchNow()
同步运行它。请记住这个方法,我们稍后会再回来看看它。现在让我们看看作业如何添加到队列中,这发生在dispatchToQueue()
方法中。
Dispatcher::dispatchToQueue()
在这里,你可以看到它将首先使用我们之前谈到的队列解析器解析出队列驱动程序的具体实现。如果你在作业类中指定了应将作业调度到的连接,则返回该特定连接的队列驱动程序,否则我们将获得默认连接的驱动程序。在我们的示例中,我们将接收Illuminate\Queue\RedisQueue
类的一个实例。如果你的作业有一个名为queue()
的方法,则调度程序将调用该方法,传递队列驱动程序和作业类的实例(我认为此行为未记录),否则它将调用pushCommandToQueue()
,该方法仅将作业发送到存储的队列驱动程序实例中。
Dispatcher::pushCommandToQueue()
根据是否将作业推送到特定队列和/或添加任何延迟,它会在队列驱动程序实现上调用相应的方法。我们将首先查看可立即处理的正常作业调度,然后再查看延迟作业的工作原理。
RedisQueue
类中的push()
方法仅包含一行:
RedisQueue::push()
它创建有效载荷并使用有效载荷和队列名称调用pushRaw()
。基本Queue
类中的createObjectPayload()
方法继承自RedisQueue
类,它生成了主要的有效载荷。有效载荷基本上存储诸如延迟、超时、可以重试作业的次数等信息。有效载荷数组中的job
属性设置为Illuminate\Queue\CallQueuedHandler@call
,我们将在讨论作业如何处理时找出它是什么以及它是做什么的。data
属性包含作业类本身的完整命名空间和序列化版本的作业实例。然后将所有这些数据存储在Redis中。正如我之前所说,Laravel使用Redis列表存储队列数据。列表的键是这样生成的:1) 如果你使用$job->onQueue('emails')
方法在调度期间指定了名称,则生成的队列名称将是queues:emails
。2) 否则,将使用你的队列配置中的默认队列名称。例如,queues:default
。因此,你使用的所有不同队列将在Redis服务器中创建不同的列表,例如queues:emails
、queues:process_image
、queues:default
等。然后将作业有效载荷推入其中一个队列。
RedisQueue::pushRaw()
如果操作需要对Redis服务器进行多个调用,则会使用在Redis服务器中直接评估的Lua脚本进行评估。即使你不了解Lua,你也可以清楚地了解它基本上调用RPUSH
Redis命令将作业有效载荷添加到队列末尾。
LuaScripts::push()
除将作业推入列表外,它还将数字“1”推入该队列的“notify”列表中。请记住这种行为,我们将在研究作业如何处理时找出原因。
延迟作业存储在Redis中的有序集中。
RedisQueue::laterRaw()
有序集的键是将:delayed
附加到当前队列名称的末尾生成的,例如queues:default:delayed
、queues:emails:delayed
等。如果你记得我们之前关于有序集的讨论,每个有序集中的元素都带有一个分数。在这里,分数将是作业可用于处理的时间戳。因此,如果你在作业中添加了5分钟的延迟,则分数将为$currentTimeStamp + 300;
。现在,要获取所有可用于处理的作业,你只需运行ZRANGEBYSCORE queues:emails:delayed -inf current_timestamp
,这将返回所有当前已过期且可供处理的作业。因此,这意味着,如果你在作业中添加了5分钟的延迟,则可以确保在5分钟结束之前不会处理该作业,但是你无法确保它在超过5分钟阈值后立即处理。这完全取决于当前队列中的项目数。关于作业调度就讲到这里。在下一部分中,我们将看到作业是如何被处理的。
译自:https://medium.com/codelogicx/laravel-job-queue-peeking-behind-the-curtain-part-1-fa7b5d1c0390
评论(0)