RitoLabo

Laravelのデータベースキュー投入とジョブ処理で非同期処理を実現する~劇的に高速化できるユーザーレスポンス~

  • 公開:
  • 更新:
  • カテゴリ: PHP Laravel
  • タグ: Laravel,MySQL,5.5,Queues,Job,worker,5.6,5.7

Laravelでは、ジョブキューを実装すると高速なユーザ体験を提供できますが、前回の記事ではその流れをメインとしていたため、syncでのキュー投入とジョブ処理を実装しました。よって実質非同期処理は行われていませんでした。

今回は、データベースを用いてキュー投入を行い、非同期のジョブ処理を実装していきます。

アジェンダ
  1. 開発環境
  2. キュー用のデータベースを作成
  3. キューサービスをデータベースへ切り替える
  4. ジョブクラスの作成と実装
  5. コントローラ・ビュー・ルーティング作成
  6. キューワーカを起動する
  7. 動作確認

開発環境

今回の開発環境は以下の通りです。

  • Linux CentOS 7
  • Apache 2.4
  • MySQL 8.0/5.7
  • PHP 7.2/7.1

Laravelのバージョンについては、5.7/5.6/5.5 にて動作確認済みです。

上記環境でなくてもArtisanコマンドが叩ける事、そしてMySQLを使えればOKです。それと、LaravelでのDB接続は終えておいてください。

Laravelのルートディレクトリを「laravel/」とします。

尚、キューとジョブの基本的な使い方については、以下を参考にしてください。
Laravelのキュー投入によるジョブ処理を導入する

キュー用のデータベースを作成

まずは、キューを投入するテーブルを作成します。

Laravelルートディレクトリに移動し、以下のArtisanコマンドを叩いてマイグレーションファイルを作成します。

# laravelルートディレクトリへ移動
cd /path/to/laravel

# artisanコマンドでマイグレーションファイルを作成する
php artisan queue:table

# 実行結果
[demo@localhost laravel]$ php artisan queue:table
Migration created successfully!

laravel/database/migrationsに、マイグレーションファイル「xxxx_create_jobs_table.php」が生成されます。

laravel
├─ database
│ ├─ migrations
   │ └─ xxxx_create_jobs_table.php

生成されたマイグレーションファイルを見てみます。

laravel/database/migrations/xxxx_create_jobs_table.php
<?php

use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;

class CreateJobsTable extends Migration
{
/**
* Run the migrations.
*
* @return void
*/
public function up()
{
Schema::create('jobs', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('queue')->index();
$table->longText('payload');
$table->unsignedTinyInteger('attempts');
$table->unsignedInteger('reserved_at')->nullable();
$table->unsignedInteger('available_at');
$table->unsignedInteger('created_at');
});
}

/**
* Reverse the migrations.
*
* @return void
*/
public function down()
{
Schema::dropIfExists('jobs');
}
}
カラム構成[id][queue][payload][attempts][reserved_at][available_at][created_at]にて、jobsテーブルが作成される。という事のようです。

それではマイグレートしてjobsテーブルを作成します。以下のArtisanコマンドを叩きます。

# laravelルートディレクトリへ移動
cd /path/to/laravel

# artisanコマンドでマイグレートを行い、jobテーブルを作成する
php artisan migrate

# 実行結果
[demo@localhost laravel]$ php artisan migrate
Migrating: xxxx_create_jobs_table
Migrated: xxxx_create_jobs_table

MySQLにログインして、テーブルが作成されているか確認します。

mysql> show tables;
+----------------------+
| Tables_in_laravel_db |
+----------------------+
|
jobs |
| migrations |
| password_resets |
| users |
+----------------------+
mysql> show columns from jobs;
+--------------+---------------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+--------------+---------------------+------+-----+---------+----------------+
| id | bigint(20) unsigned | NO | PRI | NULL | auto_increment |
| queue | varchar(255) | NO | MUL | NULL | |
| payload | longtext | NO | | NULL | |
| attempts | tinyint(3) unsigned | NO | | NULL | |
| reserved_at | int(10) unsigned | YES | | NULL | |
| available_at | int(10) unsigned | NO | | NULL | |
| created_at | int(10) unsigned | NO | | NULL | |
+--------------+---------------------+------+-----+---------+----------------+

jobsテーブルの作成を確認できました。

キューサービスをデータベースへ切り替える

デフォルトではsyncになっていますので、laravel/.envを開いて、キューサービスをdatabaseへ切り替えます。

# Laravel 5.7〜
QUEUE_CONNECTION=database

# Laravel 〜5.6
QUEUE_DRIVER=database

バージョンによって.envの設定名が違う事に注意してください。切り替えはこれで完了です。

ファイル操作クラスの作成

今回は比較用に同期処理と非同期処理の2つを実装するので、共通処理をまとめておきます。(ここはジョブとかキューとかは関係ありません)

laravel/app/Http/Components/FileOperation.php
<?php

namespace App\Http\Components;

class FileOperation
{
/**
* テキストファイルを作成する
* @return string
*/
public function makeTextFile()
{
$file = sprintf('%s/test.txt', storage_path('texts'));
if(file_exists($file)) unlink($file);
touch($file);
return $file;
}

/**
* ファイルに指定回数分の追加書き込みを行う
* @param string $file
* @param int $max
*/
public function write(string $file, int $max)
{
for($i=0; $i< $max; $i++ ) {
$current = file_get_contents($file);
$current .= $i;
file_put_contents($file, $current);
}
}
}

テキストファイルを作成するmakeTextFile()メソッドと、テキストファイルに指定回数分の追加書き込みを行うwrite()メソッドを定義しています。

ジョブクラスの作成と実装

次に、ジョブクラスを作成し、処理を実装していきます。artisanコマンドを叩いて、ジョブクラスを生成します。

# laravelルートディレクトリへ移動
cd /path/to/laravel

# artisanコマンドでジョブクラス生成する
php artisan make:job GenerateTextFile

# 実行結果
[demo@localhost laravel]$ php artisan make:job GenerateTextFile
Job created successfully.

laravel/app/Jobs 配下に GenerateTextFile.phpが生成されます。

laravel
├─ app
│ ├─ Jobs
│ │   ├─ GenerateTextFile.php

ファイルを開いて処理を実装します。

laravel/app/Jobs/GenerateTextFile.php
<?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;
use App\Http\Components\FileOperation;

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

private $file;
private $max;
private $fp;

public function __construct($file, $max)
{
$this->file = $file;
$this->max = $max;
$this->fp = new FileOperation();
}

public function handle()
{
// 書き込み
$this->fp->write($this->file, $this->max);
}
}

コントローラ・ビュー・ルーティング作成

今回の検証用のコントローラとビューを作成して、ルーティングを行います。検証用という事で、キュー投入処理版と通常処理版の2つを書いていきます。

ルーティング
laravel/routes/web.php
Route::get('sample/queues/none', 'SampleController@queuesNone');
Route::get('sample/queues/database', 'SampleController@queuesDatabase');
http://YOURDOMAIN/sample/queues/none → 通常の同期処理を行う
http://YOURDOMAIN/sample/queues/database → 非同期処理を行う
コントローラ
laravel/app/Http/Controllers/SampleController.php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\Request;
use App\Jobs\GenerateTextFile;
use App\Http\Components\FileOperation;

class SampleController extends Controller
{
const MAX = 3000; // ループ回数

private $fp;

public function __construct()
{
$this->fp = new FileOperation();
}

public function queuesNone()
{
$start = time();

$file = $this->fp->makeTextFile();

$this->fp->write($file, self::MAX);

return view('sample_queues', ['start' => $start]);
}

public function queuesDatabase()
{
$start = time();

$file = $this->fp->makeTextFile();

GenerateTextFile::dispatch($file, self::MAX);

return view('sample_queues', ['start' => $start]);
}
}
queuesNone()メソッドは通常処理(同期処理)用、queuesDatabase()メソッドは非同期処理用になります。 時間計測のために処理開始時のUnixタイムスタンプをビューへ渡しています。 そして、書き込み回数を定数で3000回に指定しています。
ビュー
laravel/resources/views/sample_queues.blade.php
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>Sample Queues</title>
</head>
<body>
<p>
Queues test!!
</p>
<?php
if(!empty($start)) { echo time() - $start; }
?>
</body>
</html>
現在のタイムスタンプから、コントローラから受け取ったタイムスタンプの差分を出す事で、処理にかかった時間(秒数)を出力しています。

キューワーカを起動する

動作確認を行う前に、キューワーカーを起動します。これを行わないと、データベースキューに投入されたジョブが処理されません。

# laravelルートディレクトリへ移動
cd /path/to/laravel

# artisanコマンドでキューワーカーを起動する
php artisan queue:work

コンソールが待機状態になれば、ワーカーが起動したことになります。

動作確認

キューワーカーを起動させたら、ブラウザからアクセスして動作確認を行いましょう。

  • 通常処理(同期処理) → http://YOURDOMAIN/sample/queues/none
  • 非同期処理 → http://YOURDOMAIN/sample/queues/database

として、それぞれブラウザからアクセスしてみます。

通常処理(同期処理)
通常処理(同期処理)
非同期処理
非同期処理

3000回のループ処理ですが、通常処理の場合は表示までに11秒かかっているのに対し、非同期処理の場合は処理自体をキューに投げて別処理になっているので、表示まではわずか1秒未満と非常に高速です。

キューワーカーを起動したコンソール画面を確認してみましょう。

[demo@localhost laravel]$ php artisan queue:work
[2019-02-11 18:56:19][1] Processing: App\Jobs\GenerateTextFile
[2019-02-11 18:56:31][1] Processed: App\Jobs\GenerateTextFile

実行結果が出力されています。

まとめ

以上でデータベースを利用したキュー&ジョブ処理の作業は完了です。

処理をキューへ投下する事で処理が分離され、非常に高速にレスポンスを返せるようになります。

また、テーブルに挿入されたキュー情報は処理されたら削除されるので、データベースも圧迫されません。

そのほかにも、処理の優先順位付けや、処理遅延時間の設定など、色々行えるので、是非試してみてください。