某些較耗時的工作像是寄信、發通知等,如果卡在處理請求的過程中的話就會造成使用者要多等上數秒才能收到回應,不過這些工作本身對於回應毫無影響,這時候就能考慮將這些工作放入隊列中等待執行,然後馬上回應使用者正在寄信或發通知中。
要建立隊列的話必須指定存放的地方,像是資料庫。
定義隊列存放的方式寫在 config/queue.php
中。
<?php
return [
'default' => env('QUEUE_CONNECTION', 'sync'),
'connections' => [
//...
],
'failed' => [
//...
],
];
首先看 connections
,這就是定義佇列存放位置的地方。
'connections' => [
'sync' => [
'driver' => 'sync',
],
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
'after_commit' => false,
],
// ...
],
要注意的是 connection
只是定義存放位置、跟一些執行隊列的方法,而各個 connection
中可以有複數個隊列,例如我可以在 database
中建立 deafault
隊列、 email
隊列、 notification
隊列,並在執行任務時指定不同的隊列。
如果沒指定隊列的話, Laravel 就會將工作放入各個 connection
定義的 default
隊列。
而如果沒指定 connection
,就會用 config/queue.php
定義的 default connection
'default' => env('QUEUE_CONNECTION', 'sync'),
跟其他 default
一樣有用環境變數,記得在 .env
改。
sync
其實沒有隊列,就是及時執行工作的意思,適合在開發環境用。
database
需要在資料庫中建表單來儲存隊列工作,預設資料表名稱是 jobs
。
可以利用預設的指令來產生建立 jobs
資料表的 migration 。
sail artisan queue:table
sail artisan migrate
至於 failed
定義對於執行時出現錯誤的工作,該存到什麼地方等待確認。
可以用指令產生預設的 database table。
php artisan queue:failed-table
php artisan migrate
能夠被存入隊列的物件需要特別定義,可以先用指令產生 Job
類別,預設放在 app/Jobs/
目錄。
sail artisan make:job ProcessMail
app/Notifications/VerifyNotification.php
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
class ProcessMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public function __construct()
{
//
}
public function handle()
{
//
}
}
引用了很多模組,最主要的是 ShouldQueue
介面以及 Queueable
,有這兩個屬性的話任何類別都能被存入隊列等待執行,例如 Notification。
<?php
namespace App\Notifications;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Notifications\Messages\MailMessage;
use Illuminate\Notifications\Notification;
class VerifyNotification extends Notification implements ShouldQueue
{
use Queueable;
//...
}
可以幫 Notification 加上 ShouldQueue
跟 Queueable
,這樣用 notify
時發送通知的工作就會先被存入隊列,不會即時執行。
use App\Notifications\VerifyNotification;
$user->notify(new VerifyNotification());
回到我們剛剛建立的 Job ,還有一個重點是 Job 必需要有 handle()
函式,實際被存入隊列而後執行的會是 handle()
中的內容。
<?php
namespace App\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;
class ProcessMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
protected $mail;
public function __construct( $user , $mail )
{
$this->user = $user;
$this->mail = $mail;
}
public function handle()
{
Mail::to($this->user)->send($this->mail);
}
}
不過參數是在建立 Job 物件時傳入的,所以要在 __construct()
中設定屬性。
建好 Job 後就能用 dispatch()
將任務放入隊列等待執行。
用 Job 改寫寄驗證信的功能。
其實 Laravel 原本的驗證信功能用的 Notification 只要加上 ShouldQueue
就可以被隊列執行了,不過為了講解來畫蛇添足一下。
use App\Jobs\ProcessMail;
class User extends Authenticatable implements MustVerifyEmail
{
//...
public function sendEmailVerificationNotification()
{
ProcessMail::dispatch( $this , new CustomVerifyMail);
}
}
這時候可以試著申請帳號寄驗證信,不過可能會發現請求執行很久而解信件馬上就被寄出。
可以確認有沒有將 .env
中的設定改為 sync
以外的 connection
,我是改成用 database
.env
QUEUE_CONNECTION=database
改完 .env
記得要讓伺服器重新載入才能夠應用。
sail artisan config:cache
再寄一次可以看到工作被存入資料庫了。
像前面說的執行工作時沒指定的話會把工作排到 default connection 裡的 default queue ,要指定的話在 dispatch 時指定:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');
不過只是存入隊列並不會執行,要用指令開啟隊列執行功能。
sail artisan queue:work
queue:work
會以目前的程式碼進行任務處理,如果改了程式碼要先關掉隊列 (Ctrl + C
)後再開 queue:work
。
如果想要省著一步的話可以用 queue:listen
,不過執行速度會慢一點。
如果沒指定的話 queue:work
會處理的是 default connection 中的 default隊列任務,可以在執行 queue:work
時指定要處理哪個 connection。
sail artisan queue:work redis
或進一步指定用處理個隊列
php artisan queue:work redis --queue=emails
當工作執行時出現 EXception
且未被 catch 的話就會失敗, queue:work
會在失敗時試圖重複執行,預設執行 3 次,可以在執行時決定重複嘗試的次數。
php artisan queue:work redis --tries=3 --backoff=3
--backoff
則是每次嘗試要間隔幾秒。
另外可以在 Job 類別中定義當工作失敗時要執行的 failed()
函式。
class ProcessMail implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
protected $user;
protected $mail;
public function __construct( $user , $mail )
{
$this->user = $user;
$this->mail = $mail;
}
public function handle()
{
Mail::to($this->user)->send($this->mail);
}
public function failed(Throwable $exception)
{
// log failure ...
}
}
failed()
會收到 handle()
拋出的 Exception ,除此之外並不會跟 handle()
共用任何物件中的參數,執行 handle()
跟執行 failed()
的物件是分開的。