【Laravel】Queueの基本:時間のかかる処理を非同期化する

laravelアイキャッチ Laravel

こんにちは、かつコーチです。

前回は、config・route・viewの3種類のキャッシュで応答速度を改善する方法を解説しました。

キャッシュは「無駄な処理を減らして速くする」アプローチでしたが、今回紹介するQueue(キュー)は、「時間のかかる処理をリクエストの外に追い出す」という、また別のアプローチで体感速度を改善する仕組みです。

デプロイ・インフラ連携編の締めくくりとして、Laravelの非同期処理の基本を押さえていきましょう。

なぜ処理を非同期化する必要があるのか

同期処理のままだと何が問題なのか

たとえば、会員登録が完了したときに確認メールを送信する処理を考えてみます。

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        $user = User::create($request->validated());

        // ここでメール送信APIへの通信が発生する
        Mail::to($user)->send(new WelcomeMail($user));

        return redirect('/dashboard');
    }
}

このコードは動作しますが、Mail::to()->send() の処理が終わるまで、ユーザーは画面が切り替わるのを待たされます。

メール送信サービスの応答が2秒かかれば、ユーザーはボタンを押してから2秒間、真っ白な画面を見つめ続けることになります。

私が実際に体験した「重い処理でリクエストがタイムアウトする」問題

私が担当していたアプリで、CSVファイルを取り込んで数千件のデータを一括登録する機能を、最初は同期処理でそのまま実装していました。

小さいテスト用CSVでは問題なく動いていたのですが、本番で実際に数万件規模のCSVがアップロードされたとき、サーバー側のタイムアウト設定(30秒)を超えてしまい、処理が中断されるという不具合が発生しました。

ユーザーから見ると「アップロードボタンを押したらエラー画面になった」としか分からず、実際にはどこまでデータが登録されたのかも分からない状態になってしまったのです。

この経験から、「時間のかかる処理はリクエストの中で完結させない」という設計の重要性を痛感しました。

Queueの基本的な仕組み

Queueとは何か

Queue(キュー)とは、「今すぐやらなくてもいい重い処理」を一旦順番待ちのリストに積んでおき、別のプロセス(ワーカー)が後から順番に処理していく仕組みです。

先ほどのメール送信の例で言えば、「メールを送る」という処理そのものは、ユーザーが画面遷移を待つ間に完了している必要はありません。

処理をQueueに積んでおけば、コントローラ側は「メール送信をお願いね」と依頼を投げた時点ですぐにレスポンスを返せます。

Queueを使う場合の処理の流れ

1. ユーザーがフォームを送信
2. コントローラがQueueにジョブを積む(一瞬で完了)
3. コントローラがすぐにレスポンスを返す(ユーザーは待たされない)
4. 裏側で動いているワーカーが、Queueからジョブを取り出して実行する

ユーザーへの応答と、実際の重い処理の実行タイミングが切り離される、というのがポイントです。

Jobクラスを作って非同期処理を実装する

Jobクラスの作成

Artisanコマンドで、非同期実行したい処理をJobクラスとして作成します。

php artisan make:job SendWelcomeEmail

生成されたクラスに、実行したい処理を書いていきます。

<?php
// app/Jobs/SendWelcomeEmail.php

namespace App\Jobs;

use App\Models\User;
use App\Mail\WelcomeMail;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Mail;

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public function __construct(
        public User $user
    ) {}

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeMail($this->user));
    }
}

ShouldQueue インターフェースを実装することで、このJobは「非同期で実行する対象」としてLaravelに認識されます。

コントローラからJobを呼び出す

class RegisterController extends Controller
{
    public function store(Request $request)
    {
        $user = User::create($request->validated());

        // メール送信処理をQueueに積むだけ(すぐに次の処理に進む)
        SendWelcomeEmail::dispatch($user);

        return redirect('/dashboard');
    }
}

Mail::to()->send() を直接呼んでいた箇所が、SendWelcomeEmail::dispatch($user) に置き換わっただけで、実際のメール送信処理は裏側のワーカーに任せられるようになりました。

Queueの接続先(ドライバ)を選ぶ

主なドライバの選択肢

Queueに積んだジョブをどこに保存しておくかは、.envQUEUE_CONNECTION で設定します。

ドライバ特徴こんな場面におすすめ
sync非同期にせず、その場で即実行するローカル開発での動作確認
databaseジョブをDBのテーブルに保存する小〜中規模で追加インフラを増やしたくない場合
redisRedisにジョブを保存するある程度の処理量があり高速に捌きたい場合
sqsAWSのマネージドキューサービスを使うAWSインフラで大規模に運用する場合

どれを選べばいいかの判断軸

初めてQueueを導入するなら、追加のミドルウェアが不要なdatabaseドライバから始めるのがおすすめです。

php artisan queue:table
php artisan migrate
# .env
QUEUE_CONNECTION=database

処理量が増えてきて、DBへの読み書きがボトルネックになってきたタイミングで、Redisドライバへの切り替えを検討すると無理がありません。

Queueワーカーを起動する

ワーカーの起動コマンド

Queueにジョブを積むだけでは、実際の処理は実行されません。

積まれたジョブを取り出して実行するワーカーというプロセスを、別途起動しておく必要があります。

php artisan queue:work

このコマンドを実行しているあいだ、ワーカーはQueueを常に監視し、新しいジョブが積まれるたびに順番に処理していきます。

つまずきやすいポイント:ワーカーを起動せず「ジョブが実行されない」と勘違いした話

Queueを初めて導入したとき、SendWelcomeEmail::dispatch($user) を呼び出したのに、いつまで経ってもメールが届かず「実装が間違っているのでは」と1時間近く悩んだことがあります。

❌ Before:ワーカーを起動せずにジョブを積む

# ジョブを積むコントローラのコードは正しい
# しかし、以下のワーカー起動コマンドを実行し忘れている

原因を調べていくと、jobs テーブルには確かにジョブのレコードが増えていることに気づきました。

つまり「ジョブを積む」ところまでは正しく動いており、単に「積まれたジョブを取り出して実行するワーカー」を起動していなかっただけだったのです。

✅ After:ワーカーを起動してからジョブの動作確認をする

# 別のターミナルでワーカーを起動しておく
php artisan queue:work

# その状態でフォームを送信すると、キューに積まれた直後に処理される

この経験以降、Queue関連の動作確認をするときは、まず php artisan queue:work が起動しているかを最初にチェックする癖がつきました。

本番環境では、このワーカーをSupervisorのようなプロセス管理ツールで常時稼働させ、万が一ワーカーが落ちても自動的に再起動される構成にしておく必要があります。

失敗したジョブへの対処

リトライとfailed jobsテーブル

外部のメール送信APIが一時的に落ちているなど、ジョブの実行中にエラーが発生することもあります。

Jobクラスには、リトライ回数を指定できます。

class SendWelcomeEmail implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 3; // 最大3回まで再試行する

    public function __construct(
        public User $user
    ) {}

    public function handle(): void
    {
        Mail::to($this->user)->send(new WelcomeMail($this->user));
    }
}

指定した回数リトライしてもすべて失敗した場合、そのジョブは failed_jobs テーブルに記録されます。

# 失敗したジョブを再実行する
php artisan queue:retry all

失敗したジョブが放置されないよう、本番運用では failed_jobs テーブルを定期的に確認する仕組みも合わせて検討しておくと安心です。

まとめ

この記事のポイント

  • Queueは、時間のかかる処理をリクエストの外に追い出し、ユーザーの待ち時間を減らす仕組み
  • ShouldQueue を実装したJobクラスを作り、dispatch() でQueueに処理を積む
  • ドライバは database から始めて、処理量に応じて redis などへの切り替えを検討する
  • php artisan queue:work でワーカーを起動しないと、積んだジョブは実行されないままになる
  • リトライ回数を設定し、失敗したジョブは failed_jobs テーブルで管理・再実行できる

次に読むべき記事

Queueで「重い処理を後回しにする」やり方が分かったところで、次は「決まった時間に定期的に処理を実行する」Task Schedulingの仕組みを見ていきます。

→ 次の記事:Task Scheduling(スケジューラ)で定期処理を実装する

タイトルとURLをコピーしました