Laravelキューと非同期タスク
キューはLaravelの「バックグラウンドチーム」です—時間のかかるタスクをキューに回して非同期処理することで, ユーザーは待機する必要がなく, リクエストが即座に返されます。
1. 学ぶこと
- キュードライバー設定:sync/database/redis/sqsの比較
- ジョブの作成とディスパッチ:dispatch() / Queue::push()
- 失敗時のリトライ機構:
tries/backoff/failed_jobsテーブル - キューの優先度, レイテンシ, バッチタスク:Batch/Bused
- Supervisorによる
queue:workプロセスの監視
2. ユーザーのストーリー
(1) 痛み:レポートのエクスポートがシステム全体をフリーズさせる
BobがShopMetrics管理パネルで「月次レポートをエクスポート」をクリックしました—10万件の注文レコードを含むCSVファイルの生成に30秒かかり, その間ページはずっとロード中のまま, Aliceも同時にダッシュボードにアクセスして動作が遅くなりました。さらに悪いことに, Bobが5つのレポートエクスポートを同時にトリガーし, PHPプロセスプールを使い果たしてサイト全体が502エラーを返しました。
(2) キューによる解決策
キューは時間のかかるタスクをバックグラウンドに送ります—ユーザーが「エクスポート」をクリックすると「レポートを生成中」というメッセージがすぐに表示され, ジョブはバックグラウンドでゆっくり実行されます。完了するとダウンロードリンク付きのメールが送信されます。
// 同期 — 30秒ブロック
$csv = ReportService::generateMonthlyReport($tenant);
// 非同期 — 即座に返却, ジョブはバックグラウンドで実行
GenerateReportJob::dispatch($tenant, 'monthly');
// ユーザーに表示:「レポートを生成中です。準備ができたらメールでお知らせします。」
(3) 成果
Bobがキューを実装した後, レポートエクスポートリクエストは100ミリ秒以内に返されるようになり, バックグラウンドジョブはスムーズに実行され, Aliceのダッシュボードもフリーズしなくなりました。
3. キュードライバー設定
(1) ドライバーの比較
| ドライバー | 永続性 | パフォーマンス | 適用用途 | コスト |
|---|---|---|---|---|
sync |
❌ 即時実行 | 最速 | 開発/テスト | 無料 |
database |
✅ DBテーブル | 遅い | 小規模プロジェクト | 無料 |
redis |
✅ メモリ | 高速 | 本番環境 | Redis |
sqs |
✅ AWS | 高い | 大規模 | 従量課金 |
beanstalkd |
✅ 専用 | 高速 | 中規模 | 無料 |
(2) 設定
# .env
QUEUE_CONNECTION=redis
# Redis接続
REDIS_HOST=127.0.0.1
REDIS_PASSWORD=null
REDIS_PORT=6379
// config/queue.php
'connections' => [
'database' => [
'driver' => 'database',
'table' => 'jobs',
'queue' => 'default',
'retry_after' => 90,
],
'redis' => [
'driver' => 'redis',
'connection' => 'default',
'queue' => '{default}',
'retry_after' => 90,
'block_for' => null,
],
],
(3) チームリストの作成
# databaseドライバーの場合
php artisan queue:table
php artisan queue:failed-table
php artisan migrate
# 作成されるもの:jobsテーブル + failed_jobsテーブル
(1) ▶ サンプル:ShopMetricsキュー設定
# .env — 開発環境
QUEUE_CONNECTION=database
# .env — 本番環境
QUEUE_CONNECTION=redis
REDIS_HOST=redis.shopmetrics.internal
REDIS_PORT=6379
# キューテーブルの作成 (databaseドライバー)
php artisan queue:table
php artisan queue:failed-table
php artisan queue:batches-table
php artisan migrate
出力:
# コマンド実行成功
4. ジョブの作成とディスパッチ
(1) ジョブの作成
php artisan make:job GenerateReportJob
# 作成される:app/Jobs/GenerateReportJob.php
(2) ジョブの定義
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = 60;
public bool $deleteWhenMissingModels = true;
public function __construct(
public Tenant $tenant,
public string $reportType,
public string $format = 'csv',
) {}
public function handle(
ReportService $reportService,
Mailer $mailer,
): void {
$path = $reportService->generate(
$this->tenant,
$this->reportType,
$this->format,
);
$url = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));
$this->tenant->users->each(function ($user) use ($url) {
$mailer->to($user)->send(new ReportReadyNotification($url, $this->reportType));
});
}
public function failed(\Throwable $exception): void
{
Log::error('Report generation failed', [
'tenant_id' => $this->tenant->id,
'report_type' => $this->reportType,
'error' => $exception->getMessage(),
]);
}
}
(3) ジョブのディスパッチ
// 基本的なディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly');
// 遅延ディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly')
->delay(now()->addMinutes(5));
// 特定のキューにディスパッチ
GenerateReportJob::dispatch($tenant, 'monthly')
->onQueue('reports');
// 条件を満たす場合のみディスパッチ
GenerateReportJob::dispatchIf($tenant->subscription?->isActive(), $tenant, 'monthly');
// レスポンス送信後にディスパッチ
GenerateReportJob::dispatchAfterResponse($tenant, 'monthly');
| ディスパッチ方法 | 説明 | 適用シナリオ |
|---|---|---|
dispatch() |
キューに登録 | 一般的な非同期タスク |
dispatchSync() |
同期実行 | 即座に完了する必要がある場合 |
dispatchAfterResponse() |
レスポンス後実行 | 軽量タスク |
delay() |
遅延実行 | スケジュールタスク |
onQueue() |
指定キュー | 優先度別 |
(1) ▶ サンプル:ShopMetrics注文処理ジョブ
// app/Jobs/ProcessOrderJob.php
class ProcessOrderJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public int $backoff = [30, 60, 120];
public function __construct(public Order $order) {}
public function handle(
OrderService $orderService,
PaymentGateway $payment,
): void {
// 支払いの引き落とし
$payment->charge($this->order);
// 在庫の更新
$orderService->deductInventory($this->order);
// イベントのブロードキャスト
event(new OrderPlaced($this->order));
// 確認メールの送信
$this->order->user->notify(new OrderConfirmationNotification($this->order));
}
public function failed(\Throwable $exception): void
{
$this->order->update(['status' => 'failed']);
$this->order->user->notify(new OrderFailedNotification($this->order));
}
}
出力:
// 実行成功
5. 失敗時のリトライ機構
(1) リトライ設定
// ジョブクラス内
public int $tries = 3; // 最大リトライ回数
public int $backoff = 60; // リトライ間隔 (秒)
public int $timeout = 120; // 1回あたりの最大秒数
// または指数バックオフ
public array $backoff = [30, 60, 120]; // 30秒, 60秒, 120秒
// または動的バックオフ
public function backoff(): int
{
return 30 * $this->attempts();
}
(2) エラー処理
# 失敗したジョブの確認
php artisan queue:failed
# 特定の失敗ジョブのリトライ
php artisan queue:retry 5
# すべての失敗ジョブのリトライ
php artisan queue:retry all
# 失敗ジョブの削除
php artisan queue:forget 5
# すべての失敗ジョブのクリア
php artisan queue:flush
(3) キュータスクのライフサイクル
flowchart LR
A[ジョブディスパッチ] --> B[キュー]
B --> C[ワーカーが取得]
C --> D{成功?}
D -->|はい| E[ジョブ完了]
D -->|いいえ| F{attempts < tries?}
F -->|はい| G[バックオフ + リトライ]
G --> B
F -->|いいえ| H[failed_jobsテーブル]
H --> I[手動リトライ / クリア]
(1) ▶ サンプル:ShopMetrics失敗リトライ設定
// app/Jobs/SendWebhookJob.php
class SendWebhookJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 5;
public array $backoff = [10, 30, 60, 120, 300];
public int $timeout = 30;
public function __construct(
public Shop $shop,
public array $payload,
) {}
public function handle(): void
{
$response = Http::timeout($this->timeout)
->post($this->shop->webhook_url, $this->payload);
if (!$response->successful()) {
$this->release($this->backoff[$this->attempts() - 1] ?? 60);
}
}
public function failed(\Throwable $exception): void
{
$this->shop->tenant->users->each(function ($user) {
$user->notify(new WebhookFailedNotification($this->shop));
});
}
}
出力:
// 実行成功
6. バッチタスク
(1) バッチの作成
php artisan queue:batches-table
php artisan migrate
// app/Jobs/ProcessTenantAnalyticsJob.php
class ProcessTenantAnalyticsJob implements ShouldQueue
{
use Batchable;
public function __construct(public Tenant $tenant) {}
public function handle(): void
{
if ($this->batch()->cancelled()) {
return;
}
AnalyticsService::computeForTenant($this->tenant);
}
}
(2) バッチのディスパッチ
use Illuminate\Bus\Batch;
use Illuminate\Support\Facades\Bus;
$batch = Bus::batch(
Tenant::active()->get()->map(
fn ($tenant) => new ProcessTenantAnalyticsJob($tenant)
)
)->then(function (Batch $batch) {
// すべてのジョブが正常に完了
Log::info("Batch {$batch->id} completed: {$batch->totalJobs} tenants processed.");
})->catch(function (Batch $batch, \Throwable $e) {
// 最初のジョブ失敗
Log::error("Batch {$batch->id} failed: {$e->getMessage()}");
})->finally(function (Batch $batch) {
// 常に実行 (成功・失敗に関わらず)
Cache::forget('analytics:computing');
})->name('Process Monthly Analytics')
->onQueue('analytics')
->dispatch();
(3) バッチ管理
// バッチの進捗確認
$batch = Bus::findBatch($batchId);
$batch->progress(); // 0-100
$batch->processedJobs();
$batch->totalJobs();
$batch->failedJobs();
$batch->finished();
// バッチのキャンセル
$batch->cancel();
| メソッド | 説明 |
|---|---|
then() |
全コールバック成功 |
catch() |
最初の失敗時コールバック |
finally() |
完了時コールバック (成功・失敗に関わらず) |
progress() |
完了率 |
cancel() |
残りのタスクをキャンセル |
(1) ▶ サンプル:ShopMetrics月次分析バッチタスク
// app/Console/Commands/ProcessMonthlyAnalytics.php
class ProcessMonthlyAnalytics extends Command
{
protected $signature = 'analytics:process-monthly';
public function handle(): int
{
$tenants = Tenant::active()->get();
$this->info("Processing analytics for {$tenants->count()} tenants...");
Bus::batch(
$tenants->map(fn ($t) => new ProcessTenantAnalyticsJob($t))
)->then(function (Batch $batch) {
$this->info("All {$batch->totalJobs} tenants processed.");
})->catch(function (Batch $batch, \Throwable $e) {
$this->error("Batch failed: {$e->getMessage()}");
})->name('Monthly Analytics')
->onQueue('analytics')
->allowFailures()
->dispatch();
return self::SUCCESS;
}
}
出力:
// 実行成功
7. Supervisorデーモン
(1) Supervisorのインストール
# Ubuntu/Debian
sudo apt-get install supervisor
# 設定ファイルの作成
sudo nano /etc/supervisor/conf.d/shopmetrics-worker.conf
(2) Supervisor設定
[program:shopmetrics-worker-default]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=default --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-default.log
stopwaitsecs=3600
[program:shopmetrics-worker-reports]
process_name=%(program_name)s_%(process_num)02d
command=php /var/www/shopmetrics/artisan queue:work redis --queue=reports --sleep=3 --tries=3 --max-time=3600
autostart=true
autorestart=true
numprocs=1
user=www-data
redirect_stderr=true
stdout_logfile=/var/log/shopmetrics/worker-reports.log
(3) supervisorコマンド
# 新しい設定を読み込む
sudo supervisorctl reread
sudo supervisorctl update
# ワーカーの開始/停止/再起動
sudo supervisorctl start shopmetrics-worker-default:*
sudo supervisorctl stop shopmetrics-worker-default:*
sudo supervisorctl restart shopmetrics-worker-default:*
# ステータス確認
sudo supervisorctl status
| プロセス数 | キュー | CPU | メモリ | 説明 |
|---|---|---|---|---|
| 2 | default | 低 | 中 | 一般タスク |
| 1 | reports | 高 | 高 | レポート生成 |
| 1 | analytics | 高 | 高 | データ分析 |
| 1 | notifications | 低 | 低 | メール/プッシュ |
(1) ▶ サンプル:ShopMetricsマルチキューワーカーの起動
# 開発 — 単一ワーカー, 全キュー
php artisan queue:work --queue=default,reports,notifications
# 本番 — キュー優先度ごとに別々のワーカー
# 優先度:high > default > low
php artisan queue:work redis --queue=high,default
php artisan queue:work redis --queue=reports,low
# タイムリミット付きでジョブを処理 (メモリリーク対策の自動再起動)
php artisan queue:work --max-time=3600 --max-jobs=1000
# キューの監視
php artisan queue:monitor redis:default,redis:reports
出力:
# コマンド実行成功
8. 総合サンプル:ShopMetrics非同期レポートシステム
// ============================================
// 総合:ShopMetrics非同期レポートシステム
// 対象:jobs, batches, retries, supervisor, notifications
// ============================================
// app/Jobs/GenerateReportJob.php
class GenerateReportJob implements ShouldQueue
{
use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;
public int $tries = 3;
public array $backoff = [60, 180, 600];
public int $timeout = 600;
public bool $deleteWhenMissingModels = true;
public function __construct(
public Tenant $tenant,
public string $reportType,
public string $format = 'csv',
public ?int $userId = null,
) {
$this->onQueue('reports');
}
public function handle(ReportService $reportService): void
{
$path = $reportService->generate(
$this->tenant,
$this->reportType,
$this->format,
);
$downloadUrl = Storage::disk('s3')->temporaryUrl($path, now()->addDays(7));
$user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
$user?->notify(new ReportReadyNotification(
downloadUrl: $downloadUrl,
reportType: $this->reportType,
expiresAt: now()->addDays(7),
));
}
public function failed(\Throwable $exception): void
{
$user = $this->userId ? User::find($this->userId) : $this->tenant->users->first();
$user?->notify(new ReportFailedNotification(
reportType: $this->reportType,
error: $exception->getMessage(),
));
}
}
// コントローラーでの使用
class ReportController extends Controller
{
public function generate(Request $request): JsonResponse
{
$validated = $request->validate([
'report_type' => 'required|in:monthly,weekly,custom',
'format' => 'sometimes|in:csv,xlsx,pdf',
'date_from' => 'sometimes|date',
'date_to' => 'sometimes|date|after:date_from',
]);
GenerateReportJob::dispatch(
tenant(),
$validated['report_type'],
$validated['format'] ?? 'csv',
auth()->id(),
);
return response()->json([
'message' => 'Report generation started. You will receive an email when ready.',
'estimated_time' => '5-15 minutes',
], 202);
}
}
❓ よくある質問
queue:workとqueue:listenの違いは何ですか?queue:workはフレームワークを再起動せずにメモリ上で実行します (高速ですが, コード変更後は手動再起動が必要)。queue:listenはタスクごとにフレームワークを再起動します (低速ですが, 新しいコードを自動的に読み込みます)。本番ではqueue:work + Supervisorを使用し, 開発ではqueue:listenまたはqueue:work --onceを使用してください。SerializesModelsトレイトがモデルIDを自動的にシリアライズし, デシリアライズ時にデータベースから再取得します。利点はジョブのデータフットプリントが小さくなること, リスクはモデルが削除されるとジョブが失敗することです (deleteWhenMissingModelsで無視できます)。allowFailures()を設定すると, 一部が失敗してもバッチの実行を継続できます。Bus::findBatch()で失敗の詳細を確認し, 失敗したタスクを手動でリトライしてください。QUEUE_CONNECTION=syncでジョブを同期的に実行します (エラーがすぐに表示されます)。本番ではphp artisan queue:failedで失敗ジョブのエラーメッセージを確認します。ジョブのfailed()メソッドで詳細情報をログに記録することもできます。queue:workプロセスをデーモンとして実行できます。📖 まとめ
- キューにより時間のかかるタスクが非同期になり, ユーザーリクエストは数秒以内に応答されます
- Redisは本番環境に最適なキュードライバーで, 高性能と永続性を提供します
- ジョブは
tries/backoff設定でリトライ戦略を指定します。指数バックオフでアバランチを防止します - 大量の類似タスクをバッチ処理し, 進捗追跡と部分失敗をサポートします
- Supervisorがワーカープロセスを監視し, クラッシュ時に自動再起動します
- 複数の優先度レベルのキュー:high/default/reports/low
📝 練習問題
-
基本問題 (⭐):
GenerateReportJobを作成し, コントローラーでCSVレポートを非同期生成するリクエストをディスパッチし, databaseドライバーでテストし,queue:workを実行してジョブが実行されることを確認してください。 -
応用問題 (⭐⭐):Redisキュードライバーを設定し, 指数バックオフ (30秒/60秒/120秒)で3回リトライを実装し, ジョブ失敗時にユーザーに通知を送信し, 手動リトライ (queue:retry)をテストしてください。
-
チャレンジ (⭐⭐⭐):
Bus::batch()を使用して月次マルチテナント分析のバッチタスクを実装してください—すべてのアクティブなテナントを反復処理し, テナントごとに1つのジョブを作成し, 進捗率を監視し, 完了時にキャッシュをクリアし, Supervisorを設定してワーカーを監視してください。



