Laravel 隊列服務為各種不同的后臺隊列提供了統(tǒng)一的 API。隊列允許你推遲耗時任務(例如發(fā)送郵件)的執(zhí)行,從而大幅提高 web 請求速度。
隊列配置文件存放在config/queue.php
。在該文件中你將會找到框架自帶的每一個隊列驅動的連接配置,包括數據庫、Beanstalkd、 IronMQ、 Amazon SQS、 Redis 以及同步(本地使用)驅動。其中還包含了一個 null 隊列驅動以拒絕隊列任務。
為了使用 database
隊列驅動,需要一張數據庫表來存放任務,要生成創(chuàng)建該表的遷移,運行 Artisan 命令 queue:table
,遷移被創(chuàng)建好了之后,使用 migrate
命令運行遷移:
php artisan queue:table
php artisan migrate
下面是以上列出隊列驅動需要安裝的依賴:
aws/aws-sdk-php ~3.0
pda/pheanstalk ~3.0
iron-io/iron_mq ~2.0
predis/predis ~1.0
默認情況下,應用的所有隊列任務都存放在 app/Jobs
目錄。你可以使用 Artisan CLI 生成新的隊列任務:
php artisan make:job SendReminderEmail --queued
該命令將會在 app/Jobs
目錄下生成一個新的類,并且該類實現了 Illuminate\Contracts\Queue\ShouldQueue
接口,告訴 Laravel 該任務應該被推送到隊列而不是同步運行。
任務類非常簡單,正常情況下只包含一個當隊列處理該任務時被執(zhí)行的 handle
方法,讓我們看一個任務類的例子:
<?php
namespace App\Jobs;
use App\User;
use App\Jobs\Job;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendReminderEmail extends Job implements SelfHandling, ShouldQueue
{
use InteractsWithQueue, SerializesModels;
protected $user;
/**
* 創(chuàng)建一個新的任務實例
*
* @param User $user
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* 執(zhí)行任務
*
* @param Mailer $mailer
* @return void
*/
public function handle(Mailer $mailer)
{
$mailer->send('emails.reminder', ['user' => $this->user], function ($m) {
//
});
$this->user->reminders()->create(...);
}
}
在本例中,注意我們能夠直接將 Eloquent 模型傳遞到對列任務的構造函數中。由于該任務使用了 SerializesModels
trait,Eloquent 模型將會在任務被執(zhí)行是優(yōu)雅地序列化和反序列化。如果你的隊列任務在構造函數中接收 Eloquent 模型,只有模型的主鍵會被序列化到隊列,當任務真正被執(zhí)行的時候,隊列系統(tǒng)會自動從數據庫中獲取整個模型實例。這對應用而言是完全透明的,從而避免序列化整個 Eloquent 模型實例引起的問題。
handle
方法在任務被隊列處理的時候被調用,注意我們可以在任務的 handle
方法中對依賴進行類型提示。Laravel 服務容器會自動注入這些依賴。
如果任務被處理的時候拋出異常,則該任務將會被自動釋放回隊列以便再次嘗試執(zhí)行。任務會持續(xù)被釋放知道嘗試次數達到應用允許的最大次數。最大嘗試次數通過 Artisan 任務 queue:listen
或 queue:work
上的--tries
開關來定義。關于運行隊列監(jiān)聽器的更多信息可以在下面看到。
如果你想要手動釋放任務,生成的任務類中自帶的 InteractsWithQueue
trait 提供了釋放隊列任務的 release
方法,該方法接收一個參數——同一個任務兩次運行之間的等待時間:
public function handle(Mailer $mailer){
if (condition) {
$this->release(10);
}
}
正如上面提到的,如果在任務處理期間發(fā)生異常,任務會自動釋放回隊列中,你可以通過 attempts
方法來檢查該任務已經嘗試運行次數:
public function handle(Mailer $mailer){
if ($this->attempts() > 3) {
//
}
}
默認的 Laravel 控制器位于 app/Http/Controllers/Controller.php
并使用了 DispatchesJobs
trait。該 trait 提供了一些允許你方便推送任務到隊列的方法,例如 dispatch
方法:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use App\Jobs\SendReminderEmail;
use App\Http\Controllers\Controller;
class UserController extends Controller{
/**
* 發(fā)送提醒郵件到指定用戶
*
* @param Request $request
* @param int $id
* @return Response
*/
public function sendReminderEmail(Request $request, $id)
{
$user = User::findOrFail($id);
$this->dispatch(new SendReminderEmail($user));
}
}
當然,有時候你想要從應用中路由或控制器之外的某些地方分發(fā)任務,因為這個原因,你可以在應用的任何類中包含 DispatchesJobs
trait,從而獲取對分發(fā)方法的訪問,舉個例子,下面是使用該 trait 的示例類:
<?php
namespace App;
use Illuminate\Foundation\Bus\DispatchesJobs;
class ExampleClass{
use DispatchesJobs;
}
為任務指定隊列
你還可以指定任務被發(fā)送到的隊列。
通過推送任務到不同隊列,你可以對隊列任務進行“分類”,甚至優(yōu)先考慮分配給多個隊列的 worker 數目。這并不會如隊列配置文件中定義的那樣將任務推送到不同隊列“連接”,而只是在單個連接中發(fā)送給特定隊列。要指定該隊列,使用任務實例上的 onQueue
方法,該方法有 Laravel 自帶的基類 App\Jobs\Job
提供:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use App\Jobs\SendReminderEmail;
use App\Http\Controllers\Controller;
class UserController extends Controller{
/**
* 發(fā)送提醒郵件到指定用戶
*
* @param Request $request
* @param int $id
* @return Response
*/
public function sendReminderEmail(Request $request, $id)
{
$user = User::findOrFail($id);
$job = (new SendReminderEmail($user))->onQueue('emails');
$this->dispatch($job);
}
}
有時候你可能想要延遲隊列任務的執(zhí)行。例如,你可能想要將一個注冊 15 分鐘后給消費者發(fā)送提醒郵件的任務放到隊列中,可以通過使用任務類上的 delay
方法來實現,該方法由 Illuminate\Bus\Queueable
trait 提供:
<?php
namespace App\Http\Controllers;
use App\User;
use Illuminate\Http\Request;
use App\Jobs\SendReminderEmail;
use App\Http\Controllers\Controller;
class UserController extends Controller{
/**
* 發(fā)送提醒郵件到指定用戶
*
* @param Request $request
* @param int $id
* @return Response
*/
public function sendReminderEmail(Request $request, $id)
{
$user = User::findOrFail($id);
$job = (new SendReminderEmail($user))->delay(60);
$this->dispatch($job);
}
}
在本例中,我們指定任務在隊列中開始執(zhí)行前延遲 60 秒。 注意:Amazon SQS 服務最大延遲時間是 15 分鐘。
映射HTTP請求變量到任務中很常見,Laravel 提供了一些幫助函數讓這種實現變得簡單,而不用每次請求時手動執(zhí)行映射。讓我么看一下 DispatchesJobs
trait 上的 dispatchFrom
方法。默認情況下,該 trait 包含在 Laravel 控制器基類中:
<?php
namespace App\Http\Controllers;
use Illuminate\Http\Request;
use App\Http\Controllers\Controller;
class CommerceController extends Controller{
/**
* 處理指定訂單
*
* @param Request $request
* @param int $id
* @return Response
*/
public function processOrder(Request $request, $id)
{
// 處理請求...
$this->dispatchFrom('App\Jobs\ProcessOrder', $request);
}
}
該方法檢查給定任務類的構造函數并從 HTTP 請求(或者其它 ArrayAccess
對象)中解析變量來填充任務需要的構造函數參數。所以,如果我們的任務類在構造函數中接收一個productId
變量,該任務將會嘗試從 HTTP 請求中獲取 productId
參數。
你還可以傳遞一個數組作為 dispatchFrom
方法的第三個參數。該數組用于填充所有請求中不存在的構造函數參數:
$this->dispatchFrom('App\Jobs\ProcessOrder', $request, [
'taxPercentage' => 20,
]);
開啟任務監(jiān)聽器
Laravel 包含了一個 Artisan 命令用來運行推送到隊列的新任務。你可以使用 queue:listen
命令運行監(jiān)聽器:
php artisan queue:listen
還可以指定監(jiān)聽器使用哪個隊列連接:
php artisan queue:listen connection
注意一旦任務開始后,將會持續(xù)運行直到手動停止。你可以使用一個過程監(jiān)視器如 Supervisor 來確保隊列監(jiān)聽器沒有停止運行。
隊列優(yōu)先級
你可以傳遞逗號分隔的隊列連接列表到 listen
任務來設置隊列優(yōu)先級:
php artisan queue:listen --queue=high,low
在本例中,high
隊列上的任務總是在從low
隊列移動任務之前被處理。
指定任務超時參數
你還可以設置每個任務允許運行的最大時間(以秒為單位):
php artisan queue:listen --timeout=60
指定隊列睡眠時間 此外,可以指定輪詢新任務之前的等待時間(以秒為單位):
php artisan queue:listen --sleep=5
需要注意的是隊列只會在隊列上沒有任務時“睡眠”,如果存在多個有效任務,該隊列會持續(xù)運行,從不睡眠。
Supervisor 為 Linux 操作系統(tǒng)提供的進程監(jiān)視器,將會在失敗時自動重啟 queue:listen
或 queue:work
命令,要在 Ubuntu 上安裝 Supervisor,使用如下命令:
sudo apt-get install supervisor
Supervisor 配置文件通常存放在/etc/supervisor/conf.d
目錄,在該目錄中,可以創(chuàng)建多個配置文件指示 Supervisor 如何監(jiān)視進程,例如,讓我們創(chuàng)建一個開啟并監(jiān)視 queue:work
進程的 laravel-worker.conf
文件:
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /home/forge/app.com/artisan queue:work sqs --sleep=3 --tries=3 --daemon
autostart=true
autorestart=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
在本例中,numprocs
指令讓 Supervisor 運行 8 個 queue:work
進程并監(jiān)視它們,如果失敗的話自動重啟。配置文件創(chuàng)建好了之后,可以使用如下命令更新 Supervisor 配置并開啟進程:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
要了解更多關于 Supervisor 的使用和配置,查看 Supervisor 文檔。此外,還可以使用 Laravel Forge 從 web 接口方便地自動配置和管理 Supervisor 配置。
Artisan 命令queue:work
包含一個--daemon
選項來強制隊列 worker 持續(xù)處理任務而不必重新啟動框架。相較于 queue:listen
命令該命令對 CPU 的使用有明顯降低:
php artisan queue:work connection --daemon
php artisan queue:work connection --daemon --sleep=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
方法確保獲取了一個新的連接。
由于后臺隊列 worker 是常駐進程,不重啟的話不會應用代碼中的更改,所以,最簡單的部署后臺隊列 worker 的方式是使用部署腳本重啟所有 worker,你可以通過在部署腳本中包含如下命令重啟所有 worker:
php artisan queue:restart
該命令會告訴所有隊列 worker 在完成當前任務處理后重啟以便沒有任務被遺漏。
注意:這個命令依賴于緩存系統(tǒng)重啟進度表,默認情況下,APC 在 CLI 任務中無法正常工作,如果你在使用 APC,需要在 APC 配置中添加 apc.enable_cli=1
。
由于事情并不總是按照計劃發(fā)展,有時候你的隊列任務會失敗。別擔心,它發(fā)生在我們大多數人身上!Laravel 包含了一個方便的方式來指定任務最大嘗試執(zhí)行次數,任務執(zhí)行次數達到最大限制后,會被插入到 failed_jobs
表,失敗任務的名字可以通過配置文件 config/queue.php
來配置。
要創(chuàng)建一個 failed_jobs 表的遷移,可以使用 queue:failed-table 命令:
php artisan queue:failed-table
運行隊列監(jiān)聽器的時候,可以在 queue:listen
命令上使用--tries
開關來指定任務最大可嘗試執(zhí)行次數:
php artisan queue:listen connection-name --tries=3
如果你想要注冊一個隊列任務失敗時被調用的事件,可以使用 Queue::failing
方法,該事件通過郵件或 HipChat 通知團隊。舉個例子,我么可以在 Laravel 自帶的 AppServiceProvider
中附件一個回調到該事件:
<?php
namespace App\Providers;
use Queue;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider{
/**
* 啟動應用服務
*
* @return void
*/
public function boot()
{
Queue::failing(function ($connection, $job, $data) {
// Notify team of failing job...
});
}
/**
* 注冊服務提供者
*
* @return void
*/
public function register()
{
//
}
}
想要更加細粒度的控制,可以在隊列任務類上直接定義 failed
方法,從而允許你在失敗發(fā)生時執(zhí)行指定動作:
<?php
namespace App\Jobs;
use App\Jobs\Job;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Bus\SelfHandling;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendReminderEmail extends Job implements SelfHandling, ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/**
* 執(zhí)行任務
*
* @param Mailer $mailer
* @return void
*/
public function handle(Mailer $mailer)
{
//
}
/**
* 處理失敗任務
*
* @return void
*/
public function failed()
{
// Called when the job is failing...
}
}
要查看已插入到 failed_jobs
數據表中的所有失敗任務,可以使用 Artisan 命令 queue:failed
:
php artisan queue:failed
該命令將會列出任務 ID,連接,對列和失敗時間,任務 ID 可用于重試失敗任務,例如,要重試一個 ID 為 5 的失敗任務,要用到下面的命令:
php artisan queue:retry 5
如果你要刪除一個失敗任務,可以使用 queue:forget
命令:
php artisan queue:forget 5
要刪除所有失敗任務,可以使用 queue:flush
命令:
php artisan queue:flush