Help us understand the problem. What is going on with this article?

Laravelのキューを使ってみた

More than 1 year has passed since last update.

キューとは

一部の処理を非同期的に実行ができます。

例えば、「ユーザー登録をしたときに完了メールを送る」処理を作るとします。
登録完了からメール送信完了して画面を表示するとメール送信処理がボトルネックになる可能性があります。

そこでメール送信処理をキューに変更し非同期にすることで、
登録完了後、メール送信処理を待たずに完了画面を表示することができます。

利用ケース

バッチで大量のデータを取得してDBへ保存。
またデータに対応する画像を取得してCloudinaryへアップする処理。

同期的に画像を取得してアップするとボトルネックになるので、
キューを使ってスピードアップを図る。

Laravel5.7とLaradockを利用しています。

Redis構築

今回はRedisを利用してやってみます。

laradock
$ docker-compose up -d redis

接続確認

laradock
$ docker-compose exec redis redis-cli

127.0.0.1:6379> keys *
(empty list or set)
127.0.0.1:6379>

繋がりました!

Laravel設定

依存パッケージインストール

laradock
$ composer require predis/predis

.env

.envファイルを修正します。

# laradockで構築したときは以下のHOSTになる
REDIS_HOST=redis

# redis利用に変更
QUEUE_CONNECTION=redis

ジョブを作成する

ジョブ登録時に画像URLと画像名を受け取って、
実行時にローカルへ画像を保存する処理を書いてみます。

laradock
$ php artisan make:job ProductImage
ProductImage
<?php

namespace App\Jobs;

use Illuminate\Bus\Queueable;
use Illuminate\Queue\SerializesModels;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;

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

    private $url;

    private $fileName;

    /**
     * Create a new job instance.
     *
     * @param string $url
     * @param string $fileName
     */
    public function __construct(string $url, string $fileName)
    {
        // 画像URL
        $this->url = $url;

        // 画像名
        $this->fileName = $fileName;
    }

    /**
     * Execute the job.
     *
     * @return void
     */
    public function handle()
    {
        // storage直下へファイルを保存
        file_put_contents("./storage/$this->fileName", file_get_contents($this->url));
        Log::info('キュー実行完了');
    }
}

コマンドの作成

実行用のコマンドを作ってみます。
Qiitaのロゴをダウンロードして保存する処理にします。

$ php artisan make:command FetchProductImage

$signatureの変更を忘れないようにしてください。

FetchProductImage
<?php

namespace App\Console\Commands;

use App\Jobs\ProductImage;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\Log;

class FetchProductImage extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'command:fetch_product_image';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'Command description';

    /**
     * Create a new command instance.
     *
     * @return void
     */
    public function __construct()
    {
        parent::__construct();
    }

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        Log::info('コマンド実行開始');

        // 非同期実行を明確化するために1分遅延させる
        ProductImage::dispatch('https://cdn.qiita.com/assets/qiita-rectangle-71910ff07b744f263e4a2657e2ffd123.png', 'Qiita.png')
            ->delay(now()->addMinutes(1));

        Log::info('コマンド実行完了');
    }
}

実行

ワーカーの立ち上げ

キューを監視して実行してくれるワーカーを立ち上げます。

$ php artisan queue:work

コマンド実行

$ php artisan command:fetch_product_image

実行ログ確認

まだコマンドのログのみ表示されています。

log
[2018-10-27 16:09:26] local.INFO: コマンド実行開始
[2018-10-27 16:09:26] local.INFO: コマンド実行完了

Redis確認

キューが登録されているのを確認できます。

$ docker-compose exec redis redis-cli

127.0.0.1:6379> keys *
1) "queues:default:delayed"
127.0.0.1:6379> zrange queues:default:delayed 0 -1
1) "{\"displayName\":\"App\\\\Jobs\\\\ProductImage\",\"job\":\"Illuminate\\\\Queue\\\\CallQueuedHandler@call\",\"maxTries\":null,\"timeout\":null,\"timeoutAt\":null,\"data\":{\"commandName\":\"App\\\\Jobs\\\\ProductImage\",\"command\":\"O:21:\\\"App\\\\Jobs\\\\ProductImage\\\":9:{s:26:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000url\\\";s:81:\\\"https:\\/\\/cdn.qiita.com\\/assets\\/qiita-rectangle-71910ff07b744f263e4a2657e2ffd123.png\\\";s:31:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000fileName\\\";s:9:\\\"Qiita.png\\\";s:6:\\\"\\u0000*\\u0000job\\\";N;s:10:\\\"connection\\\";N;s:5:\\\"queue\\\";N;s:15:\\\"chainConnection\\\";N;s:10:\\\"chainQueue\\\";N;s:5:\\\"delay\\\";O:25:\\\"Illuminate\\\\Support\\\\Carbon\\\":3:{s:4:\\\"date\\\";s:26:\\\"2018-10-27 16:10:26.434169\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:10:\\\"Asia\\/Tokyo\\\";}s:7:\\\"chained\\\";a:0:{}}\"},\"id\":\"S9aNrtHKPtoNUFSfs9psLVkv39FnEpS1\",\"attempts\":0}"

一分後

ちょうど一分後にログが出ました。

log
[2018-10-27 16:09:26] local.INFO: コマンド実行開始
[2018-10-27 16:09:26] local.INFO: コマンド実行完了
[2018-10-27 16:10:26] local.INFO: キュー実行完了

キューワーカーでも実行が確認できます。

worker
[2018-10-27 16:10:26][S9aNrtHKPtoNUFSfs9psLVkv39FnEpS1] Processing: App\Jobs\ProductImage
[2018-10-27 16:10:26][S9aNrtHKPtoNUFSfs9psLVkv39FnEpS1] Processed:  App\Jobs\ProductImage

しっかり画像が保存されています!

スクリーンショット 2018-10-27 16.13.56.png

複数のキューを実行

以下のように書き換えます。
今度はLaravelとDockerのロゴを保存する処理です。

FetchProductImage.php
        ProductImage::dispatch('https://www.docker.com/sites/default/files/vertical.png', 'docker.png')
            ->delay(now()->addMinutes(1));

        ProductImage::dispatch('http://laravel.jp/assets/img/logo-head.png', 'laravel.png')
            ->delay(now()->addMinutes(1));

コマンド実行

$ php artisan command:fetch_product_image

Redis確認

2つ登録されているのが確認できます。

redis
127.0.0.1:6379> zrange queues:default:delayed 0 -1
1) "{\"displayName\":\"App\\\\Jobs\\\\ProductImage\",\"job\":\"Illuminate\\\\Queue\\\\CallQueuedHandler@call\",\"maxTries\":null,\"timeout\":null,\"timeoutAt\":null,\"data\":{\"commandName\":\"App\\\\Jobs\\\\ProductImage\",\"command\":\"O:21:\\\"App\\\\Jobs\\\\ProductImage\\\":9:{s:26:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000url\\\";s:42:\\\"http:\\/\\/laravel.jp\\/assets\\/img\\/logo-head.png\\\";s:31:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000fileName\\\";s:11:\\\"laravel.png\\\";s:6:\\\"\\u0000*\\u0000job\\\";N;s:10:\\\"connection\\\";N;s:5:\\\"queue\\\";N;s:15:\\\"chainConnection\\\";N;s:10:\\\"chainQueue\\\";N;s:5:\\\"delay\\\";O:25:\\\"Illuminate\\\\Support\\\\Carbon\\\":3:{s:4:\\\"date\\\";s:26:\\\"2018-10-27 16:19:54.732950\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:10:\\\"Asia\\/Tokyo\\\";}s:7:\\\"chained\\\";a:0:{}}\"},\"id\":\"WcQ4hgjZqqeAsTDbSTqweAvH1nisBwUz\",\"attempts\":0}"
2) "{\"displayName\":\"App\\\\Jobs\\\\ProductImage\",\"job\":\"Illuminate\\\\Queue\\\\CallQueuedHandler@call\",\"maxTries\":null,\"timeout\":null,\"timeoutAt\":null,\"data\":{\"commandName\":\"App\\\\Jobs\\\\ProductImage\",\"command\":\"O:21:\\\"App\\\\Jobs\\\\ProductImage\\\":9:{s:26:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000url\\\";s:55:\\\"https:\\/\\/www.docker.com\\/sites\\/default\\/files\\/vertical.png\\\";s:31:\\\"\\u0000App\\\\Jobs\\\\ProductImage\\u0000fileName\\\";s:10:\\\"docker.png\\\";s:6:\\\"\\u0000*\\u0000job\\\";N;s:10:\\\"connection\\\";N;s:5:\\\"queue\\\";N;s:15:\\\"chainConnection\\\";N;s:10:\\\"chainQueue\\\";N;s:5:\\\"delay\\\";O:25:\\\"Illuminate\\\\Support\\\\Carbon\\\":3:{s:4:\\\"date\\\";s:26:\\\"2018-10-27 16:19:54.704917\\\";s:13:\\\"timezone_type\\\";i:3;s:8:\\\"timezone\\\";s:10:\\\"Asia\\/Tokyo\\\";}s:7:\\\"chained\\\";a:0:{}}\"},\"id\":\"YubmuatEQ4M3FRfKcB6CZzs0d8yI4Xlo\",\"attempts\":0}"

一分後

2つ実行されました!

worker
[2018-10-27 16:19:56][WcQ4hgjZqqeAsTDbSTqweAvH1nisBwUz] Processing: App\Jobs\ProductImage
[2018-10-27 16:19:57][WcQ4hgjZqqeAsTDbSTqweAvH1nisBwUz] Processed:  App\Jobs\ProductImage
[2018-10-27 16:19:57][YubmuatEQ4M3FRfKcB6CZzs0d8yI4Xlo] Processing: App\Jobs\ProductImage
[2018-10-27 16:19:57][YubmuatEQ4M3FRfKcB6CZzs0d8yI4Xlo] Processed:  App\Jobs\ProductImage

画像もしっかり保存されています。

スクリーンショット 2018-10-27 16.20.36.png

まとめ

簡単に非同期処理を実現できるので、いろいろなところで利用してみてはいかがでしょうか!

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away