跳轉至內容

佇列

簡介

在構建 Web 應用程式時,您可能有一些任務(例如解析和儲存上傳的 CSV 檔案)在典型的 Web 請求期間執行耗時過長。幸運的是,Laravel 允許您輕鬆建立可在後臺處理的佇列任務。透過將耗時任務轉移到佇列中,您的應用程式可以以極快的速度響應 Web 請求,併為客戶提供更好的使用者體驗。

Laravel 佇列為各種不同的佇列後端提供統一的佇列 API,例如 Amazon SQSRedis,甚至是關係型資料庫。

Laravel 的佇列配置選項儲存在應用程式的 config/queue.php 配置檔案中。在此檔案中,您將找到框架自帶的每個佇列驅動程式的連線配置,包括 database、Amazon SQSRedisBeanstalkd 驅動程式,以及一個同步驅動程式(用於開發或測試,可立即執行任務)。框架還包含一個 null 佇列驅動程式,用於丟棄已排隊的任務。

Laravel Horizon 是一個美觀的儀表盤和配置系統,專為 Redis 驅動的佇列設計。有關更多資訊,請檢視完整的 Horizon 文件

連線與佇列

在開始使用 Laravel 佇列之前,理解“連線”和“佇列”之間的區別非常重要。在 config/queue.php 配置檔案中,有一個 connections 配置陣列。此選項定義了到 Amazon SQS、Beanstalk 或 Redis 等後端佇列服務的連線。然而,任何給定的佇列連線可能擁有多個“佇列”,這些佇列可以被視為佇列任務的不同堆疊或堆疊。

請注意,queue 配置檔案中的每個連線配置示例都包含一個 queue 屬性。這是將任務傳送到給定連線時預設分發到的佇列。換句話說,如果您在沒有明確定義任務應分發到哪個佇列的情況下分發任務,該任務將被放入連線配置的 queue 屬性中定義的佇列。

1use App\Jobs\ProcessPodcast;
2 
3// This job is sent to the default connection's default queue...
4ProcessPodcast::dispatch();
5 
6// This job is sent to the default connection's "emails" queue...
7ProcessPodcast::dispatch()->onQueue('emails');

有些應用程式可能根本不需要將任務推送到多個佇列,而是傾向於使用一個簡單的佇列。但是,對於希望優先處理或分段處理任務的應用程式來說,將任務推送到多個佇列特別有用,因為 Laravel 佇列工作程序允許您透過優先順序指定應處理哪些佇列。例如,如果您將任務推送到 high 佇列,則可以執行一個給予其更高處理優先順序的工作程序。

1php artisan queue:work --queue=high,default

驅動程式注意事項和先決條件

資料庫

為了使用 database 佇列驅動程式,您需要一個數據庫表來存放任務。通常,這包含在 Laravel 預設的 0001_01_01_000002_create_jobs_table.php 資料庫遷移中;但是,如果您的應用程式不包含此遷移,可以使用 make:queue-table Artisan 命令來建立它。

1php artisan make:queue-table
2 
3php artisan migrate

Redis

為了使用 redis 佇列驅動程式,您需要在 config/database.php 配置檔案中配置 Redis 資料庫連線。

redis 佇列驅動程式不支援 serializercompression Redis 選項。

Redis 叢集

如果您的 Redis 佇列連線使用了 Redis 叢集,則您的佇列名稱必須包含 鍵雜湊標籤 (key hash tag)。這是確保給定佇列的所有 Redis 鍵都被放置在同一個雜湊槽中所需的。

1'redis' => [
2 'driver' => 'redis',
3 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
4 'queue' => env('REDIS_QUEUE', '{default}'),
5 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
6 'block_for' => null,
7 'after_commit' => false,
8],
阻塞

使用 Redis 佇列時,您可以使用 block_for 配置選項來指定驅動程式在迴圈工作並重新輪詢 Redis 資料庫之前應等待任務可用的時間。

根據您的佇列負載調整此值可能比持續輪詢 Redis 資料庫獲取新任務更有效。例如,您可以將該值設定為 5,表示驅動程式在等待任務可用時應阻塞五秒。

1'redis' => [
2 'driver' => 'redis',
3 'connection' => env('REDIS_QUEUE_CONNECTION', 'default'),
4 'queue' => env('REDIS_QUEUE', 'default'),
5 'retry_after' => env('REDIS_QUEUE_RETRY_AFTER', 90),
6 'block_for' => 5,
7 'after_commit' => false,
8],

block_for 設定為 0 會導致佇列工作程序無限期阻塞,直到任務可用。這也會阻止 SIGTERM 等訊號在下個任務處理完成之前得到處理。

其他驅動程式先決條件

所列佇列驅動程式需要以下依賴項。可以透過 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 命令時將會建立它。

1php artisan make:job ProcessPodcast

生成的類將實現 Illuminate\Contracts\Queue\ShouldQueue 介面,向 Laravel 表明該任務應被推送到佇列中以非同步執行。

可以使用 存根釋出 (stub publishing) 來自定義任務存根。

類結構

任務類非常簡單,通常僅包含一個 handle 方法,該方法在佇列處理任務時被呼叫。首先,讓我們看一個任務類的示例。在這個示例中,假設我們管理著一個播客釋出服務,需要在播客釋出前處理上傳的檔案。

1<?php
2 
3namespace App\Jobs;
4 
5use App\Models\Podcast;
6use App\Services\AudioProcessor;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Queue\Queueable;
9 
10class ProcessPodcast implements ShouldQueue
11{
12 use Queueable;
13 
14 /**
15 * Create a new job instance.
16 */
17 public function __construct(
18 public Podcast $podcast,
19 ) {}
20 
21 /**
22 * Execute the job.
23 */
24 public function handle(AudioProcessor $processor): void
25 {
26 // Process uploaded podcast...
27 }
28}

在此示例中,請注意我們能夠直接將 Eloquent 模型傳遞到佇列任務的建構函式中。由於任務使用了 Queueable trait,當任務處理時,Eloquent 模型及其已載入的關係將優雅地序列化和反序列化。

如果您的佇列任務在建構函式中接受 Eloquent 模型,則僅模型的識別符號會被序列化到佇列中。當任務實際處理時,佇列系統會自動從資料庫中重新檢索完整的模型例項及其已載入的關係。這種模型序列化方法允許向佇列驅動程式傳送更小的任務負載。

handle 方法依賴注入

handle 方法在佇列處理任務時被呼叫。請注意,我們可以在任務的 handle 方法上進行型別提示依賴項。Laravel 服務容器會自動注入這些依賴項。

如果您想完全控制容器如何將依賴注入到 handle 方法中,可以使用容器的 bindMethod 方法。bindMethod 方法接受一個接收任務和容器的回撥。在回撥中,您可以隨意以任何方式呼叫 handle 方法。通常,您應該從 App\Providers\AppServiceProvider 服務提供者boot 方法中呼叫此方法。

1use App\Jobs\ProcessPodcast;
2use App\Services\AudioProcessor;
3use Illuminate\Contracts\Foundation\Application;
4 
5$this->app->bindMethod([ProcessPodcast::class, 'handle'], function (ProcessPodcast $job, Application $app) {
6 return $job->handle($app->make(AudioProcessor::class));
7});

二進位制資料(例如原始影像內容)在傳遞給佇列任務之前應透過 base64_encode 函式處理。否則,在放入佇列時任務可能無法正確序列化為 JSON。

佇列關係

由於所有已載入的 Eloquent 模型關係在任務排隊時也會被序列化,序列化的任務字串有時會變得非常大。此外,當任務被反序列化且模型關係從資料庫重新檢索時,它們將被完整地檢索出來。在任務排隊過程中序列化模型之前應用的任何先前的關係約束,在任務反序列化時將不會應用。因此,如果您希望處理給定關係的子集,則應該在佇列任務中重新約束該關係。

或者,為了防止關係被序列化,您可以在設定屬性值時對模型呼叫 withoutRelations 方法。此方法將返回一個沒有載入關係的模型例項。

1/**
2 * Create a new job instance.
3 */
4public function __construct(
5 Podcast $podcast,
6) {
7 $this->podcast = $podcast->withoutRelations();
8}

如果您正在使用 PHP 建構函式屬性提升 並且希望表明 Eloquent 模型不應序列化其關係,則可以使用 WithoutRelations 屬性。

1use Illuminate\Queue\Attributes\WithoutRelations;
2 
3/**
4 * Create a new job instance.
5 */
6public function __construct(
7 #[WithoutRelations]
8 public Podcast $podcast,
9) {}

為了方便起見,如果您希望序列化所有模型而不包含關係,則可以將 WithoutRelations 屬性應用於整個類,而不是將其應用於每個模型。

1<?php
2 
3namespace App\Jobs;
4 
5use App\Models\DistributionPlatform;
6use App\Models\Podcast;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Queue\Queueable;
9use Illuminate\Queue\Attributes\WithoutRelations;
10 
11#[WithoutRelations]
12class ProcessPodcast implements ShouldQueue
13{
14 use Queueable;
15 
16 /**
17 * Create a new job instance.
18 */
19 public function __construct(
20 public Podcast $podcast,
21 public DistributionPlatform $platform,
22 ) {}
23}

如果任務接收的是 Eloquent 模型的集合或陣列,而不是單個模型,則當任務反序列化並執行時,該集合內的模型將不會恢復它們的關係。這是為了防止處理大量模型的任務佔用過多的資源。

唯一任務

唯一任務需要支援 的快取驅動程式。目前,memcachedredisdynamodbdatabasefilearray 快取驅動程式均支援原子鎖。

唯一任務約束不適用於批處理中的任務。

有時,您可能希望確保在任何時間點佇列中只有一個特定任務的例項。您可以透過在任務類上實現 ShouldBeUnique 介面來做到這一點。此介面不需要您在類上定義任何額外的方法。

1<?php
2 
3use Illuminate\Contracts\Queue\ShouldQueue;
4use Illuminate\Contracts\Queue\ShouldBeUnique;
5 
6class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
7{
8 // ...
9}

在上面的示例中,UpdateSearchIndex 任務是唯一的。因此,如果該任務的另一個例項已經在佇列中且尚未完成處理,則不會分發該任務。

在某些情況下,您可能希望定義一個使任務唯一的特定“鍵”,或者您可能希望指定一個超時時間,超過此時間後該任務不再保持唯一。為了實現這一點,您可以使用 UniqueFor 屬性並在任務類上定義 uniqueId 方法。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Contracts\Queue\ShouldBeUnique;
7use Illuminate\Queue\Attributes\UniqueFor;
8 
9#[UniqueFor(3600)]
10class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
11{
12 /**
13 * The product instance.
14 *
15 * @var \App\Models\Product
16 */
17 public $product;
18 
19 /**
20 * Get the unique ID for the job.
21 */
22 public function uniqueId(): string
23 {
24 return $this->product->id;
25 }
26}

在上面的示例中,UpdateSearchIndex 任務透過產品 ID 保持唯一。因此,在現有任務完成處理之前,使用相同產品 ID 的任何新任務分發都將被忽略。此外,如果現有任務在一小時內未處理,唯一鎖將被釋放,具有相同唯一鍵的另一個任務可以被分發到佇列中。

如果您的應用程式從多個 Web 伺服器或容器分發任務,則應確保所有伺服器都與同一個中央快取伺服器通訊,以便 Laravel 能夠準確確定任務是否唯一。

保持任務唯一直到處理開始

預設情況下,唯一任務在任務完成處理或嘗試完所有重試次數後會“解鎖”。但是,在某些情況下,您可能希望任務在處理前立即解鎖。為了實現這一點,您的任務應實現 ShouldBeUniqueUntilProcessing 契約,而不是 ShouldBeUnique 契約。

1<?php
2 
3use Illuminate\Contracts\Queue\ShouldQueue;
4use Illuminate\Contracts\Queue\ShouldBeUniqueUntilProcessing;
5 
6class UpdateSearchIndex implements ShouldQueue, ShouldBeUniqueUntilProcessing
7{
8 // ...
9}

唯一任務鎖

在後臺,當分發 ShouldBeUnique 任務時,Laravel 會嘗試使用 uniqueId 鍵獲取一個 。如果鎖已被持有,則不會分發任務。當任務完成處理或嘗試完所有重試次數後,此鎖會被釋放。預設情況下,Laravel 將使用預設快取驅動程式來獲取此鎖。但是,如果您希望使用另一個驅動程式來獲取鎖,可以定義一個返回應使用的快取驅動程式的 uniqueVia 方法。

1use Illuminate\Contracts\Cache\Repository;
2use Illuminate\Support\Facades\Cache;
3 
4class UpdateSearchIndex implements ShouldQueue, ShouldBeUnique
5{
6 // ...
7 
8 /**
9 * Get the cache driver for the unique job lock.
10 */
11 public function uniqueVia(): Repository
12 {
13 return Cache::driver('redis');
14 }
15}

如果您只需要限制任務的併發處理,請使用 WithoutOverlapping 任務中介軟體。

加密任務

Laravel 允許您透過 加密 確保任務資料的隱私和完整性。首先,只需將 ShouldBeEncrypted 介面新增到任務類即可。將此介面新增到類中後,Laravel 會在將任務推送到佇列之前自動對其進行加密。

1<?php
2 
3use Illuminate\Contracts\Queue\ShouldBeEncrypted;
4use Illuminate\Contracts\Queue\ShouldQueue;
5 
6class UpdateSearchIndex implements ShouldQueue, ShouldBeEncrypted
7{
8 // ...
9}

任務中介軟體

任務中介軟體允許您在佇列任務的執行前後包裹自定義邏輯,從而減少任務本身中的樣板程式碼。例如,考慮以下 handle 方法,它利用 Laravel 的 Redis 速率限制功能來允許每五秒僅處理一個任務:

1use Illuminate\Support\Facades\Redis;
2 
3/**
4 * Execute the job.
5 */
6public function handle(): void
7{
8 Redis::throttle('key')->block(0)->allow(1)->every(5)->then(function () {
9 info('Lock obtained...');
10 
11 // Handle job...
12 }, function () {
13 // Could not obtain lock...
14 
15 return $this->release(5);
16 });
17}

雖然此程式碼有效,但 handle 方法的實現變得嘈雜,因為它被 Redis 速率限制邏輯弄亂了。此外,對於我們想要進行速率限制的任何其他任務,都必須複製此速率限制邏輯。與其在 handle 方法中進行速率限制,我們可以定義一個處理速率限制的任務中介軟體。

1<?php
2 
3namespace App\Jobs\Middleware;
4 
5use Closure;
6use Illuminate\Support\Facades\Redis;
7 
8class RateLimited
9{
10 /**
11 * Process the queued job.
12 *
13 * @param \Closure(object): void $next
14 */
15 public function handle(object $job, Closure $next): void
16 {
17 Redis::throttle('key')
18 ->block(0)->allow(1)->every(5)
19 ->then(function () use ($job, $next) {
20 // Lock obtained...
21 
22 $next($job);
23 }, function () use ($job) {
24 // Could not obtain lock...
25 
26 $job->release(5);
27 });
28 }
29}

正如您所見,與 路由中介軟體 一樣,任務中介軟體會接收正在處理的任務以及應被呼叫的回撥以繼續處理該任務。

您可以使用 make:job-middleware Artisan 命令生成一個新的任務中介軟體類。建立任務中介軟體後,可以透過從任務的 middleware 方法返回它們來將其附加到任務上。此方法在 make:job Artisan 命令生成的任務中不存在,因此您需要手動將其新增到您的任務類中。

1use App\Jobs\Middleware\RateLimited;
2 
3/**
4 * Get the middleware the job should pass through.
5 *
6 * @return array<int, object>
7 */
8public function middleware(): array
9{
10 return [new RateLimited];
11}

任務中介軟體也可以分配給 可佇列的事件監聽器郵件任務通知

速率限制

儘管我們剛剛演示瞭如何編寫自己的速率限制任務中介軟體,但 Laravel 實際上包含了一個速率限制中介軟體,您可以利用它來限制任務速率。與 路由速率限制器 一樣,任務速率限制器使用 RateLimiter 門面的 for 方法定義。

例如,您可能希望允許使用者每小時備份一次資料,同時不對高階客戶施加此類限制。為了實現這一點,您可以在 AppServiceProviderboot 方法中定義一個 RateLimiter

1use Illuminate\Cache\RateLimiting\Limit;
2use Illuminate\Support\Facades\RateLimiter;
3 
4/**
5 * Bootstrap any application services.
6 */
7public function boot(): void
8{
9 RateLimiter::for('backups', function (object $job) {
10 return $job->user->vipCustomer()
11 ? Limit::none()
12 : Limit::perHour(1)->by($job->user->id);
13 });
14}

在上面的示例中,我們定義了一個每小時的速率限制;但是,您可以使用 perMinute 方法輕鬆定義基於分鐘的速率限制。此外,您可以將任何所需的值傳遞給速率限制的 by 方法;但是,此值最常用於按客戶細分速率限制。

1return Limit::perMinute(50)->by($job->user->id);

一旦定義了速率限制,就可以使用 Illuminate\Queue\Middleware\RateLimited 中介軟體將速率限制器附加到您的任務。每當任務超過速率限制時,此中介軟體會將任務釋放回佇列,並根據速率限制持續時間設定適當的延遲。

1use Illuminate\Queue\Middleware\RateLimited;
2 
3/**
4 * Get the middleware the job should pass through.
5 *
6 * @return array<int, object>
7 */
8public function middleware(): array
9{
10 return [new RateLimited('backups')];
11}

將速率受限的任務釋放回佇列仍會增加任務的總 attempts 數。您可能需要相應地調整任務類上的 TriesMaxExceptions 屬性。或者,您可能希望使用 retryUntil 方法 來定義該任務不再嘗試之前的時間量。

使用 releaseAfter 方法,您還可以指定釋放的任務在再次嘗試之前必須經過的秒數。

1/**
2 * Get the middleware the job should pass through.
3 *
4 * @return array<int, object>
5 */
6public function middleware(): array
7{
8 return [(new RateLimited('backups'))->releaseAfter(60)];
9}

如果您不希望在任務受限時重試任務,則可以使用 dontRelease 方法。

1/**
2 * Get the middleware the job should pass through.
3 *
4 * @return array<int, object>
5 */
6public function middleware(): array
7{
8 return [(new RateLimited('backups'))->dontRelease()];
9}

使用 Redis 進行速率限制

如果您正在使用 Redis,則可以使用 Illuminate\Queue\Middleware\RateLimitedWithRedis 中介軟體,該中介軟體針對 Redis 進行了微調,比基本的速率限制中介軟體效率更高。

1use Illuminate\Queue\Middleware\RateLimitedWithRedis;
2 
3public function middleware(): array
4{
5 return [new RateLimitedWithRedis('backups')];
6}

connection 方法可用於指定中介軟體應使用哪個 Redis 連線。

1return [(new RateLimitedWithRedis('backups'))->connection('limiter')];

防止任務重疊

Laravel 包含一個 Illuminate\Queue\Middleware\WithoutOverlapping 中介軟體,允許您基於任意鍵防止任務重疊。當佇列任務正在修改資源,且該資源同一時間只能由一個任務修改時,這會很有幫助。

例如,假設您有一個更新使用者信用評分的佇列任務,並且您希望防止同一使用者 ID 的信用評分更新任務重疊。為了實現這一點,您可以從任務的 middleware 方法返回 WithoutOverlapping 中介軟體:

1use Illuminate\Queue\Middleware\WithoutOverlapping;
2 
3/**
4 * Get the middleware the job should pass through.
5 *
6 * @return array<int, object>
7 */
8public function middleware(): array
9{
10 return [new WithoutOverlapping($this->user->id)];
11}

將重疊任務釋放回佇列仍會增加任務的總嘗試次數。您可能需要相應地調整任務類上的 TriesMaxExceptions 屬性。例如,保留預設的 Tries 為 1 將防止任何重疊任務稍後被重試。

相同型別的任何重疊任務都將被釋放回佇列。您還可以指定釋放的任務在再次嘗試之前必須經過的秒數。

1/**
2 * Get the middleware the job should pass through.
3 *
4 * @return array<int, object>
5 */
6public function middleware(): array
7{
8 return [(new WithoutOverlapping($this->order->id))->releaseAfter(60)];
9}

如果您希望立即刪除任何重疊的任務,以便不再重試它們,則可以使用 dontRelease 方法。

1/**
2 * Get the middleware the job should pass through.
3 *
4 * @return array<int, object>
5 */
6public function middleware(): array
7{
8 return [(new WithoutOverlapping($this->order->id))->dontRelease()];
9}

WithoutOverlapping 中介軟體由 Laravel 的原子鎖功能提供支援。有時,您的任務可能會意外失敗或超時,導致鎖未被釋放。因此,您可以使用 expireAfter 方法顯式定義鎖過期時間。例如,下面的示例將指示 Laravel 在任務開始處理三分鐘後釋放 WithoutOverlapping 鎖:

1/**
2 * Get the middleware the job should pass through.
3 *
4 * @return array<int, object>
5 */
6public function middleware(): array
7{
8 return [(new WithoutOverlapping($this->order->id))->expireAfter(180)];
9}

WithoutOverlapping 中介軟體需要支援 的快取驅動程式。目前,memcachedredisdynamodbdatabasefilearray 快取驅動程式均支援原子鎖。

跨任務類共享鎖鍵

預設情況下,WithoutOverlapping 中介軟體僅防止同一類的任務重疊。因此,儘管兩個不同的任務類可能使用相同的鎖鍵,但它們不會被阻止重疊。但是,您可以使用 shared 方法指示 Laravel 在任務類之間應用該鍵:

1use Illuminate\Queue\Middleware\WithoutOverlapping;
2 
3class ProviderIsDown
4{
5 // ...
6 
7 public function middleware(): array
8 {
9 return [
10 (new WithoutOverlapping("status:{$this->provider}"))->shared(),
11 ];
12 }
13}
14 
15class ProviderIsUp
16{
17 // ...
18 
19 public function middleware(): array
20 {
21 return [
22 (new WithoutOverlapping("status:{$this->provider}"))->shared(),
23 ];
24 }
25}

異常限流

Laravel 包含一個 Illuminate\Queue\Middleware\ThrottlesExceptions 中介軟體,允許您對異常進行限流。一旦任務丟擲指定次數的異常,所有後續執行該任務的嘗試都會被延遲,直到指定的時間間隔過去。此中介軟體對於與不穩定的第三方服務互動的任務特別有用。

例如,假設一個佇列任務與開始丟擲異常的第三方 API 互動。要對異常進行限流,可以從任務的 middleware 方法返回 ThrottlesExceptions 中介軟體。通常,此中介軟體應與實現 基於時間嘗試 的任務配對:

1use DateTime;
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
3 
4/**
5 * Get the middleware the job should pass through.
6 *
7 * @return array<int, object>
8 */
9public function middleware(): array
10{
11 return [new ThrottlesExceptions(10, 5 * 60)];
12}
13 
14/**
15 * Determine the time at which the job should timeout.
16 */
17public function retryUntil(): DateTime
18{
19 return now()->plus(minutes: 30);
20}

中介軟體接受的第一個建構函式引數是任務被限流前可以丟擲的異常數量,第二個建構函式引數是任務被限流後再次嘗試之前應經過的秒數。在上面的程式碼示例中,如果任務丟擲 10 個連續異常,我們將等待 5 分鐘,然後再嘗試該任務,並受 30 分鐘的時間限制約束。

當任務丟擲異常但尚未達到異常閾值時,通常會立即重試該任務。但是,您可以透過在將中介軟體附加到任務時呼叫 backoff 方法來指定此類任務應延遲的分鐘數。

1use Illuminate\Queue\Middleware\ThrottlesExceptions;
2 
3/**
4 * Get the middleware the job should pass through.
5 *
6 * @return array<int, object>
7 */
8public function middleware(): array
9{
10 return [(new ThrottlesExceptions(10, 5 * 60))->backoff(5)];
11}

在內部,此中介軟體使用 Laravel 的快取系統來實現速率限制,並且任務類名稱被用作快取“鍵”。您可以透過在將中介軟體附加到任務時呼叫 by 方法來覆蓋此鍵。如果您有多個任務與同一個第三方服務互動,並且希望它們共享一個通用的限流“桶”以確保它們遵守單個共享限制,這將非常有用。

1use Illuminate\Queue\Middleware\ThrottlesExceptions;
2 
3/**
4 * Get the middleware the job should pass through.
5 *
6 * @return array<int, object>
7 */
8public function middleware(): array
9{
10 return [(new ThrottlesExceptions(10, 10 * 60))->by('key')];
11}

預設情況下,此中介軟體會限制每個異常。您可以透過在將中介軟體附加到任務時呼叫 when 方法來修改此行為。只有當提供給 when 方法的閉包返回 true 時,異常才會被限流。

1use Illuminate\Http\Client\HttpClientException;
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
3 
4/**
5 * Get the middleware the job should pass through.
6 *
7 * @return array<int, object>
8 */
9public function middleware(): array
10{
11 return [(new ThrottlesExceptions(10, 10 * 60))->when(
12 fn (Throwable $throwable) => $throwable instanceof HttpClientException
13 )];
14}

when 方法將任務釋放回佇列或丟擲異常不同,deleteWhen 方法允許您在給定異常發生時完全刪除任務。

1use App\Exceptions\CustomerDeletedException;
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
3 
4/**
5 * Get the middleware the job should pass through.
6 *
7 * @return array<int, object>
8 */
9public function middleware(): array
10{
11 return [(new ThrottlesExceptions(2, 10 * 60))->deleteWhen(CustomerDeletedException::class)];
12}

如果您希望將受限異常報告給應用程式的異常處理程式,可以透過在將中介軟體附加到任務時呼叫 report 方法來實現。或者,您可以為 report 方法提供一個閉包,只有當給定閉包返回 true 時,才會報告異常。

1use Illuminate\Http\Client\HttpClientException;
2use Illuminate\Queue\Middleware\ThrottlesExceptions;
3 
4/**
5 * Get the middleware the job should pass through.
6 *
7 * @return array<int, object>
8 */
9public function middleware(): array
10{
11 return [(new ThrottlesExceptions(10, 10 * 60))->report(
12 fn (Throwable $throwable) => $throwable instanceof HttpClientException
13 )];
14}

使用 Redis 進行異常限流

如果您正在使用 Redis,則可以使用 Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis 中介軟體,該中介軟體針對 Redis 進行了微調,比基本的異常限流中介軟體效率更高。

1use Illuminate\Queue\Middleware\ThrottlesExceptionsWithRedis;
2 
3public function middleware(): array
4{
5 return [new ThrottlesExceptionsWithRedis(10, 10 * 60)];
6}

connection 方法可用於指定中介軟體應使用哪個 Redis 連線。

1return [(new ThrottlesExceptionsWithRedis(10, 10 * 60))->connection('limiter')];

跳過任務

Skip 中介軟體允許您指定應跳過/刪除任務,而無需修改任務的邏輯。如果給定條件評估為 trueSkip::when 方法將刪除任務;如果條件評估為 falseSkip::unless 方法將刪除任務。

1use Illuminate\Queue\Middleware\Skip;
2 
3/**
4 * Get the middleware the job should pass through.
5 */
6public function middleware(): array
7{
8 return [
9 Skip::when($condition),
10 ];
11}

您也可以將 Closure 傳遞給 whenunless 方法,以進行更復雜的條件評估。

1use Illuminate\Queue\Middleware\Skip;
2 
3/**
4 * Get the middleware the job should pass through.
5 */
6public function middleware(): array
7{
8 return [
9 Skip::when(function (): bool {
10 return $this->shouldSkip();
11 }),
12 ];
13}

分發任務

編寫任務類後,您可以使用任務本身的 dispatch 方法對其進行分發。傳遞給 dispatch 方法的引數將提供給任務的建構函式。

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Jobs\ProcessPodcast;
6use App\Models\Podcast;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9 
10class PodcastController extends Controller
11{
12 /**
13 * Store a new podcast.
14 */
15 public function store(Request $request): RedirectResponse
16 {
17 $podcast = Podcast::create(/* ... */);
18 
19 // ...
20 
21 ProcessPodcast::dispatch($podcast);
22 
23 return redirect('/podcasts');
24 }
25}

如果您想有條件地分發任務,可以使用 dispatchIfdispatchUnless 方法。

1ProcessPodcast::dispatchIf($accountActive, $podcast);
2 
3ProcessPodcast::dispatchUnless($accountSuspended, $podcast);

在新的 Laravel 應用程式中,database 連線被定義為預設佇列。您可以透過更改應用程式 .env 檔案中的 QUEUE_CONNECTION 環境變數來指定不同的預設佇列連線。

延遲分發

如果您想指定任務不應立即供佇列工作程序處理,可以在分發任務時使用 delay 方法。例如,讓我們指定一個任務在分發 10 分鐘後才能進行處理:

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Jobs\ProcessPodcast;
6use App\Models\Podcast;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9 
10class PodcastController extends Controller
11{
12 /**
13 * Store a new podcast.
14 */
15 public function store(Request $request): RedirectResponse
16 {
17 $podcast = Podcast::create(/* ... */);
18 
19 // ...
20 
21 ProcessPodcast::dispatch($podcast)
22 ->delay(now()->plus(minutes: 10));
23 
24 return redirect('/podcasts');
25 }
26}

在某些情況下,任務可能配置了預設延遲。如果您需要繞過此延遲並立即分發任務進行處理,可以使用 withoutDelay 方法。

1ProcessPodcast::dispatch($podcast)->withoutDelay();

Amazon SQS 佇列服務的最大延遲時間為 15 分鐘。

同步分發

如果您想立即(同步)分發任務,可以使用 dispatchSync 方法。使用此方法時,任務不會被排隊,並將在當前程序內立即執行。

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Jobs\ProcessPodcast;
6use App\Models\Podcast;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9 
10class PodcastController extends Controller
11{
12 /**
13 * Store a new podcast.
14 */
15 public function store(Request $request): RedirectResponse
16 {
17 $podcast = Podcast::create(/* ... */);
18 
19 // Create podcast...
20 
21 ProcessPodcast::dispatchSync($podcast);
22 
23 return redirect('/podcasts');
24 }
25}

延遲分發

使用延遲同步分發,您可以分發一個任務,使其在當前程序中處理,但在 HTTP 響應傳送給使用者之後。這允許您同步處理“佇列”任務,而不會減慢使用者的應用程式體驗。要延遲同步任務的執行,請將任務分發到 deferred 連線:

1RecordDelivery::dispatch($order)->onConnection('deferred');

deferred 連線也充當預設的 故障轉移佇列

同樣,background 連線在 HTTP 響應傳送給使用者後處理任務;但是,該任務是在一個單獨派生的 PHP 程序中處理的,允許 PHP-FPM / 應用程式工作程序能夠處理另一個傳入的 HTTP 請求。

1RecordDelivery::dispatch($order)->onConnection('background');

任務與資料庫事務

雖然在資料庫事務中分發任務完全沒有問題,但您應該特別小心,確保您的任務確實能夠成功執行。在事務中分發任務時,任務有可能在父事務提交之前就被工作程序處理。發生這種情況時,您在資料庫事務期間對模型或資料庫記錄所做的任何更新可能尚未在資料庫中反映出來。此外,在事務中建立的任何模型或資料庫記錄在資料庫中可能還不存在。

幸運的是,Laravel 提供了幾種解決此問題的方法。首先,您可以在佇列連線的配置陣列中設定 after_commit 連線選項:

1'redis' => [
2 'driver' => 'redis',
3 // ...
4 'after_commit' => true,
5],

after_commit 選項為 true 時,您可以在資料庫事務中分發任務;但是,Laravel 會等到開放的父資料庫事務提交後才會實際分發任務。當然,如果當前沒有開啟任何資料庫事務,任務將立即分發。

如果事務由於事務期間發生的異常而回滾,則在該事務期間分發的任務將被丟棄。

after_commit 配置選項設定為 true 還會導致任何佇列事件監聽器、郵件任務、通知和廣播事件在所有開啟的資料庫事務提交後分發。

內聯指定提交分發行為

如果您沒有將 after_commit 佇列連線配置選項設定為 true,您仍然可以指示在所有開啟的資料庫事務提交後分發特定任務。為了實現這一點,您可以將 afterCommit 方法連結到您的分發操作上:

1use App\Jobs\ProcessPodcast;
2 
3ProcessPodcast::dispatch($podcast)->afterCommit();

同樣,如果 after_commit 配置選項設定為 true,您可以指示立即分發特定任務,而無需等待任何開啟的資料庫事務提交。

1ProcessPodcast::dispatch($podcast)->beforeCommit();

任務鏈

任務鏈允許您指定一系列佇列任務,這些任務應在主任務成功執行後按順序執行。如果序列中的一個任務失敗,其餘任務將不會執行。要執行佇列任務鏈,可以使用 Bus 門面提供的 chain 方法。Laravel 的命令匯流排是一個更底層的元件,佇列任務分發正是建立在其之上的。

1use App\Jobs\OptimizePodcast;
2use App\Jobs\ProcessPodcast;
3use App\Jobs\ReleasePodcast;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::chain([
7 new ProcessPodcast,
8 new OptimizePodcast,
9 new ReleasePodcast,
10])->dispatch();

除了連結任務類例項外,您還可以連結閉包:

1Bus::chain([
2 new ProcessPodcast,
3 new OptimizePodcast,
4 function () {
5 Podcast::update(/* ... */);
6 },
7])->dispatch();

在任務中使用 $this->delete() 方法刪除任務不會阻止連結任務的處理。鏈只有在鏈中的任務失敗時才會停止執行。

鏈連線與佇列

如果您想指定應為鏈式任務使用的連線和佇列,可以使用 onConnectiononQueue 方法。除非佇列任務被顯式分配了不同的連線/佇列,否則這些方法將指定應使用的佇列連線和佇列名稱。

1Bus::chain([
2 new ProcessPodcast,
3 new OptimizePodcast,
4 new ReleasePodcast,
5])->onConnection('redis')->onQueue('podcasts')->dispatch();

向鏈中新增任務

有時,您可能需要從鏈中的另一個任務內向前置或向後追加一個任務到現有的任務鏈中。您可以使用 prependToChainappendToChain 方法來實現這一點。

1/**
2 * Execute the job.
3 */
4public function handle(): void
5{
6 // ...
7 
8 // Prepend to the current chain, run job immediately after current job...
9 $this->prependToChain(new TranscribePodcast);
10 
11 // Append to the current chain, run job at end of chain...
12 $this->appendToChain(new TranscribePodcast);
13}

鏈失敗

在連結任務時,您可以使用 catch 方法指定如果鏈中的任務失敗時應呼叫的閉包。給定回撥將接收導致任務失敗的 Throwable 例項。

1use Illuminate\Support\Facades\Bus;
2use Throwable;
3 
4Bus::chain([
5 new ProcessPodcast,
6 new OptimizePodcast,
7 new ReleasePodcast,
8])->catch(function (Throwable $e) {
9 // A job within the chain has failed...
10})->dispatch();

由於鏈回撥是由 Laravel 佇列序列化並在稍後執行的,因此不應在鏈回撥中使用 $this 變數。

自定義佇列和連線

分發到特定佇列

透過將任務推送到不同的佇列,您可以對佇列任務進行“分類”,甚至可以優先處理分配給不同佇列的工作程序數量。請記住,這不會將任務推送到由您的佇列配置檔案定義的不同的佇列“連線”,而只會推送到單個連線內的特定佇列。要指定佇列,請在分發任務時使用 onQueue 方法。

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Jobs\ProcessPodcast;
6use App\Models\Podcast;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9 
10class PodcastController extends Controller
11{
12 /**
13 * Store a new podcast.
14 */
15 public function store(Request $request): RedirectResponse
16 {
17 $podcast = Podcast::create(/* ... */);
18 
19 // Create podcast...
20 
21 ProcessPodcast::dispatch($podcast)->onQueue('processing');
22 
23 return redirect('/podcasts');
24 }
25}

或者,您可以透過在任務的建構函式中呼叫 onQueue 方法來指定任務的佇列。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Foundation\Queue\Queueable;
7 
8class ProcessPodcast implements ShouldQueue
9{
10 use Queueable;
11 
12 /**
13 * Create a new job instance.
14 */
15 public function __construct()
16 {
17 $this->onQueue('processing');
18 }
19}

分發到特定連線

如果您的應用程式與多個佇列連線互動,您可以使用 onConnection 方法指定將任務推送到哪個連線。

1<?php
2 
3namespace App\Http\Controllers;
4 
5use App\Jobs\ProcessPodcast;
6use App\Models\Podcast;
7use Illuminate\Http\RedirectResponse;
8use Illuminate\Http\Request;
9 
10class PodcastController extends Controller
11{
12 /**
13 * Store a new podcast.
14 */
15 public function store(Request $request): RedirectResponse
16 {
17 $podcast = Podcast::create(/* ... */);
18 
19 // Create podcast...
20 
21 ProcessPodcast::dispatch($podcast)->onConnection('sqs');
22 
23 return redirect('/podcasts');
24 }
25}

您可以將 onConnectiononQueue 方法連結在一起,為任務指定連線和佇列。

1ProcessPodcast::dispatch($podcast)
2 ->onConnection('sqs')
3 ->onQueue('processing');

或者,您可以透過在任務的建構函式中呼叫 onConnection 方法來指定任務的連線。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Foundation\Queue\Queueable;
7 
8class ProcessPodcast implements ShouldQueue
9{
10 use Queueable;
11 
12 /**
13 * Create a new job instance.
14 */
15 public function __construct()
16 {
17 $this->onConnection('sqs');
18 }
19}

佇列路由

您可以使用 Queue 門面的 route 方法為特定任務類定義預設連線和佇列。當您想要確保某些任務始終使用特定佇列,而無需在任務上指定連線或佇列時,這非常有用。

除了路由特定任務類外,您還可以將介面、trait 或父類傳遞給 route 方法。當您這樣做時,任何實現該介面、使用該 trait 或擴充套件該父類的任務都將自動使用配置的連線和佇列。

通常,您應該從服務提供者的 boot 方法中呼叫 route 方法。

1use App\Concerns\RequiresVideo;
2use App\Jobs\ProcessPodcast;
3use App\Jobs\ProcessVideo;
4use Illuminate\Support\Facades\Queue;
5 
6/**
7 * Bootstrap any application services.
8 */
9public function boot(): void
10{
11 Queue::route(ProcessPodcast::class, connection: 'redis', queue: 'podcasts');
12 Queue::route(RequiresVideo::class, queue: 'video');
13}

當指定了連線而沒有指定佇列時,任務將被髮送到預設佇列。

1Queue::route(ProcessPodcast::class, connection: 'redis');

您還可以透過將陣列傳遞給 route 方法來一次路由多個任務類。

1Queue::route([
2 ProcessPodcast::class => ['podcasts', 'redis'], // Queue and connection
3 ProcessVideo::class => 'videos', // Queue only (uses default connection)
4]);

佇列路由仍然可以在每個任務的基礎上被任務覆蓋。

指定任務最大嘗試次數 / 超時值

最大嘗試次數

任務嘗試是 Laravel 佇列系統的核心概念,也是許多高階功能的基礎。雖然起初可能會讓人感到困惑,但在修改預設配置之前,瞭解它們的工作方式很重要。

當任務被分發時,它被推送到佇列。然後,工作程序會拾取它並嘗試執行它。這就是一次任務嘗試。

但是,一次嘗試並不一定意味著執行了任務的 handle 方法。嘗試也可以透過多種方式被“消耗”:

  • 任務在執行期間遇到未處理的異常。
  • 任務使用 $this->release() 手動釋放回佇列。
  • 諸如 WithoutOverlappingRateLimited 之類的中介軟體未能獲取鎖並釋放了任務。
  • 任務超時。
  • 任務的 handle 方法執行並完成,沒有丟擲異常。

您可能不希望無限期地嘗試任務。因此,Laravel 提供了多種方式來指定任務可以嘗試多少次或嘗試多長時間。

預設情況下,Laravel 只會嘗試一次任務。如果您的任務使用了 WithoutOverlappingRateLimited 等中介軟體,或者您正在手動釋放任務,則很可能需要透過 tries 選項增加允許的嘗試次數。

指定任務最大嘗試次數的一種方法是透過 Artisan 命令列上的 --tries 開關。這將應用於工作程序處理的所有任務,除非被處理的任務指定了可以嘗試的次數。

1php artisan queue:work --tries=3

如果任務超過其最大嘗試次數,它將被視為“失敗”的任務。有關處理失敗任務的更多資訊,請參閱 失敗任務文件。如果向 queue:work 命令提供了 --tries=0,任務將無限期重試。

您可以採取更細粒度的方法,透過使用 Tries 屬性在任務類本身上定義任務可以嘗試的最大次數。如果任務上指定了最大嘗試次數,它將優先於命令列上提供的 --tries 值。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\Tries;
6 
7#[Tries(5)]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

如果您需要動態控制特定任務的最大嘗試次數,可以在任務上定義一個 tries 方法。

1/**
2 * Determine number of times the job may be attempted.
3 */
4public function tries(): int
5{
6 return 5;
7}

基於時間的嘗試

作為定義任務失敗前可嘗試次數的替代方案,您可以定義任務不再嘗試的時間。這允許任務在給定的時間範圍內嘗試任意次數。要定義任務不再嘗試的時間,請將 retryUntil 方法新增到您的任務類中。此方法應返回一個 DateTime 例項。

1use DateTime;
2 
3/**
4 * Determine the time at which the job should timeout.
5 */
6public function retryUntil(): DateTime
7{
8 return now()->plus(minutes: 10);
9}

如果同時定義了 retryUntiltries,Laravel 會優先考慮 retryUntil 方法。

您還可以在您的 佇列事件監聽器佇列通知 上定義 Tries 屬性或 retryUntil 方法。

最大異常數

有時您可能希望指定任務可以嘗試多次,但如果重試是由給定數量的未處理異常觸發的(而不是直接由 release 方法釋放),則應失敗。為了實現這一點,您可以在任務類上使用 TriesMaxExceptions 屬性。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Foundation\Queue\Queueable;
7use Illuminate\Queue\Attributes\MaxExceptions;
8use Illuminate\Queue\Attributes\Tries;
9use Illuminate\Support\Facades\Redis;
10 
11#[Tries(25)]
12#[MaxExceptions(3)]
13class ProcessPodcast implements ShouldQueue
14{
15 use Queueable;
16 
17 /**
18 * Execute the job.
19 */
20 public function handle(): void
21 {
22 Redis::throttle('key')->allow(10)->every(60)->then(function () {
23 // Lock obtained, process the podcast...
24 }, function () {
25 // Unable to obtain lock...
26 return $this->release(10);
27 });
28 }
29}

在此示例中,如果應用程式無法獲取 Redis 鎖,任務將釋放十秒,並將繼續重試最多 25 次。但是,如果任務丟擲三個未處理的異常,則任務將失敗。

超時

通常,您大致知道佇列任務需要花費多長時間。因此,Laravel 允許您指定“超時”值。預設情況下,超時值為 60 秒。如果任務處理時間超過超時值指定的秒數,處理任務的工作程序將退出並報錯。通常,工作程序將由 在伺服器上配置的程序管理器 自動重啟。

可以使用 Artisan 命令列上的 --timeout 開關指定任務可以執行的最大秒數。

1php artisan queue:work --timeout=30

如果任務因持續超時而超過其最大嘗試次數,它將被標記為失敗。

您還可以使用任務類上的 Timeout 屬性定義允許任務執行的最大秒數。如果任務上指定了超時,它將優先於命令列上指定的任何超時。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\Timeout;
6 
7#[Timeout(120)]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

有時,套接字或傳出 HTTP 連線等 IO 阻塞程序可能不遵守您指定的超時。因此,在使用這些功能時,您也應始終嘗試使用它們的 API 指定超時。例如,使用 Guzzle 時,應始終指定連線和請求超時值。

必須安裝 PCNTL PHP 擴展才能指定任務超時。此外,任務的“超時”值應始終小於其 “重試後” 值。否則,任務可能會在實際完成執行或超時之前被再次嘗試。

超時失敗

如果您想指示任務在超時時應被標記為 失敗,可以使用任務類上的 FailOnTimeout 屬性:

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\FailOnTimeout;
6 
7#[FailOnTimeout]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

預設情況下,當任務超時時,它會消耗一次嘗試並被釋放回佇列(如果允許重試)。但是,如果您將任務配置為超時失敗,則無論為 tries 設定的值如何,它都不會被重試。

SQS FIFO 和公平佇列

Laravel 支援 Amazon SQS FIFO (先進先出) 佇列,允許您以傳送的確切順序處理任務,同時透過訊息去重確保有且僅有一次的處理。

FIFO 佇列需要一個訊息組 ID 來確定哪些任務可以並行處理。具有相同組 ID 的任務按順序處理,而具有不同組 ID 的訊息可以併發處理。

Laravel 提供了一個流式 onGroup 方法,用於在分發任務時指定訊息組 ID。

1ProcessOrder::dispatch($order)
2 ->onGroup("customer-{$order->customer_id}");

SQS FIFO 佇列支援訊息去重以確保有且僅有一次的處理。在您的任務類中實現一個 deduplicationId 方法來提供自定義去重 ID。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Contracts\Queue\ShouldQueue;
6use Illuminate\Foundation\Queue\Queueable;
7 
8class ProcessSubscriptionRenewal implements ShouldQueue
9{
10 use Queueable;
11 
12 // ...
13 
14 /**
15 * Get the job's deduplication ID.
16 */
17 public function deduplicationId(): string
18 {
19 return "renewal-{$this->subscription->id}";
20 }
21}

FIFO 監聽器、郵件和通知

使用 FIFO 佇列時,您還需要在監聽器、郵件和通知上定義訊息組。或者,您可以將這些物件的可佇列例項分發到非 FIFO 佇列。

要為 佇列事件監聽器 定義訊息組,請在監聽器上定義一個 messageGroup 方法。您也可以選擇定義一個 deduplicationId 方法。

1<?php
2 
3namespace App\Listeners;
4 
5class SendShipmentNotification
6{
7 // ...
8 
9 /**
10 * Get the job's message group.
11 */
12 public function messageGroup(): string
13 {
14 return 'shipments';
15 }
16 
17 /**
18 * Get the job's deduplication ID.
19 */
20 public function deduplicationId(): string
21 {
22 return "shipment-notification-{$this->shipment->id}";
23 }
24}

傳送將在 FIFO 佇列上排隊的 郵件訊息 時,您應該在傳送通知時呼叫 onGroup 方法,並可選擇呼叫 withDeduplicator 方法。

1use App\Mail\InvoicePaid;
2use Illuminate\Support\Facades\Mail;
3 
4$invoicePaid = (new InvoicePaid($invoice))
5 ->onGroup('invoices')
6 ->withDeduplicator(fn () => 'invoices-'.$invoice->id);
7 
8Mail::to($request->user())->send($invoicePaid);

傳送將在 FIFO 佇列上排隊的 通知 時,您應該在傳送通知時呼叫 onGroup 方法,並可選擇呼叫 withDeduplicator 方法。

1use App\Notifications\InvoicePaid;
2 
3$invoicePaid = (new InvoicePaid($invoice))
4 ->onGroup('invoices')
5 ->withDeduplicator(fn () => 'invoices-'.$invoice->id);
6 
7$user->notify($invoicePaid);

佇列故障轉移

failover 佇列驅動程式在將任務推送到佇列時提供自動故障轉移功能。如果 failover 配置的主佇列連線因任何原因失敗,Laravel 將自動嘗試將任務推送到列表中配置的下一個連線。這對於確保生產環境中的高可用性特別有用,其中佇列可靠性至關重要。

要配置故障轉移佇列連線,請指定 failover 驅動程式並提供一個連線名稱陣列,以按順序嘗試。預設情況下,Laravel 在應用程式的 config/queue.php 配置檔案中包含了一個故障轉移配置示例。

1'failover' => [
2 'driver' => 'failover',
3 'connections' => [
4 'redis',
5 'database',
6 'sync',
7 ],
8],

配置了使用 failover 驅動程式的連線後,您需要在應用程式的 .env 檔案中將故障轉移連線設定為預設佇列連線,以使用故障轉移功能。

1QUEUE_CONNECTION=failover

接下來,為故障轉移連線列表中的每個連線啟動至少一個工作程序。

1php artisan queue:work redis
2php artisan queue:work database

您不需要為使用 syncbackgrounddeferred 佇列驅動程式的連線執行工作程序,因為這些驅動程式在當前 PHP 程序中處理任務。

當佇列連線操作失敗且啟用故障轉移時,Laravel 將分發 Illuminate\Queue\Events\QueueFailedOver 事件,允許您報告或記錄佇列連線已失敗。

如果您使用 Laravel Horizon,請記住 Horizon 僅管理 Redis 佇列。如果您的故障轉移列表包含 database,您應該與 Horizon 一起執行常規的 php artisan queue:work database 程序。

錯誤處理

如果任務處理期間丟擲異常,該任務將自動釋放回佇列,以便再次嘗試。任務將繼續釋放,直到嘗試次數達到應用程式允許的最大次數。最大嘗試次數由 queue:work Artisan 命令上使用的 --tries 開關定義。或者,可以在任務類本身上定義最大嘗試次數。有關執行佇列工作程序的更多資訊 可以在下方找到

手動釋放任務

有時您可能希望手動將任務釋放回佇列,以便稍後再次嘗試。您可以透過呼叫 release 方法來實現這一點:

1/**
2 * Execute the job.
3 */
4public function handle(): void
5{
6 // ...
7 
8 $this->release();
9}

預設情況下,release 方法會將任務釋放回佇列以進行立即處理。但是,您可以透過將整數或日期例項傳遞給 release 方法,來指示佇列在給定的秒數過去之前不使任務可供處理。

1$this->release(10);
2 
3$this->release(now()->plus(seconds: 10));

手動使任務失敗

有時您可能需要手動將任務標記為“失敗”。為此,您可以呼叫 fail 方法:

1/**
2 * Execute the job.
3 */
4public function handle(): void
5{
6 // ...
7 
8 $this->fail();
9}

如果您想因捕獲到的異常而將任務標記為失敗,可以將該異常傳遞給 fail 方法。或者,為了方便起見,您可以傳遞一個字串錯誤訊息,該訊息將為您轉換為異常。

1$this->fail($exception);
2 
3$this->fail('Something went wrong.');

有關失敗任務的更多資訊,請檢視 處理任務失敗的文件

在特定異常上使任務失敗

FailOnException 任務中介軟體 允許您在丟擲特定異常時短路重試。這允許對瞬態異常(如外部 API 錯誤)進行重試,但對於持久異常(如使用者許可權被撤銷)使任務永久失敗。

1<?php
2 
3namespace App\Jobs;
4 
5use App\Models\User;
6use Illuminate\Auth\Access\AuthorizationException;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Queue\Queueable;
9use Illuminate\Queue\Attributes\Tries;
10use Illuminate\Queue\Middleware\FailOnException;
11use Illuminate\Support\Facades\Http;
12 
13#[Tries(3)]
14class SyncChatHistory implements ShouldQueue
15{
16 use Queueable;
17 
18 /**
19 * Create a new job instance.
20 */
21 public function __construct(
22 public User $user,
23 ) {}
24 
25 /**
26 * Execute the job.
27 */
28 public function handle(): void
29 {
30 $this->user->authorize('sync-chat-history');
31 
32 $response = Http::throw()->get(
33 "https://chat.laravel.test/?user={$this->user->uuid}"
34 );
35 
36 // ...
37 }
38 
39 /**
40 * Get the middleware the job should pass through.
41 */
42 public function middleware(): array
43 {
44 return [
45 new FailOnException([AuthorizationException::class])
46 ];
47 }
48}

任務批處理

Laravel 的任務批處理功能允許您輕鬆執行一組並行任務,然後在批處理任務執行完成後執行某些操作。

在開始之前,您應該建立一個數據庫遷移來構建一個表,其中將包含有關任務批處理的元資訊,例如它們的完成百分比。此遷移可以使用 make:queue-batches-table Artisan 命令生成。

1php artisan make:queue-batches-table
2 
3php artisan migrate

定義可批處理的任務

要定義可批處理的任務,您應該像往常一樣 建立一個可佇列的任務;但是,您應該將 Illuminate\Bus\Batchable trait 新增到任務類中。此 trait 提供了對 batch 方法的訪問,可用於檢索任務在其內執行的當前批處理。

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Bus\Batchable;
6use Illuminate\Contracts\Queue\ShouldQueue;
7use Illuminate\Foundation\Queue\Queueable;
8 
9class ImportCsv implements ShouldQueue
10{
11 use Batchable, Queueable;
12 
13 /**
14 * Execute the job.
15 */
16 public function handle(): void
17 {
18 if ($this->batch()->cancelled()) {
19 // Determine if the batch has been cancelled...
20 
21 return;
22 }
23 
24 // Import a portion of the CSV file...
25 }
26}

分發批處理

要分發任務批處理,您應該使用 Bus 門面的 batch 方法。當然,批處理主要在與完成回撥結合使用時才有用。因此,您可以使用 thencatchfinally 方法為批處理定義完成回撥。當呼叫這些回撥時,它們中的每一個都將接收一個 Illuminate\Bus\Batch 例項。

執行多個佇列工作程序時,批處理中的任務將並行處理。因此,任務完成的順序可能與新增到批處理的順序不一致。有關如何按順序執行一系列任務的資訊,請諮詢我們關於 任務鏈和批處理 的文件。

在此示例中,我們將想象我們正在排隊處理一批任務,每個任務處理 CSV 檔案中的給定行數:

1use App\Jobs\ImportCsv;
2use Illuminate\Bus\Batch;
3use Illuminate\Support\Facades\Bus;
4use Throwable;
5 
6$batch = Bus::batch([
7 new ImportCsv(1, 100),
8 new ImportCsv(101, 200),
9 new ImportCsv(201, 300),
10 new ImportCsv(301, 400),
11 new ImportCsv(401, 500),
12])->before(function (Batch $batch) {
13 // The batch has been created but no jobs have been added...
14})->progress(function (Batch $batch) {
15 // A single job has completed successfully...
16})->then(function (Batch $batch) {
17 // All jobs completed successfully...
18})->catch(function (Batch $batch, Throwable $e) {
19 // Batch job failure detected...
20})->finally(function (Batch $batch) {
21 // The batch has finished executing...
22})->dispatch();
23 
24return $batch->id;

可以透過 $batch->id 屬性訪問的批處理 ID,可用於在批處理分發後 查詢 Laravel 命令匯流排 以獲取有關批處理的資訊。

由於批處理回撥是由 Laravel 佇列序列化並在稍後執行的,因此不應在回撥中使用 $this 變數。此外,由於批處理任務被包裹在資料庫事務中,觸發隱式提交的資料庫語句不應在任務中執行。

命名批處理

如果命名了批處理,某些工具(如 Laravel HorizonLaravel Telescope)可能會為批處理提供更使用者友好的除錯資訊。要為批處理分配任意名稱,您可以在定義批處理時呼叫 name 方法。

1$batch = Bus::batch([
2 // ...
3])->then(function (Batch $batch) {
4 // All jobs completed successfully...
5})->name('Import CSV')->dispatch();

批處理連線與佇列

如果您想指定應為批處理任務使用的連線和佇列,可以使用 onConnectiononQueue 方法。所有批處理任務必須在同一個連線和佇列內執行。

1$batch = Bus::batch([
2 // ...
3])->then(function (Batch $batch) {
4 // All jobs completed successfully...
5})->onConnection('redis')->onQueue('imports')->dispatch();

鏈與批處理

您可以透過將鏈式任務放入陣列中,在批處理中定義一組 鏈式任務。例如,我們可以並行執行兩個任務鏈,並在兩個任務鏈都完成處理後執行回撥:

1use App\Jobs\ReleasePodcast;
2use App\Jobs\SendPodcastReleaseNotification;
3use Illuminate\Bus\Batch;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::batch([
7 [
8 new ReleasePodcast(1),
9 new SendPodcastReleaseNotification(1),
10 ],
11 [
12 new ReleasePodcast(2),
13 new SendPodcastReleaseNotification(2),
14 ],
15])->then(function (Batch $batch) {
16 // All jobs completed successfully...
17})->dispatch();

相反,您可以透過在鏈中定義批處理來在 中執行任務批處理。例如,您可以先執行一批任務來發布多個播客,然後執行一批任務來發送釋出通知:

1use App\Jobs\FlushPodcastCache;
2use App\Jobs\ReleasePodcast;
3use App\Jobs\SendPodcastReleaseNotification;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::chain([
7 new FlushPodcastCache,
8 Bus::batch([
9 new ReleasePodcast(1),
10 new ReleasePodcast(2),
11 ]),
12 Bus::batch([
13 new SendPodcastReleaseNotification(1),
14 new SendPodcastReleaseNotification(2),
15 ]),
16])->dispatch();

向批處理新增任務

有時從批處理任務內向批處理新增額外任務可能會很有用。當您需要批處理成千上萬個任務,而這些任務在 Web 請求期間分發花費太長時間時,這種模式很有用。因此,您可以分發初始的“載入程式”任務批處理,這些任務將用更多工填充批處理。

1$batch = Bus::batch([
2 new LoadImportBatch,
3 new LoadImportBatch,
4 new LoadImportBatch,
5])->then(function (Batch $batch) {
6 // All jobs completed successfully...
7})->name('Import Contacts')->dispatch();

在此示例中,我們將使用 LoadImportBatch 任務來用額外任務填充批處理。為了實現這一點,我們可以使用可以透過任務的 batch 方法訪問的批處理例項上的 add 方法:

1use App\Jobs\ImportContacts;
2use Illuminate\Support\Collection;
3 
4/**
5 * Execute the job.
6 */
7public function handle(): void
8{
9 if ($this->batch()->cancelled()) {
10 return;
11 }
12 
13 $this->batch()->add(Collection::times(1000, function () {
14 return new ImportContacts;
15 }));
16}

您只能從屬於同一批處理的任務內向批處理新增任務。

檢查批處理

提供給批處理完成回撥的 Illuminate\Bus\Batch 例項具有各種屬性和方法,可幫助您與給定的任務批處理互動並檢查它們。

1// The UUID of the batch...
2$batch->id;
3 
4// The name of the batch (if applicable)...
5$batch->name;
6 
7// The number of jobs assigned to the batch...
8$batch->totalJobs;
9 
10// The number of jobs that have not been processed by the queue...
11$batch->pendingJobs;
12 
13// The number of jobs that have failed...
14$batch->failedJobs;
15 
16// The number of jobs that have been processed thus far...
17$batch->processedJobs();
18 
19// The completion percentage of the batch (0-100)...
20$batch->progress();
21 
22// Indicates if the batch has finished executing...
23$batch->finished();
24 
25// Cancel the execution of the batch...
26$batch->cancel();
27 
28// Indicates if the batch has been cancelled...
29$batch->cancelled();

從路由返回批處理

所有 Illuminate\Bus\Batch 例項都是 JSON 可序列化的,這意味著您可以直接從應用程式的路由之一返回它們,以檢索包含有關批處理資訊(包括其完成進度)的 JSON 有效負載。這使得在應用程式 UI 中顯示有關批處理完成進度的資訊變得很方便。

要按 ID 檢索批處理,可以使用 Bus 門面的 findBatch 方法:

1use Illuminate\Support\Facades\Bus;
2use Illuminate\Support\Facades\Route;
3 
4Route::get('/batch/{batchId}', function (string $batchId) {
5 return Bus::findBatch($batchId);
6});

取消批處理

有時您可能需要取消給定批處理的執行。可以透過在 Illuminate\Bus\Batch 例項上呼叫 cancel 方法來實現這一點:

1/**
2 * Execute the job.
3 */
4public function handle(): void
5{
6 if ($this->user->exceedsImportLimit()) {
7 $this->batch()->cancel();
8 
9 return;
10 }
11 
12 if ($this->batch()->cancelled()) {
13 return;
14 }
15}

正如您在前面的示例中可能注意到的那樣,批處理任務通常應在繼續執行之前確定其相應的批處理是否已取消。但是,為了方便起見,您可以改為將 SkipIfBatchCancelled 中介軟體 分配給任務。顧名思義,此中介軟體將指示 Laravel 在其相應的批處理已取消的情況下不處理該任務。

1use Illuminate\Queue\Middleware\SkipIfBatchCancelled;
2 
3/**
4 * Get the middleware the job should pass through.
5 */
6public function middleware(): array
7{
8 return [new SkipIfBatchCancelled];
9}

批處理失敗

當批處理任務失敗時,將呼叫 catch 回撥(如果已分配)。此回撥僅針對批處理中第一個失敗的任務呼叫。

允許失敗

當批處理中的任務失敗時,Laravel 會自動將批處理標記為“已取消”。如果您願意,可以停用此行為,以便任務失敗不會自動將批處理標記為已取消。這可以透過在分發批處理時呼叫 allowFailures 方法來實現:

1$batch = Bus::batch([
2 // ...
3])->then(function (Batch $batch) {
4 // All jobs completed successfully...
5})->allowFailures()->dispatch();

您可以選擇為 allowFailures 方法提供一個閉包,該閉包將在每次任務失敗時執行。

1$batch = Bus::batch([
2 // ...
3])->allowFailures(function (Batch $batch, $exception) {
4 // Handle individual job failures...
5})->dispatch();

重試失敗的批處理任務

為了方便起見,Laravel 提供了一個 queue:retry-batch Artisan 命令,允許您輕鬆重試給定批處理的所有失敗任務。此命令接受應重試其失敗任務的批處理的 UUID。

1php artisan queue:retry-batch 32dbc76c-4f82-4749-b610-a639fe0099b5

清理批處理

如果不清理,job_batches 表可以非常快地積累記錄。為了緩解這種情況,您應該 排程 queue:prune-batches Artisan 命令每天執行。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('queue:prune-batches')->daily();

預設情況下,所有超過 24 小時的已完成批處理都將被清理。您可以在呼叫命令時使用 hours 選項來確定保留批處理資料的時間。例如,以下命令將刪除所有在 48 小時前完成的批處理:

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('queue:prune-batches --hours=48')->daily();

有時,您的 job_batches 表可能會積累從未成功完成的批處理記錄,例如任務失敗且該任務從未成功重試的批處理。您可以指示 queue:prune-batches 命令使用 unfinished 選項來清理這些未完成的批處理記錄。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::command('queue:prune-batches --hours=48 --unfinished=72')->daily();

同樣,您的 job_batches 表也可能積累已取消批處理的批處理記錄。您可以指示 queue:prune-batches 命令使用 cancelled 選項來清理這些已取消的批處理記錄。

1use Illuminate\Support\Facades\Schedule;
2 
3Schedule::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 通訊:

1composer require aws/aws-sdk-php

然後,將 queue.batching.driver 配置選項的值設定為 dynamodb。此外,您應該在 batching 配置陣列中定義 keysecretregion 配置選項。這些選項將用於向 AWS 進行身份驗證。使用 dynamodb 驅動程式時,queue.batching.database 配置選項是不必要的。

1'batching' => [
2 'driver' => env('QUEUE_BATCHING_DRIVER', 'dynamodb'),
3 'key' => env('AWS_ACCESS_KEY_ID'),
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
6 'table' => 'job_batches',
7],

在 DynamoDB 中清理批處理

利用 DynamoDB 儲存任務批處理資訊時,用於清理儲存在關係資料庫中的批處理的常規清理命令將不起作用。相反,您可以利用 DynamoDB 的原生 TTL 功能 來自動刪除舊批處理的記錄。

如果您使用 ttl 屬性定義了 DynamoDB 表,則可以定義配置引數來指示 Laravel 如何清理批處理記錄。queue.batching.ttl_attribute 配置值定義了儲存 TTL 的屬性名稱,而 queue.batching.ttl 配置值定義了批處理記錄相對於記錄最後更新時間,在多少秒後可以從 DynamoDB 表中刪除。

1'batching' => [
2 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
3 'key' => env('AWS_ACCESS_KEY_ID'),
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
6 'table' => 'job_batches',
7 'ttl_attribute' => 'ttl',
8 'ttl' => 60 * 60 * 24 * 7, // 7 days...
9],

佇列閉包

除了將任務類分發到佇列外,您還可以分發一個閉包。這非常適合需要在當前請求週期之外執行的快速、簡單的任務。當將閉包分發到佇列時,閉包的程式碼內容會被加密簽名,以確保其在傳輸過程中不會被修改。

1use App\Models\Podcast;
2 
3$podcast = Podcast::find(1);
4 
5dispatch(function () use ($podcast) {
6 $podcast->publish();
7});

要為排隊的閉包分配名稱(佇列報告儀表盤可以使用該名稱,並且 queue:work 命令也會顯示該名稱),您可以使用 name 方法:

1dispatch(function () {
2 // ...
3})->name('Publish Podcast');

使用 catch 方法,您可以提供一個閉包,如果在用盡所有佇列 配置的重試嘗試 後,排隊的閉包未能成功完成,則應執行該閉包:

1use Throwable;
2 
3dispatch(function () use ($podcast) {
4 $podcast->publish();
5})->catch(function (Throwable $e) {
6 // This job has failed...
7});

由於 catch 回撥是由 Laravel 佇列序列化並在稍後執行的,因此不應在 catch 回撥中使用 $this 變數。

執行佇列工作程序

queue:work 命令

Laravel 包含一個 Artisan 命令,它將啟動佇列工作程序並在新任務被推送到佇列時處理它們。您可以使用 queue:work Artisan 命令執行該工作程序。請注意,一旦 queue:work 命令啟動,它將持續執行,直到被手動停止或您關閉終端。

1php artisan queue:work

要使 queue:work 程序在後臺永久執行,您應該使用程序監控器(例如 Supervisor)來確保佇列工作程序不會停止執行。

如果您希望在命令輸出中包含已處理的任務 ID、連線名稱和佇列名稱,可以在呼叫 queue:work 命令時包含 -v 標誌。

1php artisan queue:work -v

請記住,佇列工作程序是長期執行的程序,並將啟動的應用程式狀態儲存在記憶體中。因此,它們在啟動後不會注意到程式碼庫中的更改。因此,在您的部署過程中,請務必 重啟您的佇列工作程序。此外,請記住應用程式建立或修改的任何靜態狀態都不會在任務之間自動重置。

或者,您可以執行 queue:listen 命令。使用 queue:listen 命令時,無需在想要重新載入更新的程式碼或重置應用程式狀態時手動重啟工作程序;但是,此命令的效率明顯低於 queue:work 命令。

1php artisan queue:listen

執行多個佇列工作程序

要向佇列分配多個工作程序並併發處理任務,您只需啟動多個 queue:work 程序。這可以在本地透過終端中的多個選項卡完成,或者在生產環境中使用程序管理器的配置設定完成。使用 Supervisor 時,可以使用 numprocs 配置值。

指定連線和佇列

您還可以指定工作程序應利用哪個佇列連線。傳遞給 work 命令的連線名稱應對應於 config/queue.php 配置檔案中定義的連線之一。

1php artisan queue:work redis

預設情況下,queue:work 命令僅處理給定連線上預設佇列的任務。但是,您可以透過僅處理給定連線的特定佇列來進一步自定義佇列工作程序。例如,如果您的所有電子郵件都在 redis 佇列連線的 emails 佇列中處理,則可以發出以下命令來啟動僅處理該佇列的工作程序:

1php artisan queue:work redis --queue=emails

處理指定數量的任務

--once 選項可用於指示工作程序僅處理佇列中的單個任務:

1php artisan queue:work --once

--max-jobs 選項可用於指示工作程序處理給定數量的任務然後退出。當與 Supervisor 結合使用時,此選項可能很有用,這樣您的工作程序在處理給定數量的任務後會自動重啟,從而釋放它們可能積累的任何記憶體。

1php artisan queue:work --max-jobs=1000

處理所有排隊任務然後退出

--stop-when-empty 選項可用於指示工作程序處理所有任務然後優雅地退出。當您希望在佇列為空後關閉容器時,此選項在 Docker 容器內處理 Laravel 佇列時非常有用。

1php artisan queue:work --stop-when-empty

處理給定秒數的任務

--max-time 選項可用於指示工作程序處理任務給定的秒數,然後退出。當與 Supervisor 結合使用時,此選項可能很有用,這樣您的工作程序在處理給定時間的任務後會自動重啟,從而釋放它們可能積累的任何記憶體。

1# Process jobs for one hour and then exit...
2php artisan queue:work --max-time=3600

工作程序休眠時間

當佇列中有任務可用時,工作程序將持續處理任務,任務之間沒有延遲。但是,sleep 選項決定了如果沒有任務可用,工作程序將“休眠”多少秒。當然,在休眠時,工作程序不會處理任何新任務。

1php artisan queue:work --sleep=3

維護模式與佇列

當您的應用程式處於 維護模式 時,不會處理任何佇列任務。一旦應用程式退出維護模式,任務將照常處理。

要強制您的佇列工作程序即使在啟用了維護模式的情況下也處理任務,可以使用 --force 選項:

1php artisan queue:work --force

資源注意事項

守護程序佇列工作程式(Worker)在處理每個作業(Job)之前不會“重啟”框架。因此,在每個作業完成後,你應該釋放任何繁重的資源。例如,如果你正在使用 GD 庫進行影像處理,那麼在處理完影像後,你應該使用 imagedestroy 釋放記憶體。

佇列優先順序

有時你可能希望確定佇列的處理優先順序。例如,在 config/queue.php 配置檔案中,你可以將 redis 連線的預設 queue 設定為 low。但是,有時你可能希望將作業推送到 high 優先順序的佇列中,如下所示:

1dispatch((new Job)->onQueue('high'));

要啟動一個工作程式,以確保在繼續處理 low 佇列中的任何作業之前先處理完 high 佇列中的所有作業,請將以逗號分隔的佇列名稱列表傳遞給 work 命令:

1php artisan queue:work --queue=high,low

佇列工作程序與部署

由於佇列工作程式是長生命週期的程序,如果不重啟它們,它們將無法感知程式碼的變更。因此,部署使用佇列工作程式的應用程式最簡單的方法是在部署過程中重啟工作程式。你可以透過執行 queue:restart 命令來優雅地重啟所有工作程式:

1php artisan queue:restart

該命令將指示所有佇列工作程式在處理完當前作業後優雅地退出,從而確保不會丟失現有的作業。由於執行 queue:restart 命令時佇列工作程式會退出,因此你應該執行諸如 Supervisor 之類的程序管理器來自動重啟佇列工作程式。

佇列使用 快取 來儲存重啟訊號,因此在使用此功能之前,請確保已為應用程式正確配置了快取驅動程式。

任務過期與超時

作業過期

config/queue.php 配置檔案中,每個佇列連線都定義了一個 retry_after 選項。此選項指定了佇列連線在重試正在處理的作業之前應等待的秒數。例如,如果 retry_after 的值設定為 90,那麼如果作業處理了 90 秒而沒有被釋放或刪除,它將被重新放回佇列。通常,你應該將 retry_after 的值設定為你的作業完成處理所需的最長秒數。

唯一不包含 retry_after 值的佇列連線是 Amazon SQS。SQS 將根據在 AWS 控制檯中管理的 預設可見性超時 (Default Visibility Timeout) 來重試作業。

工作程式超時

queue:work Artisan 命令提供了一個 --timeout 選項。預設情況下,--timeout 的值為 60 秒。如果作業的處理時間超過了超時值指定的秒數,處理該作業的工作程式將退出並報錯。通常,工作程式會由伺服器上配置的程序管理器自動重啟。

1php artisan queue:work --timeout=60

retry_after 配置選項和 --timeout CLI 選項是不同的,但它們協同工作以確保作業不會丟失,並且每個作業僅被成功處理一次。

--timeout 的值應始終至少比你的 retry_after 配置值短幾秒。這將確保處理卡死作業的工作程式在作業被重試之前終止。如果你的 --timeout 選項比 retry_after 配置值長,你的作業可能會被處理兩次。

暫停和恢復佇列工作程序

有時你可能需要臨時阻止佇列工作程式處理新作業,而不必完全停止工作程式。例如,你可能希望在系統維護期間暫停作業處理。Laravel 提供了 queue:pausequeue:continue Artisan 命令來暫停和恢復佇列工作程式。

要暫停特定的佇列,請提供佇列連線名稱和佇列名稱:

1php artisan queue:pause database:default

在此示例中,database 是佇列連線名稱,default 是佇列名稱。一旦佇列被暫停,處理來自該佇列作業的任何工作程式將繼續完成其當前作業,但在佇列恢復之前不會獲取任何新作業。

要恢復處理暫停佇列上的作業,請使用 queue:continue 命令:

1php artisan queue:continue database:default

恢復佇列後,工作程式將立即開始處理來自該佇列的新作業。請注意,暫停佇列不會停止工作程式程序本身——它只會阻止工作程式從指定佇列處理新作業。

工作程式重啟和暫停訊號

預設情況下,佇列工作程式會在每次作業迭代時輪詢快取驅動程式以獲取重啟和暫停訊號。雖然這種輪詢對於響應 queue:restartqueue:pause 命令至關重要,但它確實會帶來少量的效能開銷。

如果你需要最佳化效能且不需要這些中斷功能,可以透過在 Queue 門面(Facade)上呼叫 withoutInterruptionPolling 方法來全域性停用此輪詢。這通常應該在 AppServiceProviderboot 方法中完成:

1use Illuminate\Support\Facades\Queue;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Queue::withoutInterruptionPolling();
9}

或者,你可以透過在 Illuminate\Queue\Worker 類上設定靜態的 $restartable$pausable 屬性,分別停用重啟或暫停輪詢:

1use Illuminate\Queue\Worker;
2 
3/**
4 * Bootstrap any application services.
5 */
6public function boot(): void
7{
8 Worker::$restartable = false;
9 Worker::$pausable = false;
10}

當停用中斷輪詢時,工作程式將不會響應 queue:restartqueue:pause 命令(取決於停用了哪些功能)。

Supervisor 配置

在生產環境中,你需要一種方法來保持 queue:work 程序執行。queue:work 程序可能會因各種原因停止執行,例如工作程式超時或執行了 queue:restart 命令。

因此,你需要配置一個程序監視器,它可以在 queue:work 程序退出時檢測到並自動重啟它們。此外,程序監視器允許你指定希望同時執行多少個 queue:work 程序。Supervisor 是 Linux 環境中常用的程序監視器,我們將在接下來的文件中討論如何配置它。

安裝 Supervisor

Supervisor 是 Linux 作業系統的一個程序監視器,如果你的 queue:work 程序失敗,它會自動重啟它們。要在 Ubuntu 上安裝 Supervisor,你可以使用以下命令:

1sudo apt-get install supervisor

如果自行配置和管理 Supervisor 對你來說太複雜,請考慮使用 Laravel Cloud,它提供了一個完全託管的平臺來執行 Laravel 佇列工作程式。

配置 Supervisor

Supervisor 配置檔案通常儲存在 /etc/supervisor/conf.d 目錄中。在此目錄中,你可以建立任意數量的配置檔案,以指示 Supervisor 如何監視你的程序。例如,讓我們建立一個 laravel-worker.conf 檔案來啟動和監視 queue:work 程序:

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

在此示例中,numprocs 指令將指示 Supervisor 執行八個 queue:work 程序並監視所有這些程序,如果它們失敗則自動重啟它們。你應該更改配置中的 command 指令以反映你所需的佇列連線和工作程式選項。

你應該確保 stopwaitsecs 的值大於你執行時間最長的作業所消耗的秒數。否則,Supervisor 可能會在作業完成處理之前將其殺死。

啟動 Supervisor

建立配置檔案後,你可以使用以下命令更新 Supervisor 配置並啟動程序:

1sudo supervisorctl reread
2 
3sudo supervisorctl update
4 
5sudo supervisorctl start "laravel-worker:*"

有關 Supervisor 的更多資訊,請參閱 Supervisor 文件

處理失敗的任務

有時你的佇列作業會失敗。別擔心,事情並不總是按計劃進行!Laravel 包含了一種便捷的方法來指定作業應嘗試的最大次數。當非同步作業超過此嘗試次數後,它將被插入到 failed_jobs 資料庫表中。失敗的同步分發作業不會儲存在此表中,其異常由應用程式直接處理。

建立 failed_jobs 表的遷移檔案通常已經存在於新的 Laravel 應用程式中。但是,如果你的應用程式不包含此表的遷移檔案,你可以使用 make:queue-failed-table 命令來建立它:

1php artisan make:queue-failed-table
2 
3php artisan migrate

執行佇列工作程式程序時,你可以使用 queue:work 命令上的 --tries 開關來指定作業應嘗試的最大次數。如果你沒有為 --tries 選項指定值,作業將只會嘗試一次,或者按照作業類中 Tries 屬性指定的次數進行嘗試。

1php artisan queue:work redis --tries=3

使用 --backoff 選項,你可以指定 Laravel 在遇到異常後重試作業前應等待的秒數。預設情況下,作業會立即被釋放回佇列以便再次嘗試:

1php artisan queue:work redis --tries=3 --backoff=3

如果你想針對每個作業配置 Laravel 在遇到異常後重試作業前應等待的秒數,可以在你的作業類上使用 Backoff 屬性:

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\Backoff;
6 
7#[Backoff(3)]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

如果你需要更復雜的邏輯來確定作業的退避(backoff)時間,可以在你的作業類上定義一個 backoff 方法:

1/**
2 * Calculate the number of seconds to wait before retrying the job.
3 */
4public function backoff(): int
5{
6 return 3;
7}

你可以透過定義退避值陣列來輕鬆配置“指數”退避。在此示例中,第一次重試的延遲為 1 秒,第二次為 5 秒,第三次為 10 秒,如果還有剩餘嘗試次數,則隨後的每次重試均為 10 秒:

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\Backoff;
6 
7#[Backoff([1, 5, 10])]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

清理失敗任務後的處理

當特定作業失敗時,你可能希望向使用者傳送提醒或撤銷該作業部分完成的操作。為此,你可以在你的作業類中定義一個 failed 方法。導致作業失敗的 Throwable 例項將被傳遞給 failed 方法:

1<?php
2 
3namespace App\Jobs;
4 
5use App\Models\Podcast;
6use App\Services\AudioProcessor;
7use Illuminate\Contracts\Queue\ShouldQueue;
8use Illuminate\Foundation\Queue\Queueable;
9use Throwable;
10 
11class ProcessPodcast implements ShouldQueue
12{
13 use Queueable;
14 
15 /**
16 * Create a new job instance.
17 */
18 public function __construct(
19 public Podcast $podcast,
20 ) {}
21 
22 /**
23 * Execute the job.
24 */
25 public function handle(AudioProcessor $processor): void
26 {
27 // Process uploaded podcast...
28 }
29 
30 /**
31 * Handle a job failure.
32 */
33 public function failed(?Throwable $exception): void
34 {
35 // Send user notification of failure, etc...
36 }
37}

在呼叫 failed 方法之前,會例項化該作業的一個新例項;因此,在 handle 方法中可能發生的任何類屬性修改都將丟失。

失敗的作業不一定是指遇到了未捕獲異常的作業。當作業用盡了所有允許的嘗試次數時,它也可能被視為失敗。這些嘗試次數可以透過多種方式消耗:

  • 任務超時。
  • 任務在執行期間遇到未處理的異常。
  • 作業被手動或透過中介軟體釋放回佇列。

如果最後一次嘗試因作業執行期間丟擲的異常而失敗,該異常將被傳遞給作業的 failed 方法。但是,如果作業是因為達到了允許的最大嘗試次數而失敗,則 $exception 將是 Illuminate\Queue\MaxAttemptsExceededException 的例項。同樣,如果作業因超過配置的超時時間而失敗,則 $exception 將是 Illuminate\Queue\TimeoutExceededException 的例項。

重試失敗任務

要檢視已插入到 failed_jobs 資料庫表中的所有失敗作業,可以使用 queue:failed Artisan 命令:

1php artisan queue:failed

queue:failed 命令將列出作業 ID、連線、佇列、失敗時間以及有關該作業的其他資訊。作業 ID 可用於重試失敗的作業。例如,要重試 ID 為 ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 的失敗作業,請執行以下命令:

1php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece

如有必要,你可以將多個 ID 傳遞給該命令:

1php artisan queue:retry ce7bb17c-cdd8-41f0-a8ec-7b4fef4e5ece 91401d2c-0784-4f43-824c-34f94a33c24d

你還可以重試特定佇列的所有失敗作業:

1php artisan queue:retry --queue=name

要重試所有失敗的作業,請執行 queue:retry 命令並將 all 作為 ID 傳遞:

1php artisan queue:retry all

如果你想刪除一個失敗的作業,可以使用 queue:forget 命令:

1php artisan queue:forget 91401d2c-0784-4f43-824c-34f94a33c24d

使用 Horizon 時,你應該使用 horizon:forget 命令而不是 queue:forget 命令來刪除失敗的作業。

要從 failed_jobs 表中刪除所有失敗的作業,可以使用 queue:flush 命令:

1php artisan queue:flush

queue:flush 命令會刪除佇列中的所有失敗作業記錄,無論失敗作業是什麼時候發生的。你可以使用 --hours 選項僅刪除在一定小時數前或更早時間失敗的作業:

1php artisan queue:flush --hours=48

忽略缺失模型

當將 Eloquent 模型注入到作業中時,模型會在放入佇列之前自動序列化,並在作業被處理時從資料庫中重新獲取。但是,如果模型在作業等待工作程式處理時被刪除,你的作業可能會以 ModelNotFoundException 失敗。

為了方便起見,你可以選擇使用作業類上的 DeleteWhenMissingModels 屬性自動刪除缺少模型的作業。當存在此屬性時,Laravel 將靜默丟棄該作業而不引發異常:

1<?php
2 
3namespace App\Jobs;
4 
5use Illuminate\Queue\Attributes\DeleteWhenMissingModels;
6 
7#[DeleteWhenMissingModels]
8class ProcessPodcast implements ShouldQueue
9{
10 // ...
11}

清理失敗任務

你可以透過呼叫 queue:prune-failed Artisan 命令來清理應用程式 failed_jobs 表中的記錄:

1php artisan queue:prune-failed

預設情況下,所有超過 24 小時的失敗作業記錄都將被清理。如果你為命令提供了 --hours 選項,則僅保留過去 N 小時內插入的失敗作業記錄。例如,以下命令將刪除所有在 48 小時前插入的失敗作業記錄:

1php 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 通訊:

1composer require aws/aws-sdk-php

接下來,將 queue.failed.driver 配置選項的值設定為 dynamodb。此外,你應該在失敗作業配置陣列中定義 keysecretregion 配置選項。這些選項將用於向 AWS 進行身份驗證。使用 dynamodb 驅動程式時,queue.failed.database 配置選項是不必要的。

1'failed' => [
2 'driver' => env('QUEUE_FAILED_DRIVER', 'dynamodb'),
3 'key' => env('AWS_ACCESS_KEY_ID'),
4 'secret' => env('AWS_SECRET_ACCESS_KEY'),
5 'region' => env('AWS_DEFAULT_REGION', 'us-east-1'),
6 'table' => 'failed_jobs',
7],

停用失敗任務儲存

你可以透過將 queue.failed.driver 配置選項的值設定為 null,來指示 Laravel 丟棄失敗的作業而不儲存它們。通常,這可以透過 QUEUE_FAILED_DRIVER 環境變數來實現。

1QUEUE_FAILED_DRIVER=null

失敗任務事件

如果你想註冊一個在作業失敗時呼叫的事件監聽器,可以使用 Queue 門面的 failing 方法。例如,我們可以從 Laravel 自帶的 AppServiceProviderboot 方法中將閉包附加到此事件:

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\Facades\Queue;
6use Illuminate\Support\ServiceProvider;
7use Illuminate\Queue\Events\JobFailed;
8 
9class AppServiceProvider extends ServiceProvider
10{
11 /**
12 * Register any application services.
13 */
14 public function register(): void
15 {
16 // ...
17 }
18 
19 /**
20 * Bootstrap any application services.
21 */
22 public function boot(): void
23 {
24 Queue::failing(function (JobFailed $event) {
25 // $event->connectionName
26 // $event->job
27 // $event->exception
28 });
29 }
30}

從佇列中清除任務

使用 Horizon 時,你應該使用 horizon:clear 命令而不是 queue:clear 命令來清除佇列中的作業。

如果你想從預設連線的預設佇列中刪除所有作業,可以使用 queue:clear Artisan 命令:

1php artisan queue:clear

你還可以提供 connection 引數和 queue 選項,以從特定的連線和佇列中刪除作業:

1php artisan queue:clear redis --queue=emails

從佇列中清除作業僅適用於 SQS、Redis 和資料庫佇列驅動程式。此外,SQS 訊息刪除過程最多需要 60 秒,因此在你清除佇列後的 60 秒內傳送到 SQS 佇列的作業也可能會被刪除。

監控佇列

如果你的佇列突然接收到大量作業,它可能會不堪重負,導致作業完成的等待時間變長。如果需要,當佇列作業計數超過指定閾值時,Laravel 可以向你發出警報。

首先,你應該安排 queue:monitor 命令每分鐘執行一次。該命令接受你希望監視的佇列名稱以及你想要的作業計數閾值:

1php artisan queue:monitor redis:default,redis:deployments --max=100

僅安排此命令不足以觸發通知來提醒你佇列過載。當命令遇到作業計數超過閾值的佇列時,將分發一個 Illuminate\Queue\Events\QueueBusy 事件。你可以在應用程式的 AppServiceProvider 中監聽此事件,以便向你或你的開發團隊傳送通知:

1use App\Notifications\QueueHasLongWaitTime;
2use Illuminate\Queue\Events\QueueBusy;
3use Illuminate\Support\Facades\Event;
4use Illuminate\Support\Facades\Notification;
5 
6/**
7 * Bootstrap any application services.
8 */
9public function boot(): void
10{
11 Event::listen(function (QueueBusy $event) {
12 Notification::route('mail', '[email protected]')
13 ->notify(new QueueHasLongWaitTime(
14 $event->connectionName,
15 $event->queue,
16 $event->size
17 ));
18 });
19}

測試

在測試分發作業的程式碼時,你可能希望指示 Laravel 不要實際執行作業,因為作業的程式碼可以直接測試,並與分發它的程式碼分開測試。當然,要測試作業本身,你可以在測試中例項化一個作業例項並直接呼叫 handle 方法。

你可以使用 Queue 門面的 fake 方法來防止佇列作業被實際推送到佇列。呼叫 Queue 門面的 fake 方法後,你可以斷言應用程式嘗試將作業推送到佇列:

1<?php
2 
3use App\Jobs\AnotherJob;
4use App\Jobs\ShipOrder;
5use Illuminate\Support\Facades\Queue;
6 
7test('orders can be shipped', function () {
8 Queue::fake();
9 
10 // Perform order shipping...
11 
12 // Assert that no jobs were pushed...
13 Queue::assertNothingPushed();
14 
15 // Assert a job was pushed to a given queue...
16 Queue::assertPushedOn('queue-name', ShipOrder::class);
17 
18 // Assert a job was pushed
19 Queue::assertPushed(ShipOrder::class);
20 
21 // Assert a job was pushed twice...
22 Queue::assertPushedTimes(ShipOrder::class, 2);
23 
24 // Assert a job was not pushed...
25 Queue::assertNotPushed(AnotherJob::class);
26 
27 // Assert that a closure was pushed to the queue...
28 Queue::assertClosurePushed();
29 
30 // Assert that a closure was not pushed...
31 Queue::assertClosureNotPushed();
32 
33 // Assert the total number of jobs that were pushed...
34 Queue::assertCount(3);
35});
1<?php
2 
3namespace Tests\Feature;
4 
5use App\Jobs\AnotherJob;
6use App\Jobs\ShipOrder;
7use Illuminate\Support\Facades\Queue;
8use Tests\TestCase;
9 
10class ExampleTest extends TestCase
11{
12 public function test_orders_can_be_shipped(): void
13 {
14 Queue::fake();
15 
16 // Perform order shipping...
17 
18 // Assert that no jobs were pushed...
19 Queue::assertNothingPushed();
20 
21 // Assert a job was pushed to a given queue...
22 Queue::assertPushedOn('queue-name', ShipOrder::class);
23 
24 // Assert a job was pushed
25 Queue::assertPushed(ShipOrder::class);
26 
27 // Assert a job was pushed twice...
28 Queue::assertPushedTimes(ShipOrder::class, 2);
29 
30 // Assert a job was not pushed...
31 Queue::assertNotPushed(AnotherJob::class);
32 
33 // Assert that a closure was pushed to the queue...
34 Queue::assertClosurePushed();
35 
36 // Assert that a closure was not pushed...
37 Queue::assertClosureNotPushed();
38 
39 // Assert the total number of jobs that were pushed...
40 Queue::assertCount(3);
41 }
42}

你可以將閉包傳遞給 assertPushedassertNotPushedassertClosurePushedassertClosureNotPushed 方法,以斷言推送了一個透過特定“真值測試”的作業。如果推送了至少一個透過該測試的作業,則斷言將成功:

1use Illuminate\Queue\CallQueuedClosure;
2 
3Queue::assertPushed(function (ShipOrder $job) use ($order) {
4 return $job->order->id === $order->id;
5});
6 
7Queue::assertClosurePushed(function (CallQueuedClosure $job) {
8 return $job->name === 'validate-order';
9});

偽造部分任務

如果你只需要偽造特定的作業,同時允許其他作業正常執行,可以將應該偽造的作業類名傳遞給 fake 方法:

1test('orders can be shipped', function () {
2 Queue::fake([
3 ShipOrder::class,
4 ]);
5 
6 // Perform order shipping...
7 
8 // Assert a job was pushed twice...
9 Queue::assertPushedTimes(ShipOrder::class, 2);
10});
1public function test_orders_can_be_shipped(): void
2{
3 Queue::fake([
4 ShipOrder::class,
5 ]);
6 
7 // Perform order shipping...
8 
9 // Assert a job was pushed twice...
10 Queue::assertPushedTimes(ShipOrder::class, 2);
11}

你可以使用 except 方法偽造除一組指定作業之外的所有作業:

1Queue::fake()->except([
2 ShipOrder::class,
3]);

測試任務鏈

要測試作業鏈(Job Chaining),你需要利用 Bus 門面的偽造功能。Bus 門面的 assertChained 方法可用於斷言一個作業鏈已被分發。assertChained 方法接受一個鏈式作業陣列作為其第一個引數:

1use App\Jobs\RecordShipment;
2use App\Jobs\ShipOrder;
3use App\Jobs\UpdateInventory;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::fake();
7 
8// ...
9 
10Bus::assertChained([
11 ShipOrder::class,
12 RecordShipment::class,
13 UpdateInventory::class
14]);

如上例所示,鏈式作業陣列可以是一組作業的類名。但是,你也可以提供一組實際的作業例項。這樣做時,Laravel 將確保作業例項屬於同一類,並具有與應用程式分發的鏈式作業相同的屬性值。

1Bus::assertChained([
2 new ShipOrder,
3 new RecordShipment,
4 new UpdateInventory,
5]);

你可以使用 assertDispatchedWithoutChain 方法來斷言一個作業在沒有作業鏈的情況下被推送。

1Bus::assertDispatchedWithoutChain(ShipOrder::class);

測試鏈式修改

如果鏈式作業向現有鏈中預置或追加了作業,你可以使用作業的 assertHasChain 方法來斷言該作業具有預期的剩餘作業鏈:

1$job = new ProcessPodcast;
2 
3$job->handle();
4 
5$job->assertHasChain([
6 new TranscribePodcast,
7 new OptimizePodcast,
8 new ReleasePodcast,
9]);

assertDoesntHaveChain 方法可用於斷言作業的剩餘鏈為空。

1$job->assertDoesntHaveChain();

測試鏈式批處理

如果你的作業鏈包含一組作業批處理,你可以透過在鏈斷言中插入 Bus::chainedBatch 定義來斷言該鏈式批處理符合你的預期:

1use App\Jobs\ShipOrder;
2use App\Jobs\UpdateInventory;
3use Illuminate\Bus\PendingBatch;
4use Illuminate\Support\Facades\Bus;
5 
6Bus::assertChained([
7 new ShipOrder,
8 Bus::chainedBatch(function (PendingBatch $batch) {
9 return $batch->jobs->count() === 3;
10 }),
11 new UpdateInventory,
12]);

測試任務批處理

Bus 門面的 assertBatched 方法可用於斷言一個作業批處理已被分發。傳遞給 assertBatched 方法的閉包接收一個 Illuminate\Bus\PendingBatch 例項,該例項可用於檢查批處理中的作業:

1use Illuminate\Bus\PendingBatch;
2use Illuminate\Support\Facades\Bus;
3 
4Bus::fake();
5 
6// ...
7 
8Bus::assertBatched(function (PendingBatch $batch) {
9 return $batch->name == 'Import CSV' &&
10 $batch->jobs->count() === 10;
11});

hasJobs 方法可用於掛起的批處理,以驗證批處理是否包含預期的作業。該方法接受一組作業例項、類名或閉包:

1Bus::assertBatched(function (PendingBatch $batch) {
2 return $batch->hasJobs([
3 new ProcessCsvRow(row: 1),
4 new ProcessCsvRow(row: 2),
5 new ProcessCsvRow(row: 3),
6 ]);
7});

使用閉包時,閉包將接收作業例項。預期的作業型別將從閉包的型別提示中推斷出來。

1Bus::assertBatched(function (PendingBatch $batch) {
2 return $batch->hasJobs([
3 fn (ProcessCsvRow $job) => $job->row === 1,
4 fn (ProcessCsvRow $job) => $job->row === 2,
5 fn (ProcessCsvRow $job) => $job->row === 3,
6 ]);
7});

你可以使用 assertBatchCount 方法來斷言分發了指定數量的批處理。

1Bus::assertBatchCount(3);

你可以使用 assertNothingBatched 來斷言沒有分發任何批處理。

1Bus::assertNothingBatched();

測試作業/批處理互動

此外,你有時可能需要測試單個作業與其底層批處理的互動。例如,你可能需要測試作業是否取消了其批處理的進一步處理。為此,你需要透過 withFakeBatch 方法為作業分配一個偽批處理。withFakeBatch 方法返回一個包含作業例項和偽批處理的元組:

1[$job, $batch] = (new ShipOrder)->withFakeBatch();
2 
3$job->handle();
4 
5$this->assertTrue($batch->cancelled());
6$this->assertEmpty($batch->added);

測試任務/佇列互動

有時,你可能需要測試佇列作業是否將自己釋放回佇列。或者,你可能需要測試作業是否刪除了自己。你可以透過例項化作業並呼叫 withFakeQueueInteractions 方法來測試這些佇列互動。

一旦作業的佇列互動被偽造,你就可以在作業上呼叫 handle 方法。呼叫作業後,可以使用各種斷言方法來驗證作業的佇列互動:

1use App\Exceptions\CorruptedAudioException;
2use App\Jobs\ProcessPodcast;
3 
4$job = (new ProcessPodcast)->withFakeQueueInteractions();
5 
6$job->handle();
7 
8$job->assertReleased(delay: 30);
9$job->assertDeleted();
10$job->assertNotDeleted();
11$job->assertFailed();
12$job->assertFailedWith(CorruptedAudioException::class);
13$job->assertNotFailed();

任務事件

使用 Queue 門面上的 beforeafter 方法,你可以指定在處理佇列作業之前或之後執行的回撥。這些回撥是執行額外日誌記錄或為儀表板增加統計資料的絕佳機會。通常,你應該從服務提供者boot 方法中呼叫這些方法。例如,我們可以使用 Laravel 自帶的 AppServiceProvider

1<?php
2 
3namespace App\Providers;
4 
5use Illuminate\Support\Facades\Queue;
6use Illuminate\Support\ServiceProvider;
7use Illuminate\Queue\Events\JobProcessed;
8use Illuminate\Queue\Events\JobProcessing;
9 
10class AppServiceProvider extends ServiceProvider
11{
12 /**
13 * Register any application services.
14 */
15 public function register(): void
16 {
17 // ...
18 }
19 
20 /**
21 * Bootstrap any application services.
22 */
23 public function boot(): void
24 {
25 Queue::before(function (JobProcessing $event) {
26 // $event->connectionName
27 // $event->job
28 // $event->job->payload()
29 });
30 
31 Queue::after(function (JobProcessed $event) {
32 // $event->connectionName
33 // $event->job
34 // $event->job->payload()
35 });
36 }
37}

使用 Queue 門面上的 looping 方法,你可以指定在工作程式嘗試從佇列中獲取作業之前執行的回撥。例如,你可能註冊一個閉包來回滾之前失敗的作業留下的任何未關閉事務:

1use Illuminate\Support\Facades\DB;
2use Illuminate\Support\Facades\Queue;
3 
4Queue::looping(function () {
5 while (DB::transactionLevel() > 0) {
6 DB::rollBack();
7 }
8});