队列


1、简介

Laravel 队列服务为各种不同的后台队列提供了统一的API。队列允许你推迟耗时任务(例如发送邮件)的执行,从而大幅提高web请求速度。

1.1 配置

队列配置文件存放在config/queue.php。在该文件中你将会找到框架自带的每一个队列驱动的连接配置,包括数据库、Beanstalkd、 IronMQ、 Amazon SQS、 Redis以及同步(本地使用)驱动。其中还包含了一个null队列驱动以拒绝队列任务。

1.2 队列驱动预备知识

数据库

为了使用database队列驱动,需要一张数据库表来存放任务,要生成创建该表的迁移,运行Artisan命令queue:table,迁移被创建好了之后,使用migrate命令运行迁移:

  1. php artisan queue:table
  2. php artisan migrate
其它队列依赖

下面是以上列出队列驱动需要安装的依赖:

  • Amazon SQS: aws/aws-sdk-php ~3.0
  • Beanstalkd: pda/pheanstalk ~3.0
  • Redis: predis/predis ~1.0

2、编写任务类

2.1 生成任务类

默认情况下,应用的所有队列任务都存放在app/Jobs目录。你可以使用Artisan CLI生成新的队列任务:

  1. php artisan make:job SendReminderEmail

该命令将会在app/Jobs目录下生成一个新的类,并且该类实现了Illuminate\Contracts\Queue\ShouldQueue接口,告诉Laravel该任务应该被推送到队列而不是同步运行。

2.2 任务类结构

任务类非常简单,正常情况下只包含一个当队列处理该任务时被执行的handle方法,让我们看一个任务类的例子:

  1. <?php
  2. namespace App\Jobs;
  3. use App\User;
  4. use App\Jobs\Job;
  5. use Illuminate\Contracts\Mail\Mailer;
  6. use Illuminate\Queue\SerializesModels;
  7. use Illuminate\Queue\InteractsWithQueue;
  8. use Illuminate\Contracts\Queue\ShouldQueue;
  9. class SendReminderEmail extends Job implements ShouldQueue
  10. {
  11. use InteractsWithQueue, SerializesModels;
  12. protected $user;
  13. /**
  14. * 创建一个新的任务实例
  15. *
  16. * @param User $user
  17. * @return void
  18. */
  19. public function __construct(User $user)
  20. {
  21. $this->user = $user;
  22. }
  23. /**
  24. * 执行任务
  25. *
  26. * @param Mailer $mailer
  27. * @return void
  28. */
  29. public function handle(Mailer $mailer)
  30. {
  31. $mailer->send('emails.reminder', ['user' => $this->user], function ($m) {
  32. //
  33. });
  34. $this->user->reminders()->create(...);
  35. }
  36. }

在本例中,注意我们能够直接将Eloquent模型传递到对列任务的构造函数中。由于该任务使用了SerializesModels trait,Eloquent模型将会在任务被执行是优雅地序列化和反序列化。如果你的队列任务在构造函数中接收Eloquent模型,只有模型的主键会被序列化到队列,当任务真正被执行的时候,队列系统会自动从数据库中获取整个模型实例。这对应用而言是完全透明的,从而避免序列化整个Eloquent模型实例引起的问题。

handle方法在任务被队列处理的时候被调用,注意我们可以在任务的handle方法中进行依赖注入。Laravel服务容器会自动注入这些依赖。

出错

如果任务被处理的时候抛出异常,则该任务将会被自动释放回队列以便再次尝试执行。任务会持续被释放知道尝试次数达到应用允许的最大次数。最大尝试次数通过Artisan任务queue:listenqueue:work上的--tries开关来定义。关于运行队列监听器的更多信息可以在下面看到。

手动释放任务

如果你想要手动释放任务,生成的任务类中自带的InteractsWithQueue trait提供了释放队列任务的release方法,该方法接收一个参数——同一个任务两次运行之间的等待时间:

  1. public function handle(Mailer $mailer){
  2. if (condition) {
  3. $this->release(10);
  4. }
  5. }
检查尝试运行次数

正如上面提到的,如果在任务处理期间发生异常,任务会自动释放回队列中,你可以通过attempts方法来检查该任务已经尝试运行次数:

  1. public function handle(Mailer $mailer){
  2. if ($this->attempts() > 3) {
  3. //
  4. }
  5. }

3、推送任务到队列

默认的 Laravel 控制器位于app/Http/Controllers/Controller.php并使用了DispatchesJobs trait。该trait提供了一些允许你方便推送任务到队列的方法,例如dispatch方法:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\User;
  4. use Illuminate\Http\Request;
  5. use App\Jobs\SendReminderEmail;
  6. use App\Http\Controllers\Controller;
  7. class UserController extends Controller{
  8. /**
  9. * 发送提醒邮件到指定用户
  10. *
  11. * @param Request $request
  12. * @param int $id
  13. * @return Response
  14. */
  15. public function sendReminderEmail(Request $request, $id)
  16. {
  17. $user = User::findOrFail($id);
  18. $this->dispatch(new SendReminderEmail($user));
  19. }
  20. }
DispatchesJobs Trait

当然,有时候你想要从应用中路由或控制器之外的某些地方分发任务,因为这个原因,你可以在应用的任何类中包含DispatchesJobs trait,从而获取对分发方法的访问,举个例子,下面是使用该trait的示例类:

  1. <?php
  2. namespace App;
  3. use Illuminate\Foundation\Bus\DispatchesJobs;
  4. class ExampleClass{
  5. use DispatchesJobs;
  6. }
dispatch方法

或者,你也可以使用全局的dispatch方法:

  1. Route::get('/job', function () {
  2. dispatch(new App\Jobs\PerformTask);
  3. return 'Done!';
  4. });
为任务指定队列

你还可以指定任务被发送到的队列。
根据任务被推送到的不同队列,你可以对队列任务进行“分类”,甚至优先考虑分配给多个队列的worker数目。这并不会如队列配置文件中定义的那样将任务推送到不同队列“连接”,而只是在单个连接中发送给特定队列。要指定该队列,使用任务实例上的 onQueue 方法,该方法由 Laravel 自带的基类 App\Jobs\Job 中的 Illuminate\Bus\Queueable trait提供:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\User;
  4. use Illuminate\Http\Request;
  5. use App\Jobs\SendReminderEmail;
  6. use App\Http\Controllers\Controller;
  7. class UserController extends Controller{
  8. /**
  9. * 发送提醒邮件到指定用户
  10. *
  11. * @param Request $request
  12. * @param int $id
  13. * @return Response
  14. */
  15. public function sendReminderEmail(Request $request, $id)
  16. {
  17. $user = User::findOrFail($id);
  18. $job = (new SendReminderEmail($user))->onQueue('emails');
  19. $this->dispatch($job);
  20. }
  21. }

3.1 延迟任务

有时候你可能想要延迟队列任务的执行。例如,你可能想要将一个注册15分钟后给消费者发送提醒邮件的任务放到队列中,可以通过使用任务类上的delay方法来实现,该方法由Illuminate\Bus\Queueable trait提供:

  1. <?php
  2. namespace App\Http\Controllers;
  3. use App\User;
  4. use Illuminate\Http\Request;
  5. use App\Jobs\SendReminderEmail;
  6. use App\Http\Controllers\Controller;
  7. class UserController extends Controller{
  8. /**
  9. * 发送提醒邮件到指定用户
  10. *
  11. * @param Request $request
  12. * @param int $id
  13. * @return Response
  14. */
  15. public function sendReminderEmail(Request $request, $id)
  16. {
  17. $user = User::findOrFail($id);
  18. $job = (new SendReminderEmail($user))->delay(60);
  19. $this->dispatch($job);
  20. }
  21. }

在本例中,我们指定任务在队列中开始执行前延迟60秒。

注意:Amazon SQS服务最大延迟时间是15分钟。

3.2 任务事件

任务完成事件

Queue::after 方法允许你在队列任务执行成功后注册一个要执行的回调函数。在该回调中我们可以添加日志、统计数据。例如,我们可以在Laravel内置的 AppServiceProvider 中添加事件回调:

  1. <?php
  2.  
  3. namespace App\Providers;
  4.  
  5. use Queue;
  6. use Illuminate\Support\ServiceProvider;
  7.  
  8. class AppServiceProvider extends ServiceProvider
  9. {
  10. /**
  11. * Bootstrap any application services.
  12. *
  13. * @return void
  14. */
  15. public function boot()
  16. {
  17. Queue::after(function ($connection, $job, $data) {
  18. //
  19. });
  20. }
  21.  
  22. /**
  23. * Register the service provider.
  24. *
  25. * @return void
  26. */
  27. public function register()
  28. {
  29. //
  30. }
  31. }

4、运行队列监听器

启动任务监听器

Laravel 包含了一个 Artisan 命令用来运行被推送到队列的新任务。你可以使用 queue:listen 命令运行监听器:

  1. php artisan queue:listen

还可以指定监听器使用哪个队列连接:

  1. php artisan queue:listen connection

注意一旦任务开始后,将会持续运行直到手动停止。你可以使用一个过程监视器如Supervisor来确保队列监听器没有停止运行。

队列优先级

你可以传递逗号分隔的队列连接列表到listen任务来设置队列优先级:

  1. php artisan queue:listen --queue=high,low

在本例中,high队列上的任务总是在从low队列移动任务之前被处理。

指定任务超时参数

你还可以设置每个任务允许运行的最大时间(以秒为单位):

  1. php artisan queue:listen --timeout=60
指定队列睡眠时间

此外,可以指定轮询新任务之前的等待时间(以秒为单位):

  1. php artisan queue:listen --sleep=5

需要注意的是队列只会在队列上没有任务时“睡眠”,如果存在多个有效任务,该队列会持续运行,从不睡眠。

4.1 Supervisor配置

Supervisor为Linux操作系统提供的进程监视器,将会在失败时自动重启queue:listenqueue:work命令,要在Ubuntu上安装Supervisor,使用如下命令:

  1. sudo apt-get install supervisor

Supervisor配置文件通常存放在/etc/supervisor/conf.d目录,在该目录中,可以创建多个配置文件指示Supervisor如何监视进程,例如,让我们创建一个开启并监视queue:work进程的laravel-worker.conf文件:

  1. [program:laravel-worker]
  2. process_name=%(program_name)s_%(process_num)02d
  3. command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --daemon
  4. autostart=true
  5. autorestart=true
  6. user=forge
  7. numprocs=8
  8. redirect_stderr=true
  9. stdout_logfile=/home/forge/app.com/worker.log

在本例中,numprocs指令让Supervisor运行8个queue:work进程并监视它们,如果失败的话自动重启。配置文件创建好了之后,可以使用如下命令更新Supervisor配置并开启进程:

  1. sudo supervisord -c /etc/supervisord.conf
  2. sudo supervisorctl -c /etc/supervisor/supervisord.conf
  3. sudo supervisorctl reread
  4. sudo supervisorctl update
  5. sudo supervisorctl start laravel-worker:*

要了解更多关于Supervisor的使用和配置,查看Supervisor文档。此外,还可以使用Laravel Forge从web接口方便地自动配置和管理Supervisor配置。

4.2 后台队列监听器

Artisan命令queue:work包含一个--daemon选项来强制队列worker持续处理任务而不必重新启动框架。相较于queue:listen命令该命令对CPU的使用有明显降低:

  1. php artisan queue:work connection --daemon
  2. php artisan queue:work connection --daemon --sleep=3
  3. php artisan queue:work connection --daemon --sleep=3 --tries=3

正如你所看到的,queue:work任务支持大多数queue:listen中有效的选项。你可以使用php artisan help queue:work任务来查看所有有效选项。

后台队列监听器编码考虑

后台队列worker在处理每个任务时不重启框架,因此,你要在任务完成之前释放资源,举个例子,如果你在使用GD库操作图片,那么就在完成时使用imagedestroy释放内存。

类似的,数据库连接应该在后台长时间运行完成后断开,你可以使用DB::reconnect方法确保获取了一个新的连接。

4.3 部署后台队列监听器

由于后台队列worker是常驻进程,不重启的话不会应用代码中的更改,所以,最简单的部署后台队列worker的方式是使用部署脚本重启所有worker,你可以通过在部署脚本中包含如下命令重启所有worker:

  1. php artisan queue:restart

该命令会告诉所有队列worker在完成当前任务处理后重启以便没有任务被遗漏。

注意:这个命令依赖于缓存系统重启进度表,默认情况下,APC在CLI任务中无法正常工作,如果你在使用APC,需要在APC配置中添加apc.enable_cli=1

5、处理失败任务

由于事情并不总是按照计划发展,有时候你的队列任务会失败。别担心,它发生在我们大多数人身上!Laravel包含了一个方便的方式来指定任务最大尝试执行次数,任务执行次数达到最大限制后,会被插入到failed_jobs表,失败任务的名字可以通过配置文件config/queue.php来配置。

要创建一个failed_jobs表的迁移,可以使用queue:failed-table命令:

  1. php artisan queue:failed-table

运行队列监听器的时候,可以在queue:listen命令上使用--tries开关来指定任务最大可尝试执行次数:

  1. php artisan queue:listen connection-name --tries=3

5.1 失败任务事件

如果你想要注册一个队列任务失败时被调用的事件,可以使用Queue::failing方法,该事件通过邮件或HipChat通知团队。举个例子,我么可以在Laravel自带的AppServiceProvider中附件一个回调到该事件:

  1. <?php
  2. namespace App\Providers;
  3. use Queue;
  4. use Illuminate\Support\ServiceProvider;
  5. class AppServiceProvider extends ServiceProvider{
  6. /**
  7. * 启动应用服务
  8. *
  9. * @return void
  10. */
  11. public function boot()
  12. {
  13. Queue::failing(function ($connection, $job, $data) {
  14. // Notify team of failing job...
  15. });
  16. }
  17. /**
  18. * 注册服务提供者
  19. *
  20. * @return void
  21. */
  22. public function register()
  23. {
  24. //
  25. }
  26. }
任务类的失败方法

想要更加细粒度的控制,可以在队列任务类上直接定义failed方法,从而允许你在失败发生时执行指定动作:

  1. <?php
  2. namespace App\Jobs;
  3. use App\Jobs\Job;
  4. use Illuminate\Contracts\Mail\Mailer;
  5. use Illuminate\Queue\SerializesModels;
  6. use Illuminate\Queue\InteractsWithQueue;
  7. use Illuminate\Contracts\Bus\SelfHandling;
  8. use Illuminate\Contracts\Queue\ShouldQueue;
  9. class SendReminderEmail extends Job implements SelfHandling, ShouldQueue
  10. {
  11. use InteractsWithQueue, SerializesModels;
  12. /**
  13. * 执行任务
  14. *
  15. * @param Mailer $mailer
  16. * @return void
  17. */
  18. public function handle(Mailer $mailer)
  19. {
  20. //
  21. }
  22. /**
  23. * 处理失败任务
  24. *
  25. * @return void
  26. */
  27. public function failed()
  28. {
  29. // Called when the job is failing...
  30. }
  31. }

5.2 重试失败任务

要查看已插入到failed_jobs数据表中的所有失败任务,可以使用Artisan命令queue:failed

  1. php artisan queue:failed

该命令将会列出任务ID,连接,对列和失败时间,任务ID可用于重试失败任务,例如,要重试一个ID为5的失败任务,要用到下面的命令:

  1. php artisan queue:retry 5

要重试所有失败任务,使用如下命令即可:

  1. php artisan queue:retry all

如果你要删除一个失败任务,可以使用queue:forget命令:

  1. php artisan queue:forget 5

要删除所有失败任务,可以使用queue:flush命令:

  1. php artisan queue:flush