隊列
簡介
Laravel 的隊列服務為不同的隊列後端系統提供一個統一的 API 。隊列允許你將一個耗時的任務延遲處理,例如像寄送 e-mail,這會使得你的應用程式對網頁請求有更快的反應。
設定
隊列的設定檔被儲存在 config/queue.php
。在這個檔案裡你可以找到包含在 Laravel 框架中,每一種隊列驅動的連結設定。它們包含了資料庫、Beanstalkd、IronMQ、Amazon SQS、Redis 以及提供本機使用的 synchronous 驅動。
另外框架也提供了 null
這個隊列驅動,用來丟棄隊列任務。
驅動必要設定
資料庫
要使用 database
這個隊列驅動的話,需要建立一個資料表來記住任務,你可以用 queue:table
這個 Artisan 指令來建立這個資料表的遷移類別。當遷移類別建好後,就可以用 migrate
這個指令來將資料表在資料庫中建立起來。
php artisan queue:table
php artisan migrate
其他隊列系統的相依套件
要使用列表裡的隊列服務前,必須安裝以下的相依套件:
- Amazon SQS:
aws/aws-sdk-php ~3.0
- Beanstalkd:
pda/pheanstalk ~3.0
- Redis:
predis/predis ~1.0
撰寫任務類別
產生任務類別
在你的應用程式中,所有能放在隊列的任務類別預設放在 app/Jobs
目錄下,你可以用以下的 Artisan 指令來產生一個新的隊列任務:
php artisan make:job SendReminderEmail
這個指令將會在 app/Jobs
下產生一個新類別,而這個類別會實作 Illuminate\Contracts\Queue\ShouldQueue
介面,讓 Laravel 知道這個任務應該是被放到隊列裡,而不是直接執行。
任務類別結構
任務類別的結構很簡單,一般來說只會包含一個讓隊列用來呼叫此任務的 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\Queue\ShouldQueue;
class SendReminderEmail extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
protected $user;
/**
* 建立一個新的任務實例。
*
* @param User $user
* @return void
*/
public function __construct(User $user)
{
$this->user = $user;
}
/**
* 執行任務。
*
* @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 模型在處理任務的時候可以被優雅地序列化和反序列化。如果你的隊列任務類別在建構子接受一個 Eloquent 模型,那麼只有可識別出該模型的屬性會被序列化至隊列裡。當任務實際被執行時,隊列系統會自動從資料庫中重新取回完整的模型。整個過程對你的應用程式來說是透明的,這樣可以避免序列化完整的 Eloquent 的模式實例所帶來的問題。
在隊列處理任務時,會呼叫 handle
方法,而這裡我們也可以透過 handle
方法的參數型別提示,讓 Laravel 的服務容器自動注入相依物件。
當發生錯誤的時候
如果在任務處理時拋出了一個例外,它會自動被釋放回隊列裡再次嘗試執行。當該任務一直出錯時,它會不斷被釋出再重試,直到超過你的應用程式所允許的最大重試值。最大重試值可以在執行 queue:listen
或 queue:work
指令時,用 --tries
選項來設定;執行隊列監聽器的更多資訊在稍後會有詳細說明。
手動釋放任務
如果你想手動釋放任務,那麼在產生出來的任務類別已經引用了 InteractsWithQueue
這個 trait,它提供了 release
方法讓我們可以釋放任務。在 release
方法中接受一個數值參數,它表示直到這個任務可以被重新執行之前,你願意等待的秒數。
public function handle(Mailer $mailer)
{
if (condition) {
$this->release(10);
}
}
檢查重試次數
如同前面提到的,當任務被處理時發生了一個例外,它會自動被釋放回隊列中。這時候你可以用 attempt
方法來檢查已經重試的次數:
public function handle(Mailer $mailer)
{
if ($this->attempts() > 3) {
//
}
}
將任務推送到隊列上
在 app/Http/Controllers/Controller.php
中 Laravel 定義了一個預設控制器,它引用了 DispatchesJob
這個 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
{
/**
* 寄送提醒的 e-mail 給指定使用者。
*
* @param Request $request
* @param int $id
* @return Response
*/
public function sendReminderEmail(Request $request, $id)
{
$user = User::findOrFail($id);
$this->dispatch(new SendReminderEmail($user));
}
}
DispatchesJobs
Trait
當然有時你不見得是從應用程式的路由或控制器來派發任務,因此你可以在應用程式中的任何類別裡引用 DispatchesJobs
這個 trait ,以便使用它的各種派發方法。以下就是使用該 trait 的類別範例:
<?php
namespace App;
use Illuminate\Foundation\Bus\DispatchesJobs;
class ExampleClass
{
use DispatchesJobs;
}
dispatch
函式
或者你可以使用 dspatch
全域函式:
Route::get('/job', function () {
dispatch(new App\Jobs\PerformTask);
return 'Done!';
});
指定任務所屬的隊列
你可以指定任務應該要送到哪一個隊列。
要推送任務到不同的隊列上,你要將任務先「分類」,甚至可能要排定每個隊列能有多少作業器可以執行任務。這並不是指任務會推送到你在設定檔所定義的不同隊列連結裡,而是推送到某個有單一連結的隊列。要指定任務執行的隊列,可以用任務實例的 onQueue
方法。onQueue
是 Illuminate\Bus\Queueable
trait 所提供的方法,而它已經包含在 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
{
/**
* 寄送提醒的 e-mail 給指定使用者。
*
* @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);
}
}
延遲性任務
有時你可能會希望隊列任務能晚一點再執行,例如在使用者註冊後 5 分鐘後才透過隊列任務寄送提醒信件。你可以透過任務類別引用的 Illuminate\Bus\Queueable
這個 trait 所提供的 delay
方法來達成這個目的:
<?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
{
/**
* 寄送提醒的 e-mail 給指定使用者。
*
* @param Request $request
* @param int $id
* @return Response
*/
public function sendReminderEmail(Request $request, $id)
{
$user = User::findOrFail($id);
$job = (new SendReminderEmail($user))->delay(60 * 5);
$this->dispatch($job);
}
}
在這個範例裡,我們指定該任務要在交給作業器執行前先延遲 5 分鐘。
注意:Amazon 的 SQS 服務最大延遲時間是 15 分鐘。
任務事件
任務完成事件
Queue::after
方法讓你能夠註冊一個回呼,當隊列任務執行完成後就會被執行。在此回呼進行額外的紀錄、隊列後續任務、或為儀表板增加統計都是很好的時機。舉個例子,我們可以在 Laravel 所包含的 AppServiceProvider
附加一個回呼到此事件:
<?php
namespace App\Providers;
use Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
class AppServiceProvider extends ServiceProvider
{
/**
* 啟動所有應用程式服務。
*
* @return void
*/
public function boot()
{
Queue::after(function (JobProcessed $event) {
// $event->connection
// $event->$job
// $event->$data
});
}
/**
* 註冊服務提供者。
*
* @return void
*/
public function register()
{
//
}
}
執行隊列監聽器
啟動隊列監聽器
Laravel 引入了一個 Artisan 指令,用來執行被推送到隊列裡的任務。你可以透過 queue:listen
指令來執行監聽器:
php artisan queue:listen
你也可以指定監聽器應該利用哪一個隊列連結:
php artisan queue:listen connection
要注意的是,一旦這個工作指令啟動後,它會持續運作直到它被手動停止。你可以利用像 Supervisor 這樣的行程監控軟體,來確保隊列監聽器不會停止執行。
隊列優先序
你可以給 listen
指令一個以逗號分隔的隊列連結列表,來設定隊列的優先序:
php artisan queue:listen --queue=high,low
在這個範例裡,在 high
這個隊列裡的任務永遠會先被處理,然後才是 low
隊列裡的任務。
指定任務的逾時參數
你還可以設定每個任務所被允許執行的時間長度,單位是秒數:
php artisan queue:listen --timeout=60
指定隊列的休眠期
此外,你可以指定隊列要等幾秒再拿取新的任務來執行:
php artisan queue:listen --sleep=5
注意這裡是指隊列在沒有任務的狀態下才會休眠;如果已經有多個任務卡在這個隊列上,那麼它會持續運作而不會休眠。
Supervisor 設定
Supervisor 是一個在 Linux 作業系統上的行程監控軟體,它會在 queue:listen
或 queue:work
指令發生失敗後自動重啟它們。要在 Ubuntu 安裝 Supervisor,可以用以下指令:
sudo apt-get install supervisor
Supervisor 的設定檔一般是放在 /etc/supervisor/conf.d
目錄下,在這個目錄中你可以建立任意數量的設定檔,要求 Supervisor 來監控你的行程。例如我們建立一個 laravel-worker.conf
來啟動與監控一個 queue:work
行程:
[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
行程,並且在它們執行失敗時重新啟動。當然,你必須更改 command
指令的 queue:work sqs
部分,以顯示你所選擇的隊列驅動
當這個設定檔建立後,你需要更新 Supervisor 的設定,並用以下指令來啟動該行程:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*
更多有關 Supervisor 的設定與使用,請參考 Supervisor 官方文件。或是你可以使用 Laravel Forge 所提供的 Web 介面,來自動設定與管理你的 Supervisor 設定。
將任務監聽器設為背景服務
在 queue:work
Artisan 指令裡包含了 --daemon
選項,強迫隊列作業器持續處理任務,而不需要重新啟動整個框架。比起 queue:listen
指令,這會顯著減少 CPU 的用量。
用 --daemon
旗標在背景服務模式下啟動隊列作業器:
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
指令來查看所有可用的選項。
在背景服務的隊列監聽器中開發所要考量的事項
在背景執行的隊列監聽器在處理完每個任務前,不會重新啟動框架;因此你應該在任務執行完成前,謹慎地釋放任何佔用記憶體較重的資源。例如你利用 GD 函式庫處理影像,就要在結束前用 imagedestroy
來釋放記憶體。
相同地,你的資料庫連結也要在使用完後關閉連線;你可以用 DB::reconnect
方法來確保有新的資料庫連線。
隨著在背景服務的任務監聽器進行佈署
從背景服務的隊列作業器是長時間執行的行程來看,除非重新啟動,否則它們將不會理會任何程式碼上的變更。所以要佈署一個有用到背景服務的隊列作業器的應用程式,最簡單的方法就是在佈署指令稿中重新啟動作業器。你可以在你的佈署指令裡加上以下指令,來優雅地重新啟動所有作業器:
php artisan queue:restart
這個指令會優雅地告知所有隊列作業器,在它們完成處理目前任務後重新啟動,所以就不會任務遺失的問題。
注意:這個指令依靠快取系統來安排重新啟動;預設狀況下,APCu 無法在 CLI 的任務上運作;如果你正在使用 APCu 的話,要把
apc.enable_cli=1
加到你的 APCu 設定裡。
處理失敗的任務
計劃永遠跟不上變化,有時候你的隊列任務就是會失敗。不過別擔心,我們有準備好它發生時的應付方法。Laravel 有個便利的方式可以指定任務的最大重試次數。當任務執行超過該重試次數,它就會被寫入至 failed_jobs
這個資料表。而失敗任務的名稱可以在 config/queue.php
這個設定檔中設定
要建立 failed_jobs
資料表的遷移類別,你可以用 queue:failed-table
指令:
php artisan queue:failed-table
當你執行隊列監聽器時,你可以用 queue:listen
指令的 --tries
參數來指定任務的最大重試次數:
php artisan queue:listen connection-name --tries=3
任務失敗事件
如果你想註冊一個當隊列任務失敗時會被呼叫的事件,你可以用 Queue::failing
方法;這樣你就有機會透過這個事件,用 e-mail 或 HipChat 來通知你的團隊。例如我們可以在 Laravel 內建的 AppServiceProvider
中對這個事件附加一個回呼函式:
<?php
namespace App\Providers;
use Queue;
use Illuminate\Queue\Events\JobFailed;
use Illuminate\Support\ServiceProvider;
class AppServiceProvider extends ServiceProvider
{
/**
* 啟動任何應用程式的服務。
*
* @return void
*/
public function boot()
{
Queue::failing(function (JobFailed $event) {
// $event->connection
// $event->$job
// $event->$data
});
}
/**
* 註冊服務容器。
*
* @return void
*/
public function register()
{
//
}
}
任務類別裡處理失敗的方法
如果想有更細膩的控制,你可以在直接在任務類別裡定義一個 failed
方法,這個方法允許你指定在錯誤發生時該怎麼動作。
<?php
namespace App\Jobs;
use App\Jobs\Job;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
class SendReminderEmail extends Job implements ShouldQueue
{
use InteractsWithQueue, SerializesModels;
/**
* 執行任務。
*
* @param Mailer $mailer
* @return void
*/
public function handle(Mailer $mailer)
{
//
}
/**
* 處理一個失敗的任務
*
* @return void
*/
public function failed()
{
// Called when the job is failing...
}
}
重新嘗試執行失敗任務
要檢視你在 failed_jobs
資料表中所有失敗的任務,你可以用 queue:failed
這個 Artisan 指令:
php artisan queue:failed
queue:failed
指令會列出所有任務 ID、連結、隊列以及失敗時間,任務 ID 會用在重試失敗的任務。例如要重試一個 ID 為 5 的失敗任務,其指令如下:
php artisan queue:retry 5
要重試所有失敗的任務,可以使用 queue:retry
並使用 all
作為 ID:
php artisan queue:retry all
如果你想刪除掉一個失敗任務,可以用 queue:forget
指令:
php artisan queue:forget 5
queue:flush
指令可以讓你刪除所有失敗的任務:
php artisan queue:flush