佇列
📝 此頁面為 Laravel 官方文檔的繁體中文翻譯。查看原始英文版本
佇列
簡介
在建立 Web 應用程式時,您可能有一些任務(例如解析和儲存上傳的 CSV 檔案)需要花費太長時間才能在典型的 Web 請求期間執行。幸運的是,Laravel 允許您輕鬆地建立佇列作業,這些作業可以在背景中處理。透過將時間密集型任務移動到佇列,您的應用程式可以以閃電般的速度回應 Web 請求,並為您的客戶提供更好的使用者體驗。
Laravel 佇列提供了跨多種不同佇列後端的統一佇列 API,例如 Amazon SQS、Redis,甚至是關聯式資料庫。
Laravel 的佇列配置選項存儲在應用程式的 config/queue.php 配置檔案中。在此檔案中,您將找到框架附帶的每個佇列驅動程式的連接配置,包括資料庫、Amazon SQS、Redis 和 Beanstalkd 驅動程式,以及一個將立即執行作業的同步驅動程式(用於開發或測試期間)。還包含一個 null 佇列驅動程式,它會丟棄佇列作業。
[!NOTE] Laravel Horizon 是一個用於 Redis 驅動的佇列的漂亮儀表板和配置系統。有關更多資訊,請查看完整的 Horizon 文檔。
連接 vs. 佇列
開始使用 Laravel 佇列之前,了解「連接」和「佇列」之間的區別非常重要。在您的 config/queue.php 配置檔案中,有一個 connections 配置陣列。此選項定義了與後端佇列服務(如 Amazon SQS、Beanstalk 或 Redis)的連接。但是,任何給定的佇列連接都可以有多個「佇列」,可以將其視為不同的佇列作業堆疊或堆。
請注意,queue 配置檔案中的每個連接配置範例都包含一個 queue 屬性。這是將作業發送到給定連接時預設將其發送到的佇列。換句話說,如果您在未明確定義應將其發送到哪個佇列的情況下派發作業,則該作業將被放置在連接配置的 queue 屬性中定義的佇列上:
use App\Jobs\ProcessPodcast;
// 此作業被發送到預設連接的預設佇列...
ProcessPodcast::dispatch();
// 此作業被發送到預設連接的 "emails" 佇列...
ProcessPodcast::dispatch()->onQueue('emails');
某些應用程式可能不需要將作業推送到多個佇列,而是更喜歡有一個簡單的佇列。但是,將作業推送到多個佇列對於希望優先處理或細分作業處理方式的應用程式可能特別有用,因為 Laravel 佇列工作者允許您指定它應按優先級處理哪些佇列。例如,如果您將作業推送到 high 佇列,您可以運行一個賦予它們更高處理優先級的工作者:
php artisan queue:work --queue=high,default
驅動程式說明和先決條件
資料庫
要使用 database 佇列驅動程式,您需要一個資料庫表來保存作業。通常,這包含在 Laravel 的預設 0001_01_01_000002_create_jobs_table.php資料庫遷移中;但是,如果您的應用程式不包含此遷移,您可以使用 make:queue-table Artisan 命令來建立它:
php artisan make:queue-table
php artisan migrate
Redis
要使用 redis 佇列驅動程式,您應該在 config/database.php 配置檔案中配置 Redis 資料庫連接。
[!WARNING]
redis佇列驅動程式不支援serializer和compressionRedis 選項。
Redis 叢集
如果您的 Redis 佇列連接使用 Redis 叢集,則您的佇列名稱必須包含金鑰雜湊標籤。這是確保給定佇列的所有 Redis 金鑰都放入同一個雜湊插槽所必需的:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', '{default}'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => null,
'after_commit' => false,
],
阻塞
使用 Redis 佇列時,您可以使用 block_for 配置選項來指定驅動程式在等待作業可用之前應阻塞多少秒,然後才迭代工作者循環並重新輪詢 Redis 資料庫。
根據您的佇列負載調整此值可能比持續輪詢 Redis 資料庫以獲取新作業更有效率。例如,您可以將值設定為 5,以指示驅動程式在等待作業可用時應阻塞五秒:
'redis' => [
'driver' => 'redis',
'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
'queue' => env('REDIS_QUEUE', 'default'),
'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
'block_for' => 5,
'after_commit' => false,
],
[!WARNING] 將
block_for設定為0將導致佇列工作者無限期阻塞,直到有作業可用。這也將阻止訊號(如SIGTERM)被處理,直到處理下一個作業。
SQS 溢位儲存
Amazon SQS 限制佇列訊息負載的最大大小。如果您需要派發可能超過此限制的負載的作業,您可以配置 Laravel 將超大 SQS 負載儲存在快取儲存中,並透過 SQS 發送指針。要啟用此功能,請向您的 SQS 佇列連接配置添加一個 overflow 陣列:
'sqs' => [
'driver' => 'sqs',
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'prefix' => env('SQS_PREFIX', 'https://sqs.us-east-1.amazonaws.com/your-account-id'),
'queue' => env('SQS_QUEUE', 'default'),
'suffix' => env('SQS_SUFFIX'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'after_commit' => false,
'overflow' => [
'enabled' => env('SQS_OVERFLOW_ENABLED', false),
'store' => env('SQS_OVERFLOW_STORE'),
'always' => false,
'delete_after_processing' => true,
'flush_on_clear' => env('SQS_OVERFLOW_FLUSH_ON_CLEAR', false),
],
],
啟用溢位儲存後,Laravel 將把至少 1 MB 的負載儲存在配置的快取儲存中。如果 always 選項為 true,則每個 SQS 負載都將儲存在快取儲存中,無論其大小如何。由於佇列作業在處理時需要從快取儲存中檢索其負載,因此您應該選擇一個可以在工作者處理之前保留負載的儲存。預設情況下,儲存的負載會在作業成功處理並從 SQS 中刪除後被刪除。
如果 flush_on_clear 選項為 true,則當 queue:clear 命令清除 SQS 佇列時,配置的溢位快取儲存將被刷新。由於刷新快取儲存可能會從該儲存中移除所有項目,因此啟用此選項時,應配置 SQS 溢位儲存以使用專用的快取儲存。
其他驅動程式先決條件
以下列出的佇列驅動程式需要以下依賴項。這些依賴項可以透過 Composer 套件管理器安裝:
- Amazon SQS:
aws/aws-sdk-php ~3.0 - Beanstalkd:
pda/pheanstalk ~5.0 - Redis:
predis/predis ~2.0或 phpredis PHP 擴展 - MongoDB:
mongodb/laravel-mongodb
建立作業
生成作業類別
預設情況下,應用程式的所有可佇列作業都存儲在 app/Jobs 目錄中。如果 app/Jobs 目錄不存在,當您運行 make:job Artisan 命令時它將被建立:
php artisan make:job ProcessPodcast
生成的類別將實現 Illuminate\Contracts\Queue\ShouldQueue 介面,向 Laravel 表示應將作業推送到佇列以非同步執行。
[!NOTE] 作業存根可以使用存根發佈來自訂。
類別結構
作業類別非常簡單,通常只包含一個 handle 方法,該方法在作業由佇列處理時被呼叫。首先,讓我們看看一個作業類別範例。在此範例中,我們假設我們管理一個播客發布服務,需要在發布之前處理上傳的播客檔案:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* 執行作業。
*/
public function handle(AudioProcessor $processor): void
{
// 處理上傳的播客...
}
}
在此範例中,請注意我們能夠將一個 Eloquent 模型直接傳遞到佇列作業的構造函數中。由於作業正在使用的 Queueable trait,Eloquent 模型及其加載的關聯將在作業處理時被優雅地序列化和反序列化。
如果您的佇列作業在其構造函數中接受一個 Eloquent 模型,則只有模型的標識符將被序列化到佇列上。當作業實際被處理時,佇列系統將自動從資料庫中重新檢索完整的模型實例及其加載的關聯。這種模型序列化方法允許將更小的作業負載發送到您的佇列驅動程式。
handle 方法依賴注入
當作業由佇列處理時,將呼叫 handle 方法。請注意,我們能夠在作業的 handle 方法上鍵入提示依賴。Laravel 服務容器會自動注入這些依賴。
如果您希望完全控制容器如何將依賴注入到 handle 方法中,可以使用容器的 bindMethod 方法。bindMethod 方法接受一個接收作業和容器的回呼。在回呼內,您可以自由地以任何您希望的方式呼叫 handle 方法。通常,您應該從 App\Providers\AppServiceProvider服務提供者的 boot 方法中呼叫此方法:
use App\Jobs\ProcessPodcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Foundation\Application;
$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
return $job->handle($app->make(AudioProcessor::class));
});
[!WARNING] 二進制資料(如原始圖像內容)應在傳遞給佇列作業之前透過
base64_encode函數傳遞。否則,作業在放入佇列時可能無法正確序列化為 JSON。
佇列關聯
由於當作業被放入佇列時,所有加載的 Eloquent 模型關聯也會被序列化,因此序列化的作業字串有時會變得相當大。此外,當作業被反序列化並從資料庫中重新檢索模型關聯時,它們將被完整地檢索。在作業佇列化過程期間在模型序列化之前應用的任何先前關聯約束在作業反序列化時將不會被應用。因此,如果您希望使用給定關聯的子集,您應該在佇列作業中重新約束該關聯。
或者,要防止關聯被序列化,您可以在設定屬性值時在模型上呼叫 withoutRelations 方法。此方法將返回一個沒有其加載關聯的模型實例:
/**
* 建立一個新的作業實例。
*/
public function __construct(
Podcast $podcast,
) {
$this->podcast = $podcast->withoutRelations();
}
如果您只需要在保留其餘關聯的同時移除特定關聯,可以使用 withoutRelation 方法:
$this->podcast = $podcast->withoutRelation('comments');
如果您正在使用 PHP 構造函數屬性提升,並且希望指示 Eloquent 模型不應序列化其關聯,可以使用 WithoutRelations 屬性:
use Illuminate\Queue\Attributes\WithoutRelations;
/**
* 建立一個新的作業實例。
*/
public function __construct(
#[WithoutRelations]
public Podcast $podcast,
) {}
為了方便,如果您希望在不帶有關係的情況下序列化所有模型,可以將 WithoutRelations 屬性應用於整個類別,而不是將屬性應用於每個模型:
<?php
namespace App\Jobs;
use App\Models\DistributionPlatform;
use App\Models\Podcast;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\WithoutRelations;
#[WithoutRelations]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public Podcast $podcast,
public DistributionPlatform $platform,
) {}
}
如果作業接收一個 Eloquent 模型的集合或陣列而不是單個模型,則當作業被反序列化並執行時,該集合中的模型將不會恢復其關聯。這是為了防止處理大量模型的作業過度使用資源。
唯一的作業
[!WARNING] 唯一的作業需要支援鎖的快取驅動程式。目前,
memcached、redis、dynamodb、database、file和array快取驅動程式支援原子鎖。
[!WARNING] 唯一的作業約束不適用於批次中的作業。
有時,您可能希望確保在任何時候只有一個特定作業的實例在佇列上。您可以透過在作業類別上實現 ShouldBeUnique 介面來執行此操作。此介面不要求您在類別上定義任何額外的方法:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
}
在上面的範例中,UpdateSearchIndex 作業是唯一的。因此,如果另一個作業實例已經在佇列上且尚未完成處理,則不會派發該作業。
在某些情況下,您可能希望定義一個特定的「金鑰」使作業唯一,或者您可能希望指定一個逾時時間,超過該時間後作業不再保持唯一。為此,您可以使用 UniqueFor 屬性,並在作業類別上定義一個 uniqueId 方法:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUnique;
use Illuminate\Queue\Attributes\UniqueFor;
#[UniqueFor(3600)]
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
/**
* 產品實例。
*
* @var \App\Models\Product
*/
public $product;
/**
* 獲取作業的唯一 ID。
*/
public function uniqueId(): string
{
return $this->product->id;
}
}
在上面的範例中,UpdateSearchIndex 作業按產品 ID 唯一。因此,任何具有相同產品 ID 的新派發都將被忽略,直到現有作業完成處理。此外,如果現有作業在一小時內未被處理,則唯一鎖將被釋放,並且可以將另一個具有相同唯一金鑰的作業派發到佇列。
[!WARNING] 如果您的應用程式從多個 Web 伺服器或容器派發作業,則應確保所有伺服器都與同一中央快取伺服器通訊,以便 Laravel 可以準確判斷作業是否唯一。
保持作業唯一直到處理開始
預設情況下,唯一的作業在作業完成處理或失敗其所有重試嘗試後被「解鎖」。但是,可能有些情況您希望您的作業在被處理之前立即解鎖。為此,您的作業應該實現 ShouldBeUniqueUntilProcessing 合約而不是 ShouldBeUnique 合約:
<?php
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
{
// ...
}
唯一作業鎖
在幕後,當派發 ShouldBeUnique 作業時,Laravel 嘗試使用 uniqueId 鍵獲取一個鎖。如果鎖已被持有,則不會派發該作業。當作業完成處理或失敗其所有重試嘗試時,釋放此鎖。預設情況下,Laravel 將使用預設快取驅動程式來獲取此鎖。但是,如果您希望使用另一個驅動程式來獲取鎖,可以定義一個 uniqueVia 方法,該方法應返回應使用的快取驅動程式:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
{
// ...
/**
* 獲取用於唯一作業鎖的快取驅動程式。
*/
public function uniqueVia(): Repository
{
return Cache::driver('redis');
}
}
[!NOTE] 如果您只需要限制作業的並發處理,請改用 WithoutOverlapping 作業中介層。
去抖動作業
有時,您可能希望確保在短時間窗口內多次派發相同作業時,只有最新的派發實際執行。您可以透過向作業添加 DebounceFor 屬性來執行此操作:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\DebounceFor;
#[DebounceFor(30)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(public int $productId)
{
}
/**
* 獲取作業的去抖動 ID。
*/
public function debounceId(): string
{
return (string) $this->productId;
}
}
在上面的範例中,在 30 秒內重複派發相同產品的 UpdateSearchIndex 將對作業進行去抖動,以便只有最新的派發運行。
如果您希望限制頻繁重新派發的作業可以被延遲的時間,可以向 DebounceFor 屬性提供 maxWait 參數:
#[DebounceFor(30, maxWait: 120)]
class UpdateSearchIndex implements ShouldQueue
{
use Queueable;
// ...
}
您可以透過在作業上定義 debounceVia 方法來自訂用於去抖動追蹤的快取儲存:
use Illuminate\Contracts\Cache\Repository;
use Illuminate\Support\Facades\Cache;
public function debounceVia(): Repository
{
return Cache::driver('redis');
}
如果去抖動作業被較新的派發取代,Laravel 將派發 Illuminate\Queue\Events\JobDebounced 事件,並從佇列中移除被取代的作業。
[!WARNING] 去抖動作業和唯一的作業是互斥的。使用
DebounceFor屬性的作業不應實現ShouldBeUnique。
[!WARNING] 如果您的應用程式從多個 Web 伺服器或容器派發去抖動作業,則應確保所有伺服器都與同一中央快取伺服器通訊。
加密作業
Laravel 允許您透過加密來確保作業資料的隱私性和完整性。首先,只需向作業類別添加 ShouldBeEncrypted 介面。一旦將此介面添加到類別中,Laravel 將在將作業推送到佇列之前自動加密它:
<?php
use Illuminate\Contracts\Queue\ShouldBeEncrypted;
use Illuminate\Contracts\Queue\ShouldQueue;
class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
{
// ...
}
作業中介層
作業中介層允許您在佇列作業的執行周圍包裝自訂邏輯,從而減少作業本身的樣板代碼。例如,考慮以下 handle 方法,該方法利用 Laravel 的 Redis 速率限制功能來允許每五秒只處理一個作業:
use Illuminate\Support\Facades\Redis;
/**
* 執行作業。
*/
public function handle(): void
{
Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
info('Lock obtained...');
// 處理作業...
}, function () {
// 無法獲取鎖...
return $this->release(5);
});
}
雖然此代碼是有效的,但 handle 方法的實現變得嘈雜,因為它被 Redis 速率限制邏輯弄得雜亂。此外,對於我們想要速率限制的任何其他作業,必須複製此速率限制邏輯。與其在 handle 方法中進行速率限制,不如定義一個處理速率限制的作業中介層:
<?php
namespace App\Jobs\Middleware;
use Closure;
use Illuminate\Support\Facades\Redis;
class RateLimited
{
/**
* 處理佇列作業。
*
* @param \Closure(object): void $next
*/
public function handle(object $job, Closure $next): void
{
Redis::throttle('key')
->block(0)->allow(1)->every(5)
->then(function () use ($job, $next) {
// 獲取鎖...
$next($job);
}, function () use ($job) {
// 無法獲取鎖...
$job->release(5);
});
}
}
正如您所見,像路由中介層一樣,作業中介層接收正在處理的作業和應被呼叫以繼續處理作業的回呼。
您可以使用 make:job-middleware Artisan 命令生成一個新的作業中介層類別。建立作業中介層後,可以透過從作業的 middleware 方法返回它們將它們附加到作業。此方法不存在於 make:job Artisan 命令搭建的作業上,因此您需要手動將其添加到作業類別:
use App\Jobs\Middleware\RateLimited;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [new RateLimited];
}
速率限制
雖然我們剛剛演示了如何編寫自己的速率限制作業中介層,但 Laravel 實際上包含了一個您可以用來速率限制作業的速率限制中介層。像路由速率限制器一樣,作業速率限制器使用 RateLimiter 外觀的 for 方法定義。
例如,您可能希望允許使用者每小時備份一次資料,同時對高級客戶施加此類限制。為此,您可以在 AppServiceProvider 的 boot 方法中定義一個 RateLimiter:
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
RateLimiter::for('backups', function (object $job) {
return $job->user->vipCustomer()
? Limit::none()
: Limit::perHour(1)->by($job->user->id);
});
}
在上面的範例中,我們定義了一個每小時速率限制;但是,您可以使用 perMinute 方法輕鬆定義基於分鐘的速率限制。此外,您可以將您希望的任何值傳遞給速率限制的 by 方法;但是,此值最常用於按客戶細分速率限制:
return Limit::perMinute(50)->by($job->user->id);
定義好速率限制後,您可以使用 Illuminate\Queue\Middleware\RateLimited 中介層將速率限制器附加到作業。每次作業超過速率限制時,此中介層將根據速率限制持續時間將作業以適當的延遲釋放回佇列:
use Illuminate\Queue\Middleware\RateLimited;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [new RateLimited('backups')];
}
將受速率限制的作業釋放回佇列仍將增加作業的 attempts 總數。您可能希望相應地調整作業類別上的 Tries 和 MaxExceptions 屬性。或者,您可能希望使用 retryUntil 方法來定義不應再嘗試作業的時間量。
使用 releaseAfter 方法,您還可以指定在重新釋放的作業被再次嘗試之前必須經過的秒數:
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->releaseAfter(60)];
}
如果您不希望作業在受速率限制時被重試,可以使用 dontRelease 方法:
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new RateLimited('backups'))->dontRelease()];
}
使用 Redis 進行速率限制
如果您正在使用 Redis,可以使用 Illuminate\Queue\Middleware\RateLimitedWithRedis 中介層,該中間件針對 Redis 進行了微調,比基本速率限制中間件更有效率:
use Illuminate\Queue\Middleware\RateLimitedWithRedis;
public function middleware(): array
{
return [new RateLimitedWithRedis('backups')];
}
connection 方法可用於指定中間件應使用哪個 Redis 連接:
return [(new RateLimitedWithRedis('backups'))->connection('limiter')];
防止作業重疊
Laravel 包含一個 Illuminate\Queue\Middleware\WithoutOverlapping 中介層,允許您根據任意鍵防止作業重疊。當佇列作業正在修改一次只應由一個作業修改的資源時,這可能很有用。
例如,讓我們想像您有一個更新使用者信用評分的佇列作業,並且您想要防止相同使用者 ID 的信用評分更新作業重疊。為此,您可以從作業的 middleware 方法返回 WithoutOverlapping 中介層:
use Illuminate\Queue\Middleware\WithoutOverlapping;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [new WithoutOverlapping($this->user->id)];
}
將重疊的作業釋放回佇列仍將增加作業的嘗試總次數。您可能希望相應地調整作業類別上的 Tries 和 MaxExceptions 屬性。例如,將 Tries 保留為預設值 1 將防止任何重疊的作業稍後被重試。
任何相同類型的重疊作業都將被釋放回佇列。您還可以指定在重新釋放的作業被再次嘗試之前必須經過的秒數:
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
}
如果您希望立即刪除任何重疊的作業,以便它們不會被重試,可以使用 dontRelease 方法:
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->dontRelease()];
}
WithoutOverlapping 中介層由 Laravel 的原子鎖功能提供支援。有時,您的作業可能會意外失敗或逾時,導致鎖未被釋放。因此,您可以使用 expireAfter 方法明確定義鎖過期時間。例如,下面的範例將指示 Laravel 在作業開始處理三分鐘後釋放 WithoutOverlapping 鎖:
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
}
[!WARNING]
WithoutOverlapping中介層需要支援鎖的快取驅動程式。目前,memcached、redis、dynamodb、database、file和array快取驅動程式支援原子鎖。
跨作業類別共享鎖金鑰
預設情況下,WithoutOverlapping 中介層只會防止相同類別的重疊作業。因此,即使兩個不同的作業類別使用相同的鎖金鑰,也不會阻止它們重疊。但是,您可以使用 shared 方法指示 Laravel 跨作業類別應用該金鑰:
use Illuminate\Queue\Middleware\WithoutOverlapping;
class ProviderIsDown
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
class ProviderIsUp
{
// ...
public function middleware(): array
{
return [
(new WithoutOverlapping("status:{$this->provider}"))->shared(),
];
}
}
節流異常
Laravel 包含一個 Illuminate\Queue\Middleware\ThrottlesExceptions 中介層,允許您對異常進行節流。一旦作業拋出給定數量的異常,所有進一步執行作業的嘗試都將被延遲,直到經過指定的時間間隔。此中間件對於與不穩定的第三方服務互動的作業特別有用。
例如,讓我們想像一個與開始拋出異常的第三方 API 互動的佇列作業。要對異常進行節流,您可以從作業的 middleware 方法返回 ThrottlesExceptions 中間件。通常,此中介層應與實現基於時間的嘗試的作業配對:
use DateTime;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [new ThrottlesExceptions(10, 5 * 60)];
}
/**
* 判斷作業應逾時的時間。
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 30);
}
中間件接受的第一個構造函數參數是作業在被節流之前可以拋出的異常數量,而第二個構造函數參數是在作業被節流後再次嘗試之前應經過的秒數。在上面的代碼範例中,如果作業連續拋出 10 個異常,我們將等待 5 分鐘後再次嘗試作業,受限於 30 分鐘的時間限制。
當作業拋出異常但尚未達到異常閾值時,作業通常會立即被重試。但是,您可以透過在將中介層附加到作業時呼叫 backoff 方法來指定應延遲此類作業的分鐘數:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
}
backoff 方法還接受一個接收拋出異常的閉包,允許動態確定延遲:
use App\Exceptions\RateLimitedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
use Throwable;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 5 * 60))->backoff(
fn (Throwable $throwable) => $throwable instanceof RateLimitedException
? $throwable->retryAfterMinutes()
: 5
)];
}
內部使用 Laravel 的快取系統來實現速率限制,作業的類別名稱用作快取「金鑰」。您可以透過在將中介層附加到作業時呼叫 by 方法來覆蓋此金鑰。如果您有多個作業與同一第三方服務互動,並且您希望它們共享一個共同的節流「儲桶」,確保它們尊重單個共享限制,這可能很有用:
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
}
預設情況下,此中間件將對每個異常進行節流。您可以透過在將中介層附加到作業時呼叫 when 方法來修改此行為。只有當提供給 when 方法的閉包返回 true 時,才會對異常進行節流:
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->when(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}
與 when 方法(將作業釋放回佇列或拋出異常)不同,deleteWhen 方法允許您在發生給定異常時完全刪除作業:
use App\Exceptions\CustomerDeletedException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)];
}
如果您希望將節流異常報告給應用程式的異常處理器,可以在將中介層附加到作業時呼叫 report 方法。或者,您可以向 report 方法提供一個閉包,只有當給定的閉包返回 true 時才會報告異常:
use Illuminate\Http\Client\HttpClientException;
use Illuminate\Queue\Middleware\ThrottlesExceptions;
/**
* 獲取作業應透過的中介層。
*
* @return array
*/
public function middleware(): array
{
return [(new ThrottlesExceptions(10, 10 * 60))->report(
fn (Throwable $throwable) => $throwable instanceof HttpClientException
)];
}
使用 Redis 節流異常
如果您正在使用 Redis,可以使用 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis 中介層,該中間件針對 Redis 進行了微調,比基本異常節流中間件更有效率:
use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
public function middleware(): array
{
return [new ThrottlesExceptionsWithRedis(10, 10 * 60)];
}
connection 方法可用於指定中間件應使用哪個 Redis 連接:
return [(new ThrottlesExceptionsWithRedis(10, 10 * 60))->connection('limiter')];
跳過作業
Skip 中間件允許您指定應跳過/刪除的作業,而無需修改作業的邏輯。Skip::when 方法將在給定條件評估為 true 時刪除作業,而 Skip::unless 方法將在條件評估為 false 時刪除作業:
use Illuminate\Queue\Middleware\Skip;
/**
* 獲取作業應透過的中介層。
*/
public function middleware(): array
{
return [
Skip::when($condition),
];
}
您還可以向 when 和 unless 方法傳遞一個 Closure 以進行更複雜的條件評估:
use Illuminate\Queue\Middleware\Skip;
/**
* 獲取作業應透過的中介層。
*/
public function middleware(): array
{
return [
Skip::when(function (): bool {
return $this->shouldSkip();
}),
];
}
派發作業
編寫好作業類別後,您可以使用作業本身的 dispatch 方法來派發它。傳遞給 dispatch 方法的參數將被傳遞給作業的構造函數:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* 儲存一個新的播客。
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast);
return redirect('/podcasts');
}
}
如果您希望有條件地派發作業,可以使用 dispatchIf 和 dispatchUnless 方法:
ProcessPodcast::dispatchIf($accountActive, $podcast);
ProcessPodcast::dispatchUnless($accountSuspended, $podcast);
在新的 Laravel 應用程式中,database 連接被定義為預設佇列。您可以透過更改應用程式 .env 檔案中的 QUEUE_CONNECTION 環境變數來指定不同的預設佇列連接。
延遲派發
如果您希望指定作業不應立即可供佇列工作者處理,可以在派發作業時使用 delay 方法。例如,讓我們指定作業在派發後 10 分鐘才可供處理:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* 儲存一個新的播客。
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// ...
ProcessPodcast::dispatch($podcast)
->delay(now()->plus(minutes: 10));
return redirect('/podcasts');
}
}
在某些情況下,作業可能有預設延遲配置。如果您需要繞過此延遲並立即派發作業進行處理,可以使用 withoutDelay 方法:
ProcessPodcast::dispatch($podcast)->withoutDelay();
[!WARNING] Amazon SQS 佇列服務的最大延遲時間為 15 分鐘。
同步派發
如果您希望立即(同步)派發作業,可以使用 dispatchSync 方法。使用此方法時,作業不會被放入佇列,而是立即在當前程序中執行:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* 儲存一個新的播客。
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 建立播客...
ProcessPodcast::dispatchSync($podcast);
return redirect('/podcasts');
}
}
延遲同步派發
使用延遲同步派發,您可以派發一個在當前程序中處理的作業,但在 HTTP 回應已發送給使用者之後。這允許您同步處理「佇列」作業,而不會減慢使用者的應用程式體驗。要延遲同步作業的執行,請將作業派發到 deferred 連接:
RecordDelivery::dispatch($order)->onConnection('deferred');
deferred 連接還作為預設故障轉移佇列。
同樣,background 連接在 HTTP 回應已發送給使用者後處理作業;但是,作業在單獨生成的 PHP 程序中處理,允許 PHP-FPM/應用程式工作者可用于處理另一個傳入的 HTTP 請求:
RecordDelivery::dispatch($order)->onConnection('background');
批量派發
如果您需要一次派發許多獨立的作業,並且不需要批次追蹤或回呼,可以使用 Bus 外觀的 bulk 方法。Laravel 將按其配置的佇列連接和佇列名稱對作業進行分組,並將每個組批量推送到相應的佇列:
use App\Jobs\ProcessUser;
use Illuminate\Support\Facades\Bus;
Bus::bulk(
$users->map(fn ($user) => new ProcessUser($user))
);
在派發前準備作業
如果作業在被推送到佇列之前需要準備或檢查其狀態,則該作業可以實現 Illuminate\Contracts\Queue\PreparesForDispatch 介面。Laravel 將在派發作業之前呼叫作業的 prepareForDispatch 方法。如果此方法返回 false,則不會派發作業:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\PreparesForDispatch;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Support\Facades\Cache;
class SyncPodcasts implements PreparesForDispatch, ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public array $podcastIds,
) {}
/**
* 在派發前準備作業。
*/
public function prepareForDispatch(): bool
{
return collect($this->podcastIds)
->reject(fn (int $id) => Cache::has("podcast-syncing:{$id}"))
->isNotEmpty();
}
}
作業和資料庫交易
雖然在資料庫交易內行派發作業完全沒問題,但您應特別注意確保您的作業實際上能夠成功執行。在交易內行派發作業時,作業有可能在父交易提交之前被工作者處理。當這種情況發生時,在資料庫交易期間對模型或資料庫記錄進行的任何更新可能尚未反映在資料庫中。此外,在交易內行建立的任何模型或資料庫記錄可能不存在於資料庫中。
幸運的是,Laravel 提供了幾種解決此問題的方法。首先,您可以在佇列連接的配置陣列中設定 after_commit 連接選項:
'redis' => [
'driver' => 'redis',
// ...
'after_commit' => true,
],
當 after_commit 選項為 true 時,您可以在資料庫交易內行派發作業;但是,Laravel 將等到打開的父資料庫交易已提交後才實際派發作業。當然,如果當前沒有打開的資料庫交易,則作業將立即被派發。
如果由於交易期間發生的異常而回滾交易,則在該交易期間派發的作業將被丟棄。
[!NOTE] 將
after_commit配置選項設定為true還會導致任何佇列事件監聽器、可郵寄物件、通知和廣播事件在所有打開的資料庫交易提交後才被派發。
行內指定提交派發行為
如果您沒有將 after_commit 佇列連接配置選項設定為 true,您仍然可以指示特定的作業應在所有打開的資料庫交易提交後被派發。為此,您可以在派發操作上鏈式呼叫 afterCommit 方法:
use App\Jobs\ProcessPodcast;
ProcessPodcast::dispatch($podcast)->afterCommit();
同樣,如果 after_commit 配置選項設定為 true,您可以指示特定的作業應立即被派發,而無需等待任何打開的資料庫交易提交:
ProcessPodcast::dispatch($podcast)->beforeCommit();
作業鏈
作業鏈允許您指定一組佇列作業,這些作業應在主作業成功執行後按順序運行。如果序列中的一個作業失敗,則不會運行其餘的作業。要執行佇列作業鏈,您可以使用 Bus 外觀提供的 chain 方法。Laravel 的命令匯流排是建立佇列作業派發的低階元件:
use App\Jobs\OptimizePodcast;
use App\Jobs\ProcessPodcast;
use App\Jobs\ReleasePodcast;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->dispatch();
除了鏈式呼叫作業類別實例外,您還可以鏈式呼叫閉包:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
function () {
Podcast::update(/* ... */);
},
])->dispatch();
[!WARNING] 在作業內行使用
$this->delete()方法刪除作業不會阻止鏈式作業被處理。鏈將僅在鏈中的作業失敗時停止執行。
鏈連接和佇列
如果您希望指定鏈式作業應使用的連接和佇列,可以使用 onConnection 和 onQueue 方法。這些方法指定應使用的佇列連接和佇列名稱,除非佇列作業被明確分配了不同的連接/佇列:
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->onConnection('redis')->onQueue('podcasts')->dispatch();
向鏈添加作業
偶爾,您可能需要從鏈中的另一個作業內將作業前置或附加到現有的作業鏈。您可以使用 prependToChain 和 appendToChain 方法來實現這一點:
/**
* 執行作業。
*/
public function handle(): void
{
// ...
// 前置到當前鏈,在當前作業後立即運行作業...
$this->prependToChain(new TranscribePodcast);
// 附加到當前鏈,在鏈末尾運行作業...
$this->appendToChain(new TranscribePodcast);
}
鏈失敗
鏈式作業時,您可以使用 catch 方法指定一個在鏈中的作業失敗時應被呼叫的閉包。給定的回呼將接收導致作業失敗的 Throwable 實例:
use Illuminate\Support\Facades\Bus;
use Throwable;
Bus::chain([
new ProcessPodcast,
new OptimizePodcast,
new ReleasePodcast,
])->catch(function (Throwable $e) {
// 鏈中的作業已失敗...
})->dispatch();
[!WARNING] 由於鏈回呼是由 Laravel 佇列序列化並在稍後執行的,因此不應在鏈回呼中使用
$this變數。
自訂佇列和連接
派發到特定佇列
透過將作業推送到不同的佇列,您可以「分類」您的佇列作業,甚至優先處理您分配給各個佇列的工作者數量。請注意,這不會將作業推送到佇列配置檔案中定義的不同佇列「連接」,而只會推送到單個連接中的特定佇列。要指定佇列,請在派發作業時使用 onQueue 方法:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* 儲存一個新的播客。
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 建立播客...
ProcessPodcast::dispatch($podcast)->onQueue('processing');
return redirect('/podcasts');
}
}
或者,您可以透過在作業的構造函數中呼叫 onQueue 方法來指定作業的佇列:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct()
{
$this->onQueue('processing');
}
}
派發到特定連接
如果您的應用程式與多個佇列連接互動,可以使用 onConnection 方法指定將作業推送到哪個連接:
<?php
namespace App\Http\Controllers;
use App\Jobs\ProcessPodcast;
use App\Models\Podcast;
use Illuminate\Http\RedirectResponse;
use Illuminate\Http\Request;
class PodcastController extends Controller
{
/**
* 儲存一個新的播客。
*/
public function store(Request $request): RedirectResponse
{
$podcast = Podcast::create(/* ... */);
// 建立播客...
ProcessPodcast::dispatch($podcast)->onConnection('sqs');
return redirect('/podcasts');
}
}
您可以鏈式呼叫 onConnection 和 onQueue 方法來指定作業的連接和佇列:
ProcessPodcast::dispatch($podcast)
->onConnection('sqs')
->onQueue('processing');
或者,您可以透過在作業的構造函數中呼叫 onConnection 方法來指定作業的連接:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct()
{
$this->onConnection('sqs');
}
}
佇列路由
您可以使用 Queue 外觀的 route 方法為特定作業類別定義預設連接和佇列。當您想要確保某些作業始終使用特定佇列而無需在作業上指定連接或佇列時,這很有用。
除了路由特定的作業類別外,您還可以向 route 方法傳遞一個介面、trait 或父類別。當您這樣做時,實現該介面、使用該 trait 或擴展該父類別的任何作業都將自動使用配置的連接和佇列。
通常,您應該從服務提供者的 boot 方法中呼叫 route 方法:
use App\Concerns\RequiresVideo;
use App\Jobs\ProcessPodcast;
use App\Jobs\ProcessVideo;
use Illuminate\Support\Facades\Queue;
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
Queue::route(RequiresVideo::class, queue: 'video');
}
當指定了連接但未指定佇列時,作業將被發送到預設佇列:
Queue::route(ProcessPodcast::class, connection: 'redis');
您還可以透過向 route 方法傳遞一個陣列來一次路由多個作業類別:
Queue::route([
ProcessPodcast::class => ['podcasts', 'redis'], // 佇列和連接
ProcessVideo::class => 'videos', // 僅佇列(使用預設連接)
]);
[!NOTE] 佇列路由仍然可以按作業基礎被作業覆蓋。
指定最大作業嘗試次數/逾時值
最大嘗試次數
作業嘗試次數是 Laravel 佇列系統的核心概念,並支援許多進階功能。雖然它們最初可能看起來很令人困惑,但在修改預設配置之前了解它們如何運作非常重要。
當派發作業時,它被推送到佇列上。然後工作者拾取它並嘗試執行它。這是一次作業嘗試。
但是,嘗試並不意味著作業的 handle 方法已被執行。嘗試也可以透過多種方式被「消耗」:
- 作業在執行期間遇到未處理的異常。
- 作業使用
$this->release()手動釋放回佇列。 - 像
WithoutOverlapping或RateLimited這樣的中間件無法獲取鎖並釋放了作業。 - 作業逾時了。
- 作業的
handle方法運行並完成,沒有拋出異常。
您可能不希望無限期地嘗試作業。因此,Laravel 提供了多種方式來指定作業可以被嘗試的次數或時間長度。
[!NOTE] 預設情況下,Laravel 只會嘗試一次作業。如果您的作業使用
WithoutOverlapping或RateLimited等中介層,或者您正在手動釋放作業,則可能需要透過tries選項增加允許的嘗試次數。
指定作業可以被嘗試的最大次數的一種方法是透過 Artisan 命令列上的 --tries 切換。這將應用於工作者處理的所有作業,除非被處理的作業指定了它可以被嘗試的次數:
php artisan queue:work --tries=3
如果作業超過其最大嘗試次數,則將被視為「失敗」作業。有關處理失敗作業的更多資訊,請查閱失敗作業文檔。如果向 queue:work 命令提供 --tries=0,則作業將被無限期重試。
您可以透過使用 Tries 屬性在作業類別本身上定義最大嘗試次數來採取更細粒度的方法。如果在作業上指定了最大嘗試次數,它將優先於命令列上提供的 --tries 值:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Tries;
#[Tries(5)]
class ProcessPodcast implements ShouldQueue
{
// ...
}
如果您需要動態控制特定作業的最大嘗試次數,可以在作業上定義一個 tries 方法:
/**
* 判斷作業可以被嘗試的次數。
*/
public function tries(): int
{
return 5;
}
基於時間的嘗試
作為定義作業在失敗之前可以被嘗試次數的替代方案,您可以定義不應再嘗試作業的時間。這允許作業在給定的時間框架內被嘗試任意次數。要定義不應再嘗試作業的時間,請向作業類別添加 retryUntil 方法。此方法應返回一個 DateTime 實例:
use DateTime;
/**
* 判斷作業應逾時的時間。
*/
public function retryUntil(): DateTime
{
return now()->plus(minutes: 10);
}
如果同時定義了 retryUntil 和 tries,Laravel 優先使用 retryUntil 方法。
最大異常數
有時您可能希望指定作業可以被嘗試多次,但如果重試是由給定數量的未處理異常觸發的(而不是直接由 release 方法釋放),則應失敗。為此,您可以在作業類別上使用 Tries 和 MaxExceptions 屬性:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\MaxExceptions;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Support\Facades\Redis;
#[Tries(25)]
#[MaxExceptions(3)]
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 執行作業。
*/
public function handle(): void
{
Redis::throttle('key')->allow(10)->every(60)->then(function () {
// 獲取鎖,處理播客...
}, function () {
// 無法獲取鎖...
return $this->release(10);
});
}
}
在此範例中,如果應用程式無法獲取 Redis 鎖,則作業將被釋放十秒,並繼續重試最多 25 次。但是,如果作業拋出三個未處理的異常,則作業將失敗。
按異常停止重試
有時異常表示佇列作業應立即失敗,而不是被釋放以進行另一次嘗試。您可以使用應用程式 bootstrap/app.php 檔案中的 dontRetry 異常方法來配置應停止作業重試的異常類型:
use App\Exceptions\InvalidPodcastSourceException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetry([
InvalidPodcastSourceException::class,
]);
})
如果您需要更多控制重試何時停止,可以向 dontRetryWhen 方法提供一個閉包。當閉包返回 true 時,作業將被標記為失敗,並且不會被重試:
use App\Exceptions\PodcastProcessingException;
use Illuminate\Foundation\Configuration\Exceptions;
->withExceptions(function (Exceptions $exceptions): void {
$exceptions->dontRetryWhen(function (PodcastProcessingException $e) {
return $e->reason() === 'Subscription expired';
});
})
逾時
通常,您大致知道您預期佇列作業需要多長時間。為此,Laravel 允許您指定一個「逾時」值。預設情況下,逾時值為 60 秒。如果作業處理的時間超過逾時值指定的秒數,則處理該作業的工作者將以錯誤退出。通常,工作者將由在伺服器上配置的進程管理器自動重新啟動。
作業可以運行的最大秒數可以使用 Artisan 命令列上的 --timeout 切換來指定:
php artisan queue:work --timeout=30
如果作業透過持續逾時超過其最大嘗試次數,則將被標記為失敗。
您還可以使用作業類別上的 Timeout 屬性來定義允許作業運行的最大秒數。如果在作業上指定了逾時,則它將優先於命令列上指定的任何逾時:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Timeout;
#[Timeout(120)]
class ProcessPodcast implements ShouldQueue
{
// ...
}
有時,IO 阻塞程序(如通訊端或外部 HTTP 連接)可能不尊重您指定的逾時。因此,使用這些功能時,您還應該始終嘗試使用它們的 API 指定逾時。例如,使用 Guzzle 時,您應該始終指定連接和請求逾時值。
[!WARNING] PCNTL PHP 擴展必須安裝才能指定作業逾時。此外,作業的「逾時」值應始終小於其「重試後」值。否則,作業可能在實際完成執行或逾時之前被重新嘗試。
逾時失敗
如果您希望指示作業在逾時時應被標記為失敗,可以在作業類別上使用 FailOnTimeout 屬性:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\FailOnTimeout;
#[FailOnTimeout]
class ProcessPodcast implements ShouldQueue
{
// ...
}
[!NOTE] 預設情況下,當作業逾時時,它消耗一次嘗試並被釋放回佇列(如果允許重試)。但是,如果您配置作業在逾時時失敗,則無論為嘗試設定的值如何,都不會重試。
SQS FIFO 和公平佇列
Laravel 支援 Amazon SQS FIFO(先進先出)和公平佇列。FIFO 佇列允許您按發送的確切順序處理作業,同時透過訊息去重確保精確一次處理。
FIFO 佇列需要一個訊息群組 ID 來判斷哪些作業可以並行處理。具有相同群組 ID 的作業按順序處理,而具有不同群組 ID 的訊息可以並行處理。
Laravel 提供了一個流暢的 onGroup 方法來在派發作業時指定訊息群組 ID:
ProcessOrder::dispatch($order)
->onGroup("customer-{$order->customer_id}");
SQS FIFO 佇列支援訊息去重,以確保精確一次處理。在您的作業類別中實現一個 deduplicationId 方法來提供自訂去重 ID:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessSubscriptionRenewal implements ShouldQueue
{
use Queueable;
// ...
/**
* 獲取作業的去重 ID。
*/
public function deduplicationId(): string
{
return "renewal-{$this->subscription->id}";
}
}
公平佇列
如果您使用的是 SQS 標準佇列,設定訊息群組將啟用公平佇列。換句話說,一旦您分配了群組,SQS 將使用它們來維護跨租戶/工作負載的公平傳遞。不需要額外的 Laravel 配置。
除了在派發時呼叫 onGroup 外,您還可以直接在作業上定義一個 messageGroup 方法:
<?php
namespace App\Jobs;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ProcessOrder implements ShouldQueue
{
use Queueable;
// ...
/**
* 獲取作業的訊息群組。
*/
public function messageGroup(): string
{
return "customer-{$this->order->customer_id}";
}
}
FIFO 監聽器、郵件和通知
使用 FIFO 佇列時,您還需要在監聽器、郵件和通知上定義訊息群組。或者,您可以將這些物件的佇列實例派發到非 FIFO 佇列。
要為佇列事件監聽器定義訊息群組,請在監聽器上定義一個 messageGroup 方法。您還可以選擇性地定義一個 deduplicationId 方法:
<?php
namespace App\Listeners;
class SendShipmentNotification
{
// ...
/**
* 獲取作業的訊息群組。
*/
public function messageGroup(): string
{
return 'shipments';
}
/**
* 獲取作業的去重 ID。
*/
public function deduplicationId(): string
{
return "shipment-notification-{$this->shipment->id}";
}
}
當發送將在 FIFO 佇列上排隊的郵件訊息時,您應該在發送通知時呼叫 onGroup 方法,並可選擇性地呼叫 withDeduplicator 方法:
use App\Mail\InvoicePaid;
use Illuminate\Support\Facades\Mail;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
Mail::to($request->user())->send($invoicePaid);
當發送將在 FIFO 佇列上排隊的通知時,您應該在發送通知時呼叫 onGroup 方法,並可選擇性地呼叫 withDeduplicator 方法:
use App\Notifications\InvoicePaid;
$invoicePaid = (new InvoicePaid($invoice))
->onGroup('invoices')
->withDeduplicator(fn () => 'invoices-'.$invoice->id);
$user->notify($invoicePaid);
佇列故障轉移
failover 佇列驅動程式在將作業推送到佇列時提供自動故障轉移功能。如果 failover 配置的主要佇列連接因任何原因失敗,Laravel 將自動嘗試將作業推送到清單中配置的下一個連接。對於確保佇列可靠性至關重要的生產環境中的高可用性,這特別有用。
要配置故障轉移佇列連接,請指定 failover 驅動程式並提供一個按順序嘗試的連接名稱陣列。預設情況下,Laravel 在應用程式的 config/queue.php 配置檔案中包含一個範例故障轉移配置:
'failover' => [
'driver' => 'failover',
'connections' => [
'redis',
'database',
'sync',
],
],
配置好使用 failover 驅動程式的連接後,您需要在應用程式的 .env 檔案中將故障轉移連接設定為預設佇列連接,以使用故障轉移功能:
QUEUE_CONNECTION=failover
接下來,為故障轉移連接清單中的每個連接啟動至少一個工作者:
php artisan queue:work redis
php artisan queue:work database
[!NOTE] 您不需要為使用
sync、background或deferred佇列驅動程式的連接運行工作者,因為這些驅動程式在當前 PHP 程序中處理作業。
當佇列連接操作失敗並啟用故障轉移時,Laravel 將派發 Illuminate\Queue\Events\QueueFailedOver 事件,允許您檢舉或記錄佇列連接已失敗。
[!NOTE] 如果您使用 Laravel Horizon,請記住 Horizon 只管理 Redis 佇列。如果您的故障轉移清單包含
database,則應在 Horizon 旁邊運行一個常規的php artisan queue:work database程序。
錯誤處理
如果作業在處理期間拋出異常,作業將自動被釋放回佇列,以便可以再次嘗試。作業將繼續被釋放,直到它已被嘗試應用程式允許的最大次數。最大嘗試次數由 queue:work Artisan 命令上使用的 --tries 切換定義。或者,最大嘗試次數可以在作業類別本身上定義。可以在下面找到有關運行佇列工作者的更多資訊。
手動釋放作業
有時您可能希望手動將作業釋放回佇列,以便稍後可以再次嘗試。您可以透過呼叫 release 方法來實現這一點:
/**
* 執行作業。
*/
public function handle(): void
{
// ...
$this->release();
}
預設情況下,release 方法將作業釋放回佇列以進行即時處理。但是,您可以透過向 release 方法傳遞一個整數或日期實例來指示佇列在經過給定秒數之前不使作業可供處理:
$this->release(10);
$this->release(now()->plus(seconds: 10));
手動失敗作業
偶爾您可能需要手動將作業標記為「失敗」。為此,您可以呼叫 fail 方法:
/**
* 執行作業。
*/
public function handle(): void
{
// ...
$this->fail();
}
如果您希望因為捕獲的異常而將作業標記為失敗,可以將異常傳遞給 fail 方法。或者,為了方便,您可以傳遞一個字串錯誤訊息,它將為您轉換為異常:
$this->fail($exception);
$this->fail('Something went wrong.');
[!NOTE] 有關失敗作業的更多資訊,請查看處理作業失敗的文檔。
在特定異常時失敗作業
FailOnException作業中介層允許您在拋出特定異常時中斷重試。這允許在暫時性異常(如外部 API 錯誤)時重試,但在永久性異常(如使用者權限被撤銷)時永久失敗作業:
<?php
namespace App\Jobs;
use App\Models\User;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Illuminate\Queue\Attributes\Tries;
use Illuminate\Queue\Middleware\FailOnException;
use Illuminate\Support\Facades\Http;
#[Tries(3)]
class SyncChatHistory implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public User $user,
) {}
/**
* 執行作業。
*/
public function handle(): void
{
$this->user->authorize('sync-chat-history');
$response = Http::throw()->get(
"https://chat.laravel.test/?user={$this->user->uuid}"
);
// ...
}
/**
* 獲取作業應透過的中介層。
*/
public function middleware(): array
{
return [
new FailOnException([AuthorizationException::class])
];
}
}
作業批次
Laravel 的作業批次功能允許您輕鬆地並行執行一組作業,然後在作業批次完成執行時執行某些操作。
開始之前,您應該建立一個資料庫遷移來建立一個表,該表將包含關於作業批次的元資料資訊,例如它們的完成百分比。可以使用 make:queue-batches-table Artisan 命令生成此遷移:
php artisan make:queue-batches-table
php artisan migrate
定義可批次作業
要定義一個可批次作業,您應該像平常一樣建立一個可佇列作業;但是,您應該將 Illuminate\Bus\Batchable trait 添加到作業類別。此 trait 提供對 batch 方法的存取,該方法可用於檢索作業正在其中執行的當前批次:
<?php
namespace App\Jobs;
use Illuminate\Bus\Batchable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportCsv implements ShouldQueue
{
use Batchable, Queueable;
/**
* 執行作業。
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
// 判斷批次是否已被取消...
return;
}
// 導入 CSV 檔案的一部分...
}
}
派發批次
要派發一批作業,您應該使用 Bus 外觀的 batch 方法。當然,當與完成回呼結合使用時,批次處理主要才有用。因此,您可以使用 then、catch 和 finally 方法來為批次定義完成回呼。呼叫時,每個回呼都將接收一個 Illuminate\Bus\Batch 實例。
運行多個佇列工作者時,批次中的作業將被並行處理。因此,作業完成的順序可能與它們被添加到批次的順序不同。有關如何按順序執行一系列作業的資訊,請查閱我們關於作業鏈和批次的文檔。
在此範例中,我們將想像我們正在佇列化一批作業,每個作業處理 CSV 檔案中的給定行數:
use App\Jobs\ImportCsv;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
use Throwable;
$batch = Bus::batch([
new ImportCsv(1, 100),
new ImportCsv(101, 200),
new ImportCsv(201, 300),
new ImportCsv(301, 400),
new ImportCsv(401, 500),
])->before(function (Batch $batch) {
// 批次已建立但尚未添加任何作業...
})->progress(function (Batch $batch) {
// 單個作業已成功完成...
})->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->catch(function (Batch $batch, Throwable $e) {
// 檢測到批次作業失敗...
})->finally(function (Batch $batch) {
// 批次已完成執行...
})->dispatch();
return $batch->id;
批次的 ID(可以透過 $batch->id 屬性存取)可用於在派發後查詢 Laravel 命令匯流排以獲取有關批次的資訊。
[!WARNING] 由於批次回呼是由 Laravel 佇列序列化並在稍後執行的,因此不應在回呼中使用
$this變數。此外,由於批次作業被包裝在資料庫交易中,因此不應在作業內行執行觸發隱式提交的資料庫語句。
命名批次
某些工具(如 Laravel Horizon 和 Laravel Telescope)如果批次被命名,可能會為批次提供更使用者友好的調試資訊。要為批次分配一個任意名稱,您可以在定義批次時呼叫 name 方法:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->name('Import CSV')->dispatch();
批次連接和佇列
如果您希望指定批次作業應使用的連接和佇列,可以使用 onConnection 和 onQueue 方法。所有批次作業必須在同一個連接和佇列中執行:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->onConnection('redis')->onQueue('imports')->dispatch();
鏈和批次
您可以透過將鏈式作業放在陣列中來在批次內定義一組鏈式作業。例如,我們可以並行執行兩個作業鏈,並在兩個作業鏈都完成處理時執行一個回呼:
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
Bus::batch([
[
new ReleasePodcast(1),
new SendPodcastReleaseNotification(1),
],
[
new ReleasePodcast(2),
new SendPodcastReleaseNotification(2),
],
])->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->dispatch();
相反,您可以透過在鏈內定義批次來在鏈內運行批次作業。例如,您可以首先運行一批作業來發布多個播客,然後運行一批作業來發送發布通知:
use App\Jobs\FlushPodcastCache;
use App\Jobs\ReleasePodcast;
use App\Jobs\SendPodcastReleaseNotification;
use Illuminate\Support\Facades\Bus;
Bus::chain([
new FlushPodcastCache,
Bus::batch([
new ReleasePodcast(1),
new ReleasePodcast(2),
]),
Bus::batch([
new SendPodcastReleaseNotification(1),
new SendPodcastReleaseNotification(2),
]),
])->dispatch();
向批次添加作業
有時,從批次作業內向批次添加額外的作業可能很有用。當您需要對數千個作業進行批次處理時,此模式可能很有用,這些作業可能需要太長時間才能在 Web 請求期間派發。因此,您可能希望派發一批初始的「載入器」作業,用更多的作業來充實批次:
$batch = Bus::batch([
new LoadImportBatch,
new LoadImportBatch,
new LoadImportBatch,
])->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->name('Import Contacts')->dispatch();
在此範例中,我們將使用 LoadImportBatch 作業用額外的作業來充實批次。為此,我們可以使用可以透過作業的 batch 方法存取的批次實例上的 add 方法:
use App\Jobs\ImportContacts;
use Illuminate\Support\Collection;
/**
* 執行作業。
*/
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
$this->batch()->add(Collection::times(1000, function () {
return new ImportContacts;
}));
}
[!WARNING] 您只能從屬於同一批次的作業中向批次添加作業。
檢查批次
提供給批次完成回呼的 Illuminate\Bus\Batch 實例具有多種屬性和方法,可協助您與給定的批次作業互動並檢查它:
// 批次的 UUID...
$batch->id;
// 批次的名稱(如果適用)...
$batch->name;
// 分配給批次的作業數量...
$batch->totalJobs;
// 尚未由佇列處理的作業數量...
$batch->pendingJobs;
// 已失敗的作業數量...
$batch->failedJobs;
// 截至目前為止已處理的作業數量...
$batch->processedJobs();
// 批次的完成百分比(0-100)...
$batch->progress();
// 指示批次是否已完成執行...
$batch->finished();
// 取消批次的執行...
$batch->cancel();
// 指示批次是否已被取消...
$batch->cancelled();
從路由返回批次
所有 Illuminate\Bus\Batch 實例都是 JSON 可序列化的,這意味著您可以直接從應用程式的路由返回它們,以檢索包含有關批次資訊(包括其完成進度)的 JSON 負載。這使得在應用程式的 UI 中顯示有關批次完成進度的資訊變得非常方便。
要按 ID 檢索批次,您可以使用 Bus 外觀的 findBatch 方法:
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Route;
Route::get('/batch/{batchId}', function (string $batchId) {
return Bus::findBatch($batchId);
});
取消批次
有時您可能需要取消給定批次的執行。這可以透過在 Illuminate\Bus\Batch 實例上呼叫 cancel 方法來實現:
/**
* 執行作業。
*/
public function handle(): void
{
if ($this->user->exceedsImportLimit()) {
$this->batch()->cancel();
return;
}
if ($this->batch()->cancelled()) {
return;
}
}
正如您可能在前面的範例中注意到的,批次作業通常應在繼續執行之前判斷其相應的批次是否已被取消。但是,為了方便,您可以將 SkipIfBatchCancelled中間件分配給作業。顧名思義,此中間件將指示 Laravel 在相應的批次已被取消時不處理作業:
use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
/**
* 獲取作業應透過的中介層。
*/
public function middleware(): array
{
return [new SkipIfBatchCancelled];
}
批次失敗
當批次作業失敗時,catch 回呼(如果已分配)將被呼叫。此回呼僅在批次中第一個失敗的作業時被呼叫。
允許失敗
當批次中的作業失敗時,Laravel 將自動將批次標記為「已取消」。如果您願意,可以禁用此行為,以便作業失敗不會自動將批次標記為已取消。這可以透過在派發批次時呼叫 allowFailures 方法來實現:
$batch = Bus::batch([
// ...
])->then(function (Batch $batch) {
// 所有作業都成功完成了...
})->allowFailures()->dispatch();
您可以選擇性地向 allowFailures 方法提供一個閉包,該閉包將在每次作業失敗時執行:
$batch = Bus::batch([
// ...
])->allowFailures(function (Batch $batch, $exception) {
// 處理單個作業失敗...
})->dispatch();
重試失敗的批次作業
為了方便,Laravel 提供了一個 queue:retry-batch Artisan 命令,允許您輕鬆重試給定批次的所有失敗作業。此命令接受應重試其失敗作業的批次 UUID:
php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5
修剪批次
如果不進行修剪,job_batches 表可以非常快地累積記錄。為了緩解這種情況,您應該安排 queue:prune-batches Artisan 命令每天運行:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches')->daily();
預設情況下,所有超過 24 小時的已完成批次都將被修剪。您可以在呼叫命令時使用 hours 選項來確定保留批次資料的時間長度。例如,以下命令將刪除所有在 48 小時前完成的批次:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48')->daily();
有時,您的 job_batches 表可能會累積從未成功完成的批次的批次記錄,例如作業失敗且從未成功重試的批次。您可以使用 unfinished 選項指示 queue:prune-batches 命令修剪這些未完成的批次記錄:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();
同樣,您的 job_batches 表也可能累積已取消批次的批次記錄。您可以使用 cancelled 選項指示 queue:prune-batches 命令修剪這些已取消的批次記錄:
use Illuminate\Support\Facades\Schedule;
Schedule::command('queue:prune-batches --hours=48 --cancelled=72')->daily();
在 DynamoDB 中儲存批次
Laravel 還提供支援在 DynamoDB 中儲存批次元資料資訊,而不是關聯式資料庫。但是,您必須手動建立一個 DynamoDB 表來儲存所有批次記錄。
通常,此表應命名為 job_batches,但您應該根據應用程式 queue 配置檔案中 queue.batching.table 配置值的值來命名該表。
DynamoDB 批次表配置
job_batches 表應有一個名為 application 的字串主分割索引鍵和一個名為 id 的字串主排序鍵。鍵的 application 部分將包含您的應用程式名稱,該名稱由應用程式 app 配置檔案中的 name 配置值定義。由於應用程式名稱是 DynamoDB 表鍵的一部分,您可以使用同一個表為多個 Laravel 應用程式儲存作業批次。
此外,如果您希望利用自動批次修剪,可以為您的表定義 ttl 屬性。
DynamoDB 配置
接下來,安裝 AWS SDK,以便您的 Laravel 應用程式可以與 Amazon DynamoDB 通訊:
composer require aws/aws-sdk-php
然後,將 queue.batching.driver 配置選項的值設定為 dynamodb。此外,您應該在 batching 配置陣列中定義 key、secret 和 region 配置選項。這些選項將用於與 AWS 認證。使用 dynamodb 驅動程式時,queue.batching.database 配置選項是不必要的:
'batching' => [
'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
],
在 DynamoDB 中修剪批次
使用 DynamoDB 儲存作業批次資訊時,用於修剪儲存在關聯式資料庫中的批次的典型修剪命令將不起作用。相反,您可以利用 DynamoDB 的原生 TTL 功能來自動移除舊批次的記錄。
如果您使用 ttl 屬性定義了 DynamoDB 表,則可以定義配置參數來指示 Laravel 如何修剪批次記錄。queue.batching.ttl_attribute 配置值定義了持有 TTL 的屬性的名稱,而 queue.batching.ttl 配置值定義了從 DynamoDB 表中移除批次記錄的秒數,相對於記錄上次更新的時間:
'batching' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'job_batches',
'ttl_attribute' => 'ttl',
'ttl' => 60 * 60 * 24 * 7, // 7 天...
],
佇列閉包
除了將作業類別派發到佇列外,您還可以派發一個閉包。這對於需要在當前請求週期之外執行的快速、簡單任務非常有用。將閉包派發到佇列時,閉包的代碼內容會被加密簽署,以便在傳輸過程中無法被修改:
use App\Models\Podcast;
$podcast = Podcast::find(1);
dispatch(function () use ($podcast) {
$podcast->publish();
});
要為佇列閉包分配一個名稱(可用於佇列報告儀表板以及 queue:work 命令顯示),您可以使用 name 方法:
dispatch(function () {
// ...
})->name('Publish Podcast');
使用 catch 方法,您可以提供一個在佇列閉包在耗盡您佇列的所有配置重試嘗試後未能成功完成時應執行的閉包:
use Throwable;
dispatch(function () use ($podcast) {
$podcast->publish();
})->catch(function (Throwable $e) {
// 此作業已失敗...
});
[!WARNING] 由於
catch回呼是由 Laravel 佇列序列化並在稍後執行的,因此不應在catch回呼中使用$this變數。
運行佇列工作者
queue:work 命令
Laravel 包含一個 Artisan 命令,該命令將啟動一個佇列工作者並在作業被推送到佇列時處理新的作業。您可以使用 queue:work Artisan 命令運行工作者。請注意,一旦 queue:work 命令開始,它將繼續執行直到手動停止或您關閉終端機:
php artisan queue:work
[!NOTE] 要讓
queue:work程序永久在背景中運行,您應該使用進程監控器(如 Supervisor)來確保佇列工作者不會停止運行。
如果您希望處理的作業 ID、連接名稱和佇列名稱包含在命令的輸出中,可以在呼叫 queue:work 命令時包含 -v 標誌:
php artisan queue:work -v
請記住,佇列工作者是長期運行的程序,並將已啟動的應用程式狀態存儲在記憶體中。因此,它們在啟動後不會注意到代碼庫中的更改。因此,在部署過程中,請務必重新啟動您的佇列工作者。此外,請記住,應用程式建立或修改的任何靜態狀態都不會在作業之間自動重設。
或者,您可以運行 queue:listen 命令。使用 queue:listen 命令時,當您想要重新載入更新的代碼或重設應用程式狀態時,不必手動重新啟動工作者;但是,此命令比 queue:work 命令效率低得多:
php artisan queue:listen
運行多個佇列工作者
要將多個工作者分配給一個佇列並並行處理作業,您應該簡單地啟動多個 queue:work 程序。這可以在本地透過終端機中的多個標籤頁完成,也可以在生產中使用進程管理器的配置設定完成。使用 Supervisor 時,您可以使用 numprocs 配置值。
指定連接和佇列
您還可以指定工作者應使用哪個佇列連接。傳遞給 work 命令的連接名稱應對應於 config/queue.php 配置檔案中定義的連接之一:
php artisan queue:work redis
預設情況下,queue:work 命令只處理給定連接上預設佇列的作業。但是,您可以透過只處理給定連接的特定佇列來進一步自訂佇列工作者。例如,如果您所有的電子郵件都在 redis 佇列連接上的 emails 佇列中處理,您可以發出以下命令來啟動只處理該佇列的工作者:
php artisan queue:work redis --queue=emails
處理指定數量的作業
--once 選項可用於指示工作者只從佇列中處理單個作業:
php artisan queue:work --once
--max-jobs 選項可用於指示工作者處理給定數量的作業然後退出。當與 Supervisor 結合使用時,此選項可能很有用,以便在處理給定數量的作業後自動重新啟動工作者,釋放它們可能累積的任何記憶體:
php artisan queue:work --max-jobs=1000
處理所有佇列作業然後退出
--stop-when-empty 選項可用於指示工作者處理所有作業然後優雅地退出。如果您希望在佇列為空後關閉 Docker 容器,則在 Docker 容器中處理 Laravel 佇列時,此選項可能很有用:
php artisan queue:work --stop-when-empty
在給定秒數內處理作業
--max-time 選項可用於指示工作者在給定秒數內處理作業然後退出。當與 Supervisor 結合使用時,此選項可能很有用,以便在處理作業給定時間後自動重新啟動工作者,釋放它們可能累積的任何記憶體:
# 處理作業一個小時然後退出...
php artisan queue:work --max-time=3600
工作者睡眠時間
當佇列上有作業可用時,工作者將在作業之間無延遲地持續處理作業。但是,sleep 選項決定如果沒有作業可用,工作者將「睡眠」多少秒。當然,在睡眠期間,工作者不會處理任何新作業:
php artisan queue:work --sleep=3
維護模式和佇列
當您的應用程式處於維護模式時,不會處理任何佇列作業。一旦應用程式退出維護模式,作業將正常繼續處理。
要強制您的佇列工作者即使在啟用維護模式時也處理作業,您可以使用 --force 選項:
php artisan queue:work --force
資源考量
守護程序佇列工作者在處理每個作業之前不會「重新啟動」框架。因此,您應該在每個作業完成後釋放任何繁重的資源。例如,如果您正在使用 GD 庫進行圖像操作,則在完成處理圖像後應使用 imagedestroy 釋放記憶體。
佇列優先級
有時您可能希望優先處理您的佇列的處理方式。例如,在您的 config/queue.php 配置檔案中,您可以將 redis 連接的預設 queue 設定為 low。但是,偶爾您可能希望將作業推送到 high 優先級佇列,像這樣:
dispatch((new Job)->onQueue('high'));
要啟動一個工作者來確保所有 high 佇列作業在繼續處理 low 佇列上的任何作業之前被處理,請將以逗號分隔的佇列名稱清單傳遞給 work 命令:
php artisan queue:work --queue=high,low
佇列工作者和部署
由於佇列工作者是長期運行的程序,它們在不重新啟動的情況下不會注意到代碼的更改。因此,使用佇列工作者部署應用程式的最簡單方法是在部署過程中重新啟動工作者。您可以透過發出 queue:restart 命令來優雅地重新啟動所有工作者:
php artisan queue:restart
此命令將指示所有佇列工作者在完成處理當前作業後優雅地退出,以便不會丟失現有作業。由於佇列工作者在執行 queue:restart 命令時將退出,因此您應該運行進程管理器(如 Supervisor)來自動重新啟動佇列工作者。
[!NOTE] 佇列使用快取來儲存重新啟動訊號,因此在使用此功能之前,您應該驗證是否為應用程式正確配置了快取驅動程式。
回應工作者訊號
當佇列工作者在處理作業時收到終止訊號(如 SIGQUIT、SIGTERM 或 SIGINT)時,工作者將在退出之前完成其當前作業。但是,您的作業可能需要在您的伺服器或容器協調器停止程序之前對訊號做出回應。例如,長時間運行的匯入作業可能需要停止拉取新記錄並保存其當前進度。
要從作業內行回應工作者訊號,請實現 Illuminate\Contracts\Queue\Interruptible 介面並在作業上定義一個 interrupted 方法。工作者收到的訊號編號將被傳遞給 interrupted 方法:
<?php
namespace App\Jobs;
use App\Models\Import;
use Illuminate\Contracts\Queue\Interruptible;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
class ImportProducts implements ShouldQueue, Interruptible
{
use Queueable;
protected bool $shouldStop = false;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public Import $import,
) {}
/**
* 執行作業。
*/
public function handle(): void
{
foreach ($this->import->pendingRows() as $row) {
if ($this->shouldStop) {
break;
}
// 匯入產品行...
}
$this->import->saveProgress();
}
/**
* 處理佇列工作者收到的訊號。
*/
public function interrupted(int $signal): void
{
$this->shouldStop = true;
}
}
interrupted 方法僅在工作者在作業當前運行時收到程序訊號時被呼叫。它不是逾時或作業的 failed 方法的替代品。
作業過期和逾時
作業過期
在您的 config/queue.php 配置檔案中,每個佇列連接都定義了一個 retry_after 選項。此選項指定佇列連接在重試正在處理的作業之前應等待多少秒。例如,如果 retry_after 的值設定為 90,則如果作業已經處理了 90 秒而未被釋放或刪除,則它將被釋放回佇列。通常,您應該將 retry_after 值設定為您的作業合理完成處理所需的最大秒數。
[!WARNING] 唯一不包含
retry_after值的佇列連接是 Amazon SQS。SQS 將根據預設可見性逾時重試作業,該逾時在 AWS 控制台中管理。
工作者逾時
queue:work Artisan 命令公開了一個 --timeout 選項。預設情況下,--timeout 值為 60 秒。如果作業處理的時間超過逾時值指定的秒數,則處理該作業的工作者將以錯誤退出。通常,工作者將由在伺服器上配置的進程管理器自動重新啟動:
php artisan queue:work --timeout=60
retry_after 配置選項和 --timeout CLI 選項是不同的,但它們協同工作以確保作業不會丟失,並且作業只會被成功處理一次。
[!WARNING]
--timeout值應始終比您的retry_after配置值至少短幾秒。這將確保處理凍結作業的工作者始終在作業被重試之前被終止。如果您的--timeout選項比您的retry_after配置值長,則您的作業可能會被處理兩次。
暫停和恢復佇列工作者
有時您可能需要暫時阻止佇列工作者處理新作業,而不完全停止工作者。例如,您可能希望在系統維護期間暫停作業處理。Laravel 提供了 queue:pause 和 queue:continue Artisan 命令來暫停和恢復佇列工作者。
要暫停特定的佇列,請提供佇列連接名稱和佇列名稱:
php artisan queue:pause database:default
在此範例中,database 是佇列連接名稱,default 是佇列名稱。一旦佇列被暫停,處理該佇列作業的任何工作者將繼續完成其當前作業,但直到佇列恢復之前不會拾取任何新作業。
要在暫停的佇列上繼續處理作業,請使用 queue:continue 命令:
php artisan queue:continue database:default
恢復佇列後,工作者將立即開始處理該佇列的新作業。請注意,暫停佇列不會停止工作者程序本身 - 它只會阻止工作者處理指定佇列的新作業。
工作者重新啟動和暫停訊號
預設情況下,佇列工作者在每次作業迭代時都會輪詢快取驅動程式以獲取重新啟動和暫停訊號。雖然這種輪詢對於回應 queue:restart 和 queue:pause 命令是必要的,但它確實會引入少量的效能開銷。
如果您需要最佳化效能並且不需要這些中斷功能,可以透過在 Queue 外觀上呼叫 withoutInterruptionPolling 方法來全域禁用此輪詢。這通常應該在 AppServiceProvider 的 boot 方法中完成:
use Illuminate\Support\Facades\Queue;
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Queue::withoutInterruptionPolling();
}
或者,您可以透過在 Illuminate\Queue\Worker 類別上設定靜態 $restartable 或 $pausable 屬性來單獨禁用重新啟動或暫停輪詢:
use Illuminate\Queue\Worker;
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Worker::$restartable = false;
Worker::$pausable = false;
}
[!WARNING] 當禁用中斷輪詢時,工作者將不會回應
queue:restart或queue:pause命令(取決於哪些功能被禁用)。
Supervisor 配置
在生產環境中,您需要一種方法來保持 queue:work 程序運行。一個 queue:work 程序可能因為各種原因停止運行,例如工作者逾時或執行 queue:restart 命令。
為此,您需要配置一個進程監控器來偵測您的 queue:work 程序何時退出並自動重新啟動它們。此外,進程監控器允許您指定您希望同時運行多少個 queue:work 程序。Supervisor 是 Linux 環境中常用的進程監控器,我們將在以下文檔中討論如何配置它。
安裝 Supervisor
Supervisor 是一個用於 Linux 作業系統的進程監控器,如果您的 queue:work 程序失敗,它將自動重新啟動它們。要在 Ubuntu 上安裝 Supervisor,您可以使用以下命令:
sudo apt-get install supervisor
[!NOTE] 如果自行配置和管理 Supervisor 聽起來很壓倒性,請考慮使用 Laravel Cloud,它提供了一個完全託管的平台來運行 Laravel 佇列工作者。
配置 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 --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=forge
numprocs=8
redirect_stderr=true
stdout_logfile=/home/forge/app.com/worker.log
stopwaitsecs=3600
在此範例中,numprocs 指令將指示 Supervisor 運行八個 queue:work 程序並監控它們,如果它們失敗則自動重新啟動它們。您應該更改配置的 command 指令以反映您所需的佇列連接和工作者選項。
[!WARNING] 您應該確保
stopwaitsecs的值大於您最長運行作業消耗的秒數。否則,Supervisor 可能會在作業完成處理之前將其終止。
啟動 Supervisor
建立配置檔案後,您可以使用以下命令更新 Supervisor 配置並啟動程序:
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start "laravel-worker:*"
有關 Supervisor 的更多資訊,請查閱 Supervisor 文檔。
處理失敗作業
有時您的佇列作業可能會失敗。別擔心,事情並不總是按計劃進行!Laravel 提供了一種便利的方法來指定作業應被嘗試的最大次數。非同步作業超過此嘗試次數後,將被插入 failed_jobs 資料庫表中。失敗的同步派發作業不會存儲在此表中,其異常會立即被應用程式處理。
新 Laravel 應用程式中通常已經存在一個用於建立 failed_jobs 表的遷移。但是,如果您的應用程式不包含此表的遷移,您可以使用 make:queue-failed-table 命令來建立遷移:
php artisan make:queue-failed-table
php artisan migrate
運行佇列工作者程序時,您可以使用 queue:work 命令上的 --tries 切換來指定作業應被嘗試的最大次數。如果未指定 --tries 選項的值,則作業只會被嘗試一次或如作業類別的 Tries 屬性所指定的那樣多次:
php artisan queue:work redis --tries=3
使用 --backoff 選項,您可以指定 Laravel 在重試遇到異常的作業之前應等待多少秒。預設情況下,作業會立即被釋放回佇列,以便可以再次嘗試:
php artisan queue:work redis --tries=3 --backoff=3
如果您希望配置 Laravel 在重試遇到異常的作業之前應等待多少秒(按作業基礎),可以在作業類別上使用 Backoff 屬性:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff(3)]
class ProcessPodcast implements ShouldQueue
{
// ...
}
如果您需要更複雜的邏輯來確定作業的退避時間,可以在作業類別上定義一個 backoff 方法:
/**
* 計算在重試作業之前應等待的秒數。
*/
public function backoff(): int
{
return 3;
}
您可以透過定義一個退避值陣列來輕鬆配置「指數」退避。在此範例中,重試延遲將為第一次重試 1 秒,第二次重試 5 秒,第三次重試 10 秒,如果還有更多剩餘嘗試,則每次後續重試 10 秒:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\Backoff;
#[Backoff([1, 5, 10])]
class ProcessPodcast implements ShouldQueue
{
// ...
}
在失敗作業後清理
當特定作業失敗時,您可能希望向使用者發送警報或還原作業部分完成的任何操作。為此,您可以在作業類別上定義一個 failed 方法。導致作業失敗的 Throwable 實例將被傳遞給 failed 方法:
<?php
namespace App\Jobs;
use App\Models\Podcast;
use App\Services\AudioProcessor;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;
use Throwable;
class ProcessPodcast implements ShouldQueue
{
use Queueable;
/**
* 建立一個新的作業實例。
*/
public function __construct(
public Podcast $podcast,
) {}
/**
* 執行作業。
*/
public function handle(AudioProcessor $processor): void
{
// 處理上傳的播客...
}
/**
* 處理作業失敗。
*/
public function failed(?Throwable $exception): void
{
// 向使用者發送失敗通知等...
}
}
[!WARNING] 在呼叫
failed方法之前會實例化作業的新實例;因此,在handle方法內行發生的任何類別屬性修改都將丟失。
失敗的作業不一定遇到未處理異常。當作業耗盡其所有允許的嘗試次數時,它也可能被視為失敗。這些嘗試可以透過多種方式被消耗:
- 作業逾時了。
- 作業在執行期間遇到未處理的異常。
- 作業被手動或由中介層釋放回佇列。
如果最後一次嘗試因作業執行期間拋出的異常而失敗,則該異常將被傳遞給作業的 failed 方法。但是,如果作業因達到最大允許嘗試次數而失敗,則 $exception 將是 Illuminate\Queue\MaxAttemptsExceededException 的實例。同樣,如果作業因超過配置的逾時而失敗,則 $exception 將是 Illuminate\Queue\TimeoutExceededException 的實例。
重試失敗作業
要查看已插入 failed_jobs 資料庫表中的所有失敗作業,您可以使用 queue:failed Artisan 命令:
php artisan queue:failed
queue:failed 命令將列出作業 ID、連接、佇列、失敗時間以及有關作業的其他資訊。作業 ID 可用於重試失敗的作業。例如,要重試 ID 為 ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 的失敗作業,請發出以下命令:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece
如果需要,您可以向命令傳遞多個 ID:
php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d
您還可以重試特定佇列的所有失敗作業:
php artisan queue:retry --queue=name
要重試所有失敗作業,請執行 queue:retry 命令並傳遞 all 作為 ID:
php artisan queue:retry all
如果您希望刪除失敗作業,可以使用 queue:forget 命令:
php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d
[!NOTE] 使用 Horizon 時,您應該使用
horizon:forget命令而不是queue:forget命令來刪除失敗作業。
要從 failed_jobs 表中刪除所有失敗作業,您可以使用 queue:flush 命令:
php artisan queue:flush
queue:flush 命令會從您的佇列中移除所有失敗作業記錄,無論失敗作業有多老舊。您可以使用 --hours 選項只刪除在特定小時前或更早失敗的作業:
php artisan queue:flush --hours=48
忽略缺失模型
將 Eloquent 模型注入作業時,模型會在被放入佇列之前自動序列化,並在作業被處理時從資料庫中重新檢索。但是,如果模型在作業等待被工作者處理時已被刪除,則您的作業可能會因 ModelNotFoundException 而失敗。
為了方便,您可以選擇使用作業類別上的 DeleteWhenMissingModels 屬性自動刪除具有缺失模型的作業。存在此屬性時,Laravel 將在不拋出異常的情況下靜默丟棄作業:
<?php
namespace App\Jobs;
use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
#[DeleteWhenMissingModels]
class ProcessPodcast implements ShouldQueue
{
// ...
}
修剪失敗作業
您可以透過呼叫 queue:prune-failed Artisan 命令來修剪應用程式 failed_jobs 表中的記錄:
php artisan queue:prune-failed
預設情況下,所有超過 24 小時的失敗作業記錄都將被修剪。如果您向命令提供 --hours 選項,則僅保留最後 N 小時內插入的失敗作業記錄。例如,以下命令將刪除在 48 小時前插入的所有失敗作業記錄:
php artisan queue:prune-failed --hours=48
在 DynamoDB 中儲存失敗作業
Laravel 還提供在 DynamoDB 中而不是關聯式資料庫表中儲存失敗作業記錄的支援。但是,您必須手動建立 DynamoDB 表來儲存所有失敗作業記錄。通常,此表應命名為 failed_jobs,但您應該根據應用程式 queue 配置檔案中 queue.failed.table 配置值的值來命名該表。
failed_jobs 表應有一個名為 application 的字串主分割索引鍵和一個名為 uuid 的字串主排序鍵。鍵的 application 部分將包含您的應用程式名稱,該名稱由應用程式 app 配置檔案中的 name 配置值定義。由於應用程式名稱是 DynamoDB 表鍵的一部分,您可以使用同一個表為多個 Laravel 應用程式儲存失敗作業。
此外,請確保安裝 AWS SDK,以便您的 Laravel 應用程式可以與 Amazon DynamoDB 通訊:
composer require aws/aws-sdk-php
接下來,將 queue.failed.driver 配置選項的值設定為 dynamodb。此外,您應該在失敗作業配置陣列中定義 key、secret 和 region 配置選項。這些選項將用於與 AWS 認證。使用 dynamodb 驅動程式時,queue.failed.database 配置選項是不必要的:
'failed' => [
'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
'key' => env('AWS_ACCESS_KEY_ID'),
'secret' => env('AWS_SECRET_ACCESS_KEY'),
'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
'table' => 'failed_jobs',
],
禁用失敗作業儲存
您可以透過將 queue.failed.driver 配置選項的值設定為 null 來指示 Laravel 丟棄失敗作業而不儲存它們。通常,可以透過 QUEUE_FAILED_DRIVER 環境變數來完成此操作:
QUEUE_FAILED_DRIVER=null
失敗作業事件
如果您希望註冊一個在作業失敗時將被呼叫的事件監聽器,可以使用 Queue 外觀的 failing 方法。例如,我們可以從 Laravel 附帶的 AppServiceProvider 的 boot 方法中將一個閉包附加到此事件:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobFailed;
class AppServiceProvider extends ServiceProvider
{
/**
* 註冊任何應用程式服務。
*/
public function register(): void
{
// ...
}
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Queue::failing(function (JobFailed $event) {
// $event->connectionName
// $event->job
// $event->exception
});
}
}
從佇列中清除作業
[!NOTE] 使用 Horizon 時,您應該使用
horizon:clear命令而不是queue:clear命令從佇列中清除作業。
如果您希望從預設連接的預設佇列中刪除所有作業,可以使用 queue:clear Artisan 命令:
php artisan queue:clear
您還可以提供 connection 參數和 queue 選項來從特定連接和佇列中刪除作業:
php artisan queue:clear redis --queue=emails
[!WARNING] 從佇列中清除作業僅適用於 SQS、Redis 和資料庫佇列驅動程式。此外,SQS 訊息刪除過程需要最多 60 秒,因此在清除佇列後最多 60 秒內發送到 SQS 佇列的作業也可能被刪除。
監控您的佇列
如果您的佇列突然湧入大量作業,它可能會不堪重負,導致作業完成等待時間過長。如果您願意,Laravel 可以在您的佇列作業數量超過指定閾值時向您發出警報。
首先,您應該安排 queue:monitor 命令每分鐘運行一次。該命令接受您希望監控的佇列名稱以及您所需的作業數量閾值:
php artisan queue:monitor redis:default,redis:deployments --max=100
僅安排此命令不足以觸發通知以警告您佇列不堪重負的狀態。當命令遇到作業數量超過您閾值的佇列時,將派發 Illuminate\Queue\Events\QueueBusy 事件。您可以從應用程式的 AppServiceProvider 中監聽此事件以向您或您的開發團隊發送通知:
use App\Notifications\QueueHasLongWaitTime;
use Illuminate\Queue\Events\QueueBusy;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\Facades\Notification;
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Event::listen(function (QueueBusy $event) {
Notification::route('mail', 'dev@example.com')
->notify(new QueueHasLongWaitTime(
$event->connectionName,
$event->queue,
$event->size
));
});
}
測試
測試派發作業的代碼時,您可能希望指示 Laravel 不要實際執行作業本身,因為可以單獨且獨立於派發作業的代碼來測試作業的代碼。當然,要測試作業本身,您可以實例化一個作業實例並在測試中直接呼叫 handle 方法。
您可以使用 Queue 外觀的 fake 方法來阻止佇列作業實際被推送到佇列。呼叫 Queue 外觀的 fake 方法後,您可以斷言應用程式嘗試將作業推送到佇列:
<?php
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
test('orders can be shipped', function () {
Queue::fake();
// 執行訂單發貨...
// 斷言沒有推送作業...
Queue::assertNothingPushed();
// 斷言作業已被推送到給定的佇列...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// 斷言作業已被推送
Queue::assertPushed(ShipOrder::class);
// 斷言作業恰好被推送一次...
Queue::assertPushedOnce(ShipOrder::class);
// 斷言作業已被推送兩次...
Queue::assertPushedTimes(ShipOrder::class, 2);
// 斷言作業未被推送...
Queue::assertNotPushed(AnotherJob::class);
// 斷言閉包已被推送到佇列...
Queue::assertClosurePushed();
// 斷言閉包未被推送...
Queue::assertClosureNotPushed();
// 斷言被推送的作業總數...
Queue::assertCount(3);
});
<?php
namespace Tests\Feature;
use App\Jobs\AnotherJob;
use App\Jobs\ShipOrder;
use Illuminate\Support\Facades\Queue;
use Tests\TestCase;
class ExampleTest extends TestCase
{
public function test_orders_can_be_shipped(): void
{
Queue::fake();
// 執行訂單發貨...
// 斷言沒有推送作業...
Queue::assertNothingPushed();
// 斷言作業已被推送到給定的佇列...
Queue::assertPushedOn('queue-name', ShipOrder::class);
// 斷言作業已被推送
Queue::assertPushed(ShipOrder::class);
// 斷言作業恰好被推送一次...
Queue::assertPushedOnce(ShipOrder::class);
// 斷言作業已被推送兩次...
Queue::assertPushedTimes(ShipOrder::class, 2);
// 斷言作業未被推送...
Queue::assertNotPushed(AnotherJob::class);
// 斷言閉包已被推送到佇列...
Queue::assertClosurePushed();
// 斷言閉包未被推送...
Queue::assertClosureNotPushed();
// 斷言被推送的作業總數...
Queue::assertCount(3);
}
}
您可以向 assertPushed、assertNotPushed、assertClosurePushed 或 assertClosureNotPushed 方法傳遞一個閉包,以斷言派發了一個通過給定「真理測試」的作業。如果至少派發了一個通過給定真理測試的作業,則斷言將成功:
use Illuminate\Queue\CallQueuedClosure;
Queue::assertPushed(function (ShipOrder $job) use ($order) {
return $job->order->id === $order->id;
});
Queue::assertClosurePushed(function (CallQueuedClosure $job) {
return $job->name === 'validate-order';
});
偽造部分作業
如果您只需要偽造特定的作業,同時允許您的其他作業正常執行,可以將應被偽造的作業類別名稱傳遞給 fake 方法:
test('orders can be shipped', function () {
Queue::fake([
ShipOrder::class,
]);
// 執行訂單發貨...
// 斷言作業已被推送兩次...
Queue::assertPushedTimes(ShipOrder::class, 2);
});
public function test_orders_can_be_shipped(): void
{
Queue::fake([
ShipOrder::class,
]);
// 執行訂單發貨...
// 斷言作業已被推送兩次...
Queue::assertPushedTimes(ShipOrder::class, 2);
}
您可以使用 except 方法偽造除指定作業集之外的所有作業:
Queue::fake()->except([
ShipOrder::class,
]);
測試作業鏈
要測試作業鏈,您將需要利用 Bus 外觀的偽造功能。Bus 外觀的 assertChained 方法可用於斷言派發了一鏈作業。assertChained 方法接受一個鏈式作業陣列作為其第一個參數:
use App\Jobs\RecordShipment;
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertChained([
ShipOrder::class,
RecordShipment::class,
UpdateInventory::class
]);
正如您在上面的範例中所見,鏈式作業陣列可以是作業類別名稱的陣列。但是,您還可以提供實際作業實例的陣列。這樣做時,Laravel 將確保作業實例是相同的類別,並且與您的應用程式派發的鏈式作業具有相同的屬性值:
Bus::assertChained([
new ShipOrder,
new RecordShipment,
new UpdateInventory,
]);
您可以使用 assertDispatchedWithoutChain 方法來斷言派發了一個沒有作業鏈的作業:
Bus::assertDispatchedWithoutChain(ShipOrder::class);
測試鏈修改
如果鏈式作業前置或附加作業到現有鏈,您可以使用作業的 assertHasChain 方法來斷言該作業具有預期的剩餘作業鏈:
$job = new ProcessPodcast;
$job->handle();
$job->assertHasChain([
new TranscribePodcast,
new OptimizePodcast,
new ReleasePodcast,
]);
assertDoesntHaveChain 方法可用於斷言作業的剩餘鏈為空:
$job->assertDoesntHaveChain();
測試鏈式批次
如果您的作業鏈包含一批作業,您可以透過在鏈斷言中插入 Bus::chainedBatch 定義來斷言鏈式批次符合您的期望:
use App\Jobs\ShipOrder;
use App\Jobs\UpdateInventory;
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::assertChained([
new ShipOrder,
Bus::chainedBatch(function (PendingBatch $batch) {
return $batch->jobs->count() === 3;
}),
new UpdateInventory,
]);
測試作業批次
Bus 外觀的 assertBatched 方法可用於斷言派發了一批作業。傳遞給 assertBatched 方法的閉包接收一個 Illuminate\Bus\PendingBatch 的實例,可用於檢查批次中的作業:
use Illuminate\Bus\PendingBatch;
use Illuminate\Support\Facades\Bus;
Bus::fake();
// ...
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->name == 'Import CSV' &&
$batch->jobs->count() === 10;
});
hasJobs 方法可用於待處理批次來驗證批次包含預期的作業。該方法接受一個作業實例、類別名稱或閉包的陣列:
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
new ProcessCsvRow(row: 1),
new ProcessCsvRow(row: 2),
new ProcessCsvRow(row: 3),
]);
});
使用閉包時,閉包將接收作業實例。預期的作業類型將從閉包的類型提示中推斷:
Bus::assertBatched(function (PendingBatch $batch) {
return $batch->hasJobs([
fn (ProcessCsvRow $job) => $job->row === 1,
fn (ProcessCsvRow $job) => $job->row === 2,
fn (ProcessCsvRow $job) => $job->row === 3,
]);
});
您可以使用 assertBatchCount 方法來斷言派發了給定數量的批次:
Bus::assertBatchCount(3);
您可以使用 assertNothingBatched 來斷言沒有派發任何批次:
Bus::assertNothingBatched();
測試作業/批次互動
此外,您可能偶爾需要測試單個作業與其底層批次的互動。例如,您可能需要測試作業是否取消了其批次的進一步處理。為此,您需要透過 withFakeBatch 方法為作業分配一個虛擬批次。withFakeBatch 方法返回一個包含作業實例和虛擬批次的元組:
[$job, $batch] = (new ShipOrder)->withFakeBatch();
$job->handle();
$this->assertTrue($batch->cancelled());
$this->assertEmpty($batch->added);
測試作業/佇列互動
有時,您可能需要測試佇列作業將自身釋放回佇列。或者,您可能需要測試作業是否刪除了自身。您可以透過實例化作業並呼叫 withFakeQueueInteractions 方法來測試這些佇列互動。
作業的佇列互動被偽造後,您可以呼叫作業上的 handle 方法。呼叫作業後,可以使用各種斷言方法來驗證作業的佇列互動:
use App\Exceptions\CorruptedAudioException;
use App\Jobs\ProcessPodcast;
$job = (new ProcessPodcast)->withFakeQueueInteractions();
$job->handle();
$job->assertReleased(delay: 30);
$job->assertDeleted();
$job->assertNotDeleted();
$job->assertFailed();
$job->assertFailedWith(CorruptedAudioException::class);
$job->assertNotFailed();
作業事件
使用 Queue外觀上的 before 和 after 方法,您可以指定在佇列作業被處理之前或之後執行的回呼。這些回呼是執行額外日誌記錄或為儀表板增加統計資料的好機會。通常,您應該從服務提供者的 boot 方法中呼叫這些方法。例如,我們可以使用 Laravel 附帶的 AppServiceProvider:
<?php
namespace App\Providers;
use Illuminate\Support\Facades\Queue;
use Illuminate\Support\ServiceProvider;
use Illuminate\Queue\Events\JobProcessed;
use Illuminate\Queue\Events\JobProcessing;
class AppServiceProvider extends ServiceProvider
{
/**
* 註冊任何應用程式服務。
*/
public function register(): void
{
// ...
}
/**
* 啟動任何應用程式服務。
*/
public function boot(): void
{
Queue::before(function (JobProcessing $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
Queue::after(function (JobProcessed $event) {
// $event->connectionName
// $event->job
// $event->job->payload()
});
}
}
使用 Queue外觀上的 looping 方法,您可以指定在工作者嘗試從佇列中獲取作業之前執行的回呼。例如,您可以註冊一個閉包來回滾先前失敗的作業留下來的任何打開的交易:
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Queue;
Queue::looping(function () {
while (DB::transactionLevel() > 0) {
DB::rollBack();
}
});
當佇列工作者無法從佇列中檢索作業時,Laravel 還會派發一個 Illuminate\Queue\Events\WorkerIdle 事件:
use Illuminate\Queue\Events\WorkerIdle;
use Illuminate\Support\Facades\Event;
Event::listen(function (WorkerIdle $event) {
// $event->connectionName
// $event->queue
// $event->workerOptions
});