技術 6 分鐘閱讀
Laravel Queue 與背景任務:讓使用者不再等待
前言
使用者點了「送出」按鈕後要等 10 秒才看到結果——這種體驗誰都受不了。很多耗時的操作(寄信、圖片處理、PDF 生成)其實不需要讓使用者等待,放到背景處理就好。Laravel Queue 就是解決這個問題的利器。
Queue 的基本概念
Queue(佇列)就像一個待辦清單:
- 你把要做的事(Job)丟進佇列
- Worker 在背景一個一個把事情做完
- 使用者不需要等待
設定 Queue Driver
# .env
QUEUE_CONNECTION=redis
Redis 是最推薦的選擇。開發環境如果沒有 Redis,可以用 database:
QUEUE_CONNECTION=database
如果用 database driver,需要建立 jobs 表:
php artisan queue:table
php artisan migrate
建立 Job
php artisan make:job SendWelcomeEmail
// app/Jobs/SendWelcomeEmail.php
class SendWelcomeEmail implements ShouldQueue
{
use Queueable;
public function __construct(
public User $user
) {}
public function handle(): void
{
Mail::to($this->user->email)->send(
new WelcomeMail($this->user)
);
}
}
派發 Job
// 在 Controller 中
public function register(Request $request)
{
$user = User::create($request->validated());
// 把寄信任務丟到佇列
SendWelcomeEmail::dispatch($user);
return response()->json([
'message' => '註冊成功!歡迎信將在稍後寄出。',
'user' => $user,
]);
}
使用者立即看到「註冊成功」,不需要等信寄完。
實際應用場景
圖片處理
class ProcessProductImage implements ShouldQueue
{
use Queueable;
public function __construct(
public Product $product,
public string $imagePath
) {}
public function handle(): void
{
// 產生不同尺寸的縮圖
$sizes = [
'thumb' => [150, 150],
'medium' => [600, 600],
'large' => [1200, 1200],
];
foreach ($sizes as $name => [$width, $height]) {
$resized = Image::make(storage_path("app/{$this->imagePath}"))
->fit($width, $height)
->encode('webp', 80);
$filename = pathinfo($this->imagePath, PATHINFO_FILENAME);
Storage::put("products/{$name}/{$filename}.webp", $resized);
}
// 更新資料庫
$this->product->update([
'images_processed' => true,
]);
}
}
PDF 報表生成
class GenerateMonthlyReport implements ShouldQueue
{
use Queueable;
public function __construct(
public int $year,
public int $month,
public User $requestedBy
) {}
public function handle(): void
{
$orders = Order::whereYear('created_at', $this->year)
->whereMonth('created_at', $this->month)
->with('items')
->get();
$pdf = Pdf::loadView('reports.monthly', [
'orders' => $orders,
'year' => $this->year,
'month' => $this->month,
]);
$filename = "report-{$this->year}-{$this->month}.pdf";
Storage::put("reports/{$filename}", $pdf->output());
// 通知使用者報表已生成
$this->requestedBy->notify(
new ReportReadyNotification($filename)
);
}
}
第三方 API 同步
class SyncInventoryToERP implements ShouldQueue
{
use Queueable;
public int $tries = 3;
public int $backoff = 60; // 失敗後等 60 秒再重試
public function __construct(
public Product $product
) {}
public function handle(): void
{
$response = Http::timeout(30)->post('https://erp.example.com/api/inventory', [
'sku' => $this->product->sku,
'quantity' => $this->product->stock,
'price' => $this->product->price,
]);
if (!$response->successful()) {
throw new \Exception("ERP 同步失敗:{$response->status()}");
}
$this->product->update(['erp_synced_at' => now()]);
}
}
錯誤處理
重試機制
class ImportProducts implements ShouldQueue
{
use Queueable;
public int $tries = 3; // 最多重試 3 次
public int $maxExceptions = 2; // 最多允許 2 次例外
public int $timeout = 120; // 超時 120 秒
// 漸進式延遲重試
public function backoff(): array
{
return [10, 30, 60]; // 10秒、30秒、60秒
}
public function handle(): void
{
// 處理邏輯
}
// Job 最終失敗時的處理
public function failed(\Throwable $exception): void
{
Log::error('商品匯入失敗', [
'error' => $exception->getMessage(),
]);
// 通知管理員
Notification::route('mail', 'admin@example.com')
->notify(new JobFailedNotification($exception));
}
}
手動失敗處理
public function handle(): void
{
$data = $this->fetchExternalData();
if (empty($data)) {
// 手動標記失敗,不再重試
$this->fail(new \Exception('外部資料為空'));
return;
}
// 繼續處理
}
Job Chain 與 Batch
Job Chain:依序執行
Bus::chain([
new ValidateImportFile($file),
new ImportProducts($file),
new SendImportCompleteNotification($user),
])->dispatch();
Job Batch:並行執行
$jobs = $products->map(function ($product) {
return new UpdateProductPrice($product);
});
Bus::batch($jobs)
->then(function (Batch $batch) {
// 全部完成
Log::info("批次更新完成,共 {$batch->totalJobs} 個任務");
})
->catch(function (Batch $batch, \Throwable $e) {
// 有任務失敗
Log::error("批次更新有錯誤:{$e->getMessage()}");
})
->dispatch();
Queue 指定佇列
// 把不同優先度的任務放到不同佇列
SendWelcomeEmail::dispatch($user)->onQueue('emails');
ProcessProductImage::dispatch($product, $path)->onQueue('images');
GenerateMonthlyReport::dispatch($year, $month, $user)->onQueue('reports');
啟動 Worker 時指定處理順序:
# 先處理 emails,再處理 images 和 reports
php artisan queue:work --queue=emails,images,reports
部署與監控
Supervisor 設定
[program:laravel-worker]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/html/artisan queue:work redis --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=2
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/worker.log
Laravel Horizon
如果使用 Redis,強烈推薦安裝 Horizon:
composer require laravel/horizon
php artisan horizon:install
php artisan horizon
Horizon 提供漂亮的儀表板,可以監控佇列的狀態、失敗的任務、處理速度等。
結語
Queue 是 Laravel 最強大的功能之一。善用它可以大幅提升使用者體驗,也能讓系統架構更靈活。從最簡單的寄信開始用,慢慢你會發現越來越多場景可以用 Queue 來處理。
#Laravel #Queue #Redis #後端開發