Laravelのキューを使ってみた

# キューとは
一部の処理を非同期的に実行ができます。

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

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

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

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

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

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

```bash:laradock
$ docker-compose up -d redis
```

接続確認

```bash:laradock
$ docker-compose exec redis redis-cli

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

繋がりました！


# Laravel設定
## 依存パッケージインストール
```bash:laradock
$ composer require predis/predis
```

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

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

# redis利用に変更
QUEUE_CONNECTION=redis
```

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

```bash:laradock
$ php artisan make:job ProductImage
```

```php: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のロゴをダウンロードして保存する処理にします。

```bash
$ php artisan make:command FetchProductImage
```

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

```php: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('コマンド実行完了');
    }
}

```

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

```bash
$ php artisan queue:work
```

## コマンド実行

```bash
$ php artisan command:fetch_product_image
```

## 実行ログ確認
まだコマンドのログのみ表示されています。

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

## Redis確認

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

```bash
$ 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}"
```

## 一分後

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

```bash: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: キュー実行完了
```

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

```bash: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](https://qiita-image-store.s3.amazonaws.com/0/58712/bb58a667-3ffa-0237-323d-99b48ba39479.png)

# 複数のキューを実行

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

```php: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));
```

## コマンド実行

```bash
$ php artisan command:fetch_product_image
```

## Redis確認

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

```bash: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つ実行されました！

```bash: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](https://qiita-image-store.s3.amazonaws.com/0/58712/08d1a434-0c9a-2022-6d49-058776fc0450.png)

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