当ブログ人気記事!
転職体験談
Laravelで学ぶWebアプリ開発
誰でも作れるチャットアプリ
未経験への勧め

【実践Laravel】RedisからDBにデータバックアップするバッチ(バルクアップデート使用)

プログラミング

Laravelでバッチを作りたい!
Laravelでバルクアップデートを実装したい!
LaravelでCronを使いたい!

こういった悩みに対して書きました!


この記事を書いている私(@Shoot58153748)は、
2020年3月現在メガベンチャーの社内スタートアップの部署で
エンジニア(1年目)をしており、

プログラミング未経験からメガベンチャーへの転職を成功させた経験・ノウハウ
Webエンジニアになってから学んだこと

をブログにまとめています。


前回までは、いいね、通報、インプレッションなどのユーザーアクションに関する機能
を作ってきました。

今回はバッチ実装です

現在いいねとインプレッション総数をRedisで管理しているので、
Redisに格納されているデータをDBに反映するバッチを作成。

そして作成したバッチをいちいち手で叩くのはナンセンスなので、
1日に1回深夜に実行するスケジューラーの実装も合わせて実装します。


あと、数千、数十万件のデータを扱うことを想定し、
OffsetとLimit、バルクアップデートの実装も加えました。


今回学べるポイントを以下にまとめました。
是非参考にしてください!


今回のポイント

  • Laravelでのバッチ作成法
  • Laravelでのバルクアップデート
  • Laravelでのバッチスケジューラ
スポンサーリンク

画面イメージ(ターミナル)

バルクアップデートとは

バルクアップデートとは、1回のクエリで複数のレコードを一括して更新する手法のことです。


何回もクエリ発行せずに済むので、
更新処理が高速に行うことができます。(万単位だと数十倍は異なるかも)

今回の場合、
万単位のツイート情報の更新を想定しているので、バルクアップデートを導入することにしました。

特にバッチ処理などは該当レコード数が多いので、
バルクアップデート上手く活用できると設計の幅が広がります。

Laravelには特にライブラリが用意されてないので
自分で準備する必要があります。

バッチ実装

処理の流れ


Redisからいいねとインプレッションのデータ(以下のデータ構造)を1000件ずつ取得します。


SortedSet型

Key

“laravel_database_shoot_tweet:like”

“laravel_database_shoot_tweet:impression”

Score

{likeCount}

{impressionCount}

Member

{tweetId}

{tweetId}


1000件取得したら、
ログ出力

--updateオプションが付いていない場合、更新作業をせずに終了

--likeオプション付きの場合、
いいね総数をバルクアップデート

--impressionオプション付きの場合
インプレッション総数をバルクアップデート

という流れで実装していきます。

実装

今回の制作物は以下になります。

  • バッチクラス
  • Redisクラス
  • Configファイル
  • Kernel(バッチスケジューラ)


まず前準備として、artisanコマンドでバッチクラスを自動生成します。

// app/Http/Console/CommandsディレクトリにPHPファイルを自動生成
php artisan make:command UpdateUserActionCommand

バッチクラス

ファイル名:app/Http/Console/Commands/UpdateUserAction.php

<?php

namespace App\Console\Commands;

use Illuminate\Console\Command;
use App\Tweet;
use App\RedisModel;
use Illuminate\Support\Facades\DB;

class UpdateUserActionCommand extends Command
{
    /**
     * The name and signature of the console command.
     *
     * @var string
     */
    protected $signature = 'user-action:update {--like} {--impression} {--update}';

    /**
     * The console command description.
     *
     * @var string
     */
    protected $description = 'update user action to tweet';

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

    /**
     * Execute the console command.
     *
     * @return mixed
     */
    public function handle()
    {
        // extract all tweet count
        $allTweetCount = Tweet::count();

        // chunk tweet user action info
        $chunkSize = 1000;
        $chunkLoopCount = intval($allTweetCount / $chunkSize) + 1;
        for ($chunk_i = 0; $chunk_i < $chunkLoopCount; $chunk_i++) {
            $offset = $chunk_i * $chunkSize;
            $limit = ($chunk_i + 1) * $chunkSize -1;

            $likeTweetMap = RedisModel::zrangeTweetAction(config('tweet.USER_ACTION.LIKE'), $offset, $limit);
            $impressionTweetMap = RedisModel::zrangeTweetAction(config('tweet.USER_ACTION.IMPRESSION'), $offset, $limit);

            // log output
            foreach ($likeTweetMap as $key => $value) {
                echo "tweetId: $key, likeCount: $value \n";
                \Log::info("tweetId: $key, likeCount: $value");
            }

            echo "----------------------------- \n";

            foreach ($impressionTweetMap as $key => $value) {
                echo "tweetId: $key, impressionCount: $value \n";
                \Log::info("tweetId: $key, impressionCount: $value ");
            }

            if (!$this->option('update')) {
                echo "if you add --update with --like and --impression, like and impression info will be updated \n";
                return 0;
            }

            // update like
            if ($this->option('like')) {
                if (count($likeTweetMap)) {
                    $this->bulkUpdate(config('tweet.USER_ACTION.LIKE'), $likeTweetMap);
                }
            }

            // update impression
            if ($this->option('impression')) {
                if(count($impressionTweetMap)) {
                    $this->bulkUpdate(config('tweet.USER_ACTION.IMPRESSION'), $impressionTweetMap);
                }
            }
        }
    }

    private function bulkUpdate($actionType, $tweetMap)
    {
        $idList = "";
        $countList = "";
        foreach ($tweetMap as $tweetId => $count) {
            if (is_null($count)) {
                continue;
            }
            $idList .= "," . $tweetId;
            $countList .= "," . $count;
        }

        $field = sprintf(config('const.BULK_UPDATE.FIELD'), $idList);
        $elt = sprintf(config('const.BULK_UPDATE.ELT'), $field, $countList);
        $query = sprintf(config('tweet.BULK_UPDATE_QUERY.USER_ACTION_COUNT'), $actionType, $elt, ltrim($idList, ","));

        // bulk update
        DB::connection()->getPdo()->query($query);
        \Log::info($query);
    }
}



handle()はバッチ処理のメインとなるメソッド(自動生成される)
この部分に処理を記述していきます。

設計思想としては、

「ブロック(1000件)ごとにバルクアップデートで一括更新」

を反映したい。。


ツイート全体の件数allTweetCountからfor文のループ回数loopCountを算出

ループ内では、offsetlimitを設定し、Redisからzrangeコマンドでいいねとインプレッションデータを1000件ずつデータを取得する。

コマンドを確認で叩く時用に、--updateオプションを用意しました。
逆にオプション--updateが付けることで実際にDBを更新する権限が得られ、

--like--impressionも合わせるとそれぞれのデータを更新できます。
もちろん、片方だけとかもできますよ。


そして、バルクアップデートのクエリー作成と処理をプライベートメソッドbulkUpdate()に切り出す。


ちなみに実際のクエリは、以下のようになる
(例)

UPDATE tweets SET like_count = ELT(FIELD(id,259,256,258,260,261),0,1,1,1,1) WHERE id IN (259,256,25
8,260,261)

UPDATE tweets SET impression_count = ELT(FIELD(id,256,257,258,259,260,261,262,263,264,265,266,267),
1,1,1,1,1,1,1,1,1,1,1,1) WHERE id IN (256,257,258,259,260,261,262,263,264,265,266,267)

コードに埋め込んだときにスマートだったので、ELTFIELDを使ってバルクアップデートのSQLを書いています。

Redisクラス

ファイル名:app/RedisModel.php

<?php

namespace App;

use Illuminate\Support\Facades\Redis;

class RedisModel
{

...

+    public static function zrangeTweetAction($actionType, $offset, $limit)
+    {
+        $baseKey = implode(':', [config('tweet.TWEET_BASE_KEY'), $actionType]);
+        return Redis::command('ZRANGE', [$baseKey, $offset, $limit, 'WITHSCORES']);
+    }

}

zrange "laravel_database_shoot_tweet:like" 0 999 WITHSCORES

を表しています。

Configファイル

ファイル名:configs/tweet.php

<?php

return [

...

+    'BULK_UPDATE_QUERY' => [
+        'USER_ACTION_COUNT' => 'UPDATE tweets SET %s_count = %s WHERE id IN (%s)',
+     ]

];

ファイル名:configs/tweet.php

<?php

return [

    'BULK_UPDATE' => [
        'FIELD' => 'FIELD(id%s)',
        'ELT' => 'ELT(%s%s)',
    ]

];

クエリをコードにベタがきするのは気がひけるので、
バルクアップデート用のクエリを定数化しました。

Kernel

ファイル名:app/Console/Kernel.php

...

    protected function schedule(Schedule $schedule)
    {
        $schedule->command('user-action:update --like --impression --update')->daily();
    }

...

実はLaravelには、Cronのようにバッチを定期的に実行するための仕組みが用意されています。

Console/Kernel.phpというファイルでスケジューラ登録の設定ができます。

今回は、毎日深夜12時に実行するようにします。


最後に、以下のコマンドを叩けば、登録完了です。
自動でバッチが動いてくれます。

// 本当は-eで設定するのはNG。-rと間違えたらcronの設定全部消える〜
crontab -e

// エディタ出てくるので下記テキストを入力、保存
* * * * * cd /{アプリのディレクトリパスを入力してください} && php artisan schedule:run >> /dev/null 2>&1

まとめ

以上、Laravelのバッチ実装でした!


今回のポイントと成果物をまとめます。

ポイント

  • Laravelでのバッチ作成法
  • Laravelでのバルクアップデート
  • Laravelでのバッチスケジューラ

制作物

  • バッチクラス
  • Redisクラス
  • Configファイル
  • Kernelクラス(バッチスケジューラ)


Webアプリ開発では、
リアルタイムではDB更新の処理が重い場合、
バッチで定期的にまとめて処理する

というケースも出てきます。

そして
まとめて更新!をする際は

「バルクアップデート」は有効な手段です。

Laravelでのバッチ、バルクアップデート、スケジューラ、
ぜひ使いこなしたいですね!

次回以降ですが、
Vuetifyというライブラリを用いて、
CSSを書かずに楽にログイン画面を作ります!



もし、こんな機能を実装してほしいというリクエストがあったら
連絡ください!

実装して皆さんに共有したいと思います。


それではまた!

プログラミング
スポンサーリンク
シェアする
SHOOTをフォローする
WebエンジニアSHOOTのブログ

コメント

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