【Laravel】キュー(Queue)とジョブ(Job)の使い方|非同期処理の実装から本番運用まで

はじめに

メール送信・PDF生成・外部APIへの連続リクエストなど、時間のかかる処理をコントローラの中に書いていませんか?

そのままではレスポンスが遅くなり、ユーザー体験を損ないます。タイムアウトや失敗時の再試行も難しくなります。

LaravelのQueue(キュー)とJob(ジョブ)を使うと、重い処理をバックグラウンドに切り出してレスポンスを即時返しつつ、ワーカーが非同期で処理を実行する構成を作れます。

この記事では、Queueの基本的な仕組みから実装手順、失敗時の対処、本番運用のポイントまで整理します。

Queueの仕組み

Laravelのキューは以下の流れで動作します。

  1. アプリケーションが Job をキュー(待ち行列)に投入(dispatch)
  2. キューにジョブが溜まる(バックエンドはRedis・DBなど)
  3. ワーカー(queue:work)がジョブを取り出して実行
  4. 成功すればジョブを削除、失敗すればリトライまたは failed_jobs に記録
コントローラ
  └─ dispatch(Job) ──→ キュー(Redis / DB / SQS)
                              └─ queue:work ──→ Jobを実行

コネクションとキューの違い

混同しやすいので整理しておきます。

用語意味
コネクションバックエンドへの接続設定redisdatabasesqs
キューコネクション上の論理的な待ち行列defaultemailshigh

1つのコネクションに複数のキューを持てます。優先度の高いジョブ用に別キューを用意する構成がよく使われます。

ドライバの選択と初期設定

config/queue.php.env でドライバを設定します。

# .env
QUEUE_CONNECTION=database  # database / redis / sqs / sync

databaseドライバ(開発・小規模向け)

# ジョブテーブルを作成
php artisan make:queue-table
php artisan migrate

redisドライバ(本番推奨)

predis/predis または phpredis 拡張が必要です。

composer require predis/predis
QUEUE_CONNECTION=redis
REDIS_HOST=127.0.0.1
REDIS_PORT=6379

syncドライバ(テスト・デバッグ用)

ディスパッチと同時に同期実行されます。ワーカー不要でキューの動作確認に便利です。

QUEUE_CONNECTION=sync

Jobの作成

php artisan make:job ProcessReport

app/Jobs/ProcessReport.php が生成されます。

<?php

namespace App\Jobs;

use App\Models\Report;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Queue\Queueable;

class ProcessReport implements ShouldQueue
{
    use Queueable;

    public function __construct(
        public Report $report,
    ) {}

    public function handle(): void
    {
        // バックグラウンドで実行したい処理
        // 例:レポートのCSV生成・集計・外部API送信など
    }
}

ShouldQueue を実装することで、このJobがキュー経由で実行されるようになります。handle() メソッドがワーカーに取り出されたときに実行されます。

Eloquentモデルをコンストラクタに渡す場合の注意

Eloquentモデルを渡すと、キューに載るペイロードにはモデルのIDのみが保存され、実行時に再度DBから取得されます。

そのため 「ディスパッチした時点のモデルの状態」と「実行時のモデルの状態」が異なる場合があることに注意が必要です。

また、ロード済みのリレーションもシリアライズ対象になり、ペイロードが肥大化することがあります。不要なリレーションを外してからディスパッチするのが安全です。

// リレーションを外してからディスパッチ
ProcessReport::dispatch($report->withoutRelations());

Jobのディスパッチ

コントローラやサービスクラスから dispatch でキューに投入します。

use App\Jobs\ProcessReport;

// デフォルトキューに投入
ProcessReport::dispatch($report);

// 特定のキューに投入
ProcessReport::dispatch($report)->onQueue('reports');

// 指定秒数後に実行(遅延実行)
ProcessReport::dispatch($report)->delay(now()->addSeconds(30));

// 同期実行(キューを経由しない)
ProcessReport::dispatchSync($report);

dispatch ヘルパを使う方法

dispatch(new ProcessReport($report));
dispatch(new ProcessReport($report))->onQueue('reports');

Jobのオプション設定

Jobクラスにプロパティを定義することで、リトライ回数やタイムアウトを設定できます。

class ProcessReport implements ShouldQueue
{
    use Queueable;

    // 最大試行回数
    public int $tries = 3;

    // タイムアウト(秒)
    public int $timeout = 60;

    // 失敗とみなすまでの最大時間(秒)
    public int $maxExceptions = 2;

    // リトライ間隔(秒)
    public function backoff(): array
    {
        return [10, 30, 60]; // 1回目10秒後、2回目30秒後、3回目60秒後
    }
}

ワーカーの起動

# デフォルトキューのワーカーを起動
php artisan queue:work

# 特定のコネクション・キューを指定
php artisan queue:work redis --queue=high,default

# 1件だけ処理して終了
php artisan queue:work --once

# メモリ上限(MB)を指定
php artisan queue:work --memory=256

queue:work はデーモンとして動作し、ジョブを繰り返し処理します。コードを変更した場合は ワーカーを再起動しないと変更が反映されません

# キャッシュをクリアしてワーカーを再起動
php artisan queue:restart

queue:workとqueue:listenの違い

コマンドメモリ効率コード変更の反映用途
queue:work良い(常駐)再起動が必要本番環境
queue:listen悪い(毎回起動)自動反映開発環境

本番では queue:work を使い、デプロイ後に queue:restart を実行するのが基本です。

失敗したJobの処理

failed_jobsテーブルの準備

php artisan make:failed-jobs-table
php artisan migrate

失敗時のコールバック

Jobクラスに failed() メソッドを定義すると、失敗時に呼ばれます。

public function failed(\Throwable $exception): void
{
    // 失敗を通知する処理
    // 例:Slackへの通知・ステータスの更新など
    \Log::error('ProcessReport failed', [
        'report_id' => $this->report->id,
        'error'     => $exception->getMessage(),
    ]);
}

失敗したJobを手動でリトライ

# 失敗したJobを一覧表示
php artisan queue:failed

# 特定のJobをリトライ
php artisan queue:retry {id}

# すべての失敗Jobをリトライ
php artisan queue:retry all

# 失敗Jobを削除
php artisan queue:forget {id}
php artisan queue:flush

本番運用:Supervisorの設定

本番環境では、ワーカーが落ちたときに自動で再起動できるよう Supervisor でプロセス管理するのが定番です。

; /etc/supervisor/conf.d/laravel-worker.conf

[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
stopasgroup=true
killasgroup=true
user=www-data
numprocs=2
redirect_stderr=true
stdout_logfile=/var/www/html/storage/logs/worker.log
stopwaitsecs=3600
# Supervisorの設定を反映
sudo supervisorctl reread
sudo supervisorctl update
sudo supervisorctl start laravel-worker:*

numprocs でワーカーのプロセス数を増やすと並列処理できます。ジョブの処理量に合わせて調整してください。

デプロイ時の手順

# コードをデプロイ後に実行
php artisan queue:restart

queue:restart はワーカーを即座に停止させるのではなく、現在処理中のジョブが完了してから再起動します。Supervisorが自動で新しいプロセスを立ち上げます。

テスト

Queue::fake() を使うと、実際にキューに投入せずにディスパッチをテストできます。

use Illuminate\Support\Facades\Queue;
use App\Jobs\ProcessReport;

public function test_report_job_is_dispatched(): void
{
    Queue::fake();

    $report = Report::factory()->create();

    // ジョブをディスパッチする処理を呼ぶ
    $this->post('/reports', ['report_id' => $report->id]);

    // ジョブがディスパッチされたかアサート
    Queue::assertPushed(ProcessReport::class);

    // 特定の条件でディスパッチされたかアサート
    Queue::assertPushed(ProcessReport::class, function ($job) use ($report) {
        return $job->report->id === $report->id;
    });

    // ディスパッチされていないことをアサート
    Queue::assertNotPushed(AnotherJob::class);
}

実務でよく使うパターン

メール送信を非同期化する

ShouldQueue を実装した Mailable は自動でキュー経由になります。

use App\Mail\WelcomeMail;
use Illuminate\Support\Facades\Mail;

Mail::to($user)->queue(new WelcomeMail($user));

チェーンジョブ(順次実行)

複数のJobを順番に実行させるには withChain を使います。

ProcessReport::withChain([
    new NotifyUser($user),
    new CleanupTempFiles(),
])->dispatch($report);

前のJobが失敗するとチェーンはそこで止まります。

バッチジョブ(並列実行・完了検知)

Laravel 8以降で使えるバッチ機能です。複数Jobを並列実行し、すべて完了したときにコールバックを実行できます。

php artisan make:batch-table
php artisan migrate
use Illuminate\Support\Facades\Bus;

$batch = Bus::batch([
    new ProcessChunk($records->slice(0, 100)),
    new ProcessChunk($records->slice(100, 100)),
    new ProcessChunk($records->slice(200, 100)),
])->then(function (Batch $batch) {
    // すべて成功したとき
    Log::info('All chunks processed.');
})->catch(function (Batch $batch, \Throwable $e) {
    // いずれかが失敗したとき
    Log::error('Batch failed: ' . $e->getMessage());
})->finally(function (Batch $batch) {
    // 成否問わず最後に実行
})->dispatch();

まとめ

項目ポイント
ドライバ開発は database、本番は redis が一般的
JobShouldQueue を実装、handle() に処理を書く
dispatch::dispatch() でキューに投入、->onQueue() でキュー指定
ワーカー本番はSupervisorで常駐管理、デプロイ後に queue:restart
失敗対応failed_jobs テーブルで記録、queue:retry で再実行
テストQueue::fake() でディスパッチをアサート

キューを導入するとアプリケーションは「リクエスト経路」と「ワーカー経路」の二本立てになります。ログの出し分け、失敗ジョブの監視、デプロイ時のワーカー再起動をセットで設計しておくと、本番環境でも安心して運用できます。

LaravelLaravel

Posted by 千原 耕司