Laravel内で行われているRedisの仕組みを覗いてみる

本投稿はTECOTEC Advent Calendar 2022の2日目の記事です。

こんにちは、証券フロンティア事業部の西永です。
今回はLaravel内で行われているRedisの仕組みを覗いた際、気づいたことを備忘録として残したいと思います。

そもそもRedisとは

Redis は、リモートディクショナリサーバー の略で、高速でオープンソースのメモリ内 key-value データストアです。このプロジェクトは、Redis の最初のデベロッパーである Salvatore Sanfilippo 氏が、イタリアのスタートアップのスケーラビリティを改善したいと考えていたときに始まりました。そこから、彼はデータベース、キャッシュ、メッセージブローカー、キューとして利用されている Redis を開発しました。 aws.amazon.com

主にキャッシュとして使うkey-valueデータストア(KVS)ですね。
AWSだとAmazon MemoryDB for Redis と Amazon ElastiCache for Redisで使えますが、今回はAWS本体ではなくRedisに着目します。

キャッシュの仕組みについて

ここのブログが非常に参考になります。大体ここを見れば分かると思います。 www.bit-hive.com

超かいつまんで書くと、タグの中身を|で結合&sha1()で変換したものをキーとして採用することで、タグの削除を行う際にキーの全件検索(keys *)を防いでいます。
(Redisでキーの抽出を行う際はO(N)なのでキーが多いほど時間がかかります)

取得例

use Illuminate\Support\Facades\Cache;

Cache::store('cache')->tags('user_subscriptions', 'user_subscriptions_user_id_ユーザー名')->remember('isSubscribed', $cacheTime, $callback);

中身の処理(かなり簡略化)

ちなみにこの6373257fa23fc828194949の値はどうやって生成しているかについてですが、こちらもLaravelの方で独自に生成しています。 github.com

uniqid('', true)4b340550242239.64159797のような文字列を生成した後、str_replace()4b34055024223964159797に置換して使用しています。
値が被ったら意味がないのでその対策ですね。

standard_refについて

あともう一つ、Laravel経由でRedisを使う(セットする)際、タグ*1と実数値*2以外にもstandard_refなるものが設定されます。

standard_ref
「キャッシュの仕組みについて」で取り上げた仕組みだけだとこのstandard_refは不要そうに思えますが、こちらは一体何に使えるのでしょうか。
 
 
まず、Laravelのソースを確認すると下記で使われており、辿っていくとput()/set()/remember()する際に同時にstandard_refがセットされるような構造になっています。 github.com

つまり、Laravel経由で値を設定する際は大体standard_refが設定されることになります。

次にこのstandard_refの中身はどうやって見るのかについてですが、こちらはRedisのSet型を利用しているため、その関連のコマンド等は大体使用できます。 redis.shibu.jp

使用例

上記画像を見れば分かる通り、standard_refにはそのタグに関連した実数値のキー情報が入っています。
つまりstandard_refを用いれば、タグに紐づいている実数値のキーを逆引きで持ってくることができます。
この逆引きを利用して、Laravel内部ではCache::store('cache')->tags()->flush()を行う際、タグに紐づいている実数値のキーを一緒に消すようにしています。
flush() -> deleteStandardKeys() -> deleteKeysByReference() -> deleteValues() の箇所で消しています) github.com

削除する際、keysを使って検索して削除だと計算量が多くなりがち*3なので、計算量の肥大化を防ぐ工夫が行われているのは興味深いですね。
 
しかしこのstandard_refですが、自由に保持期間が設定できる実数値のキーとは違い保持期間が無期限のため、キーの削除生成を繰り返しているとどんどん不要なstandard_refが増えてメモリを圧迫していきます。

-1は保持期間無期限、-2は存在しない

この問題はLaravelのissueでも取り上げられており、コラボレーターの回答としては

If you can't flush your cache database on a regular basis, don't use tags or write a custom script to clean up leftovers.
(DeepL翻訳) キャッシュデータベースを定期的にフラッシュできない場合は、タグを使用しないか、カスタムスクリプトを書いて残飯をクリーンアップしてください。 github.com

とのこと。
カスタムスクリプトについてはそのissue内に有志の方が作られたスクリプトが載せられているので、それを用いることでメモリ圧迫問題を回避できる(かも)。

悲しみの-1マークと有志スクリプト

一方Laravel 8でタグの削除については改善されており、standard_refに加えてタグ*4も無期限保持ですが、Cache::store($cacheType)->tags($tags)->flush()を使用した際にちゃんとタグは物理削除するよう修正されています。 github.com

Redisでメモリが圧迫気味の方は確認してみてはいかがでしょうか。

おわりに

今取り組んでいる案件でもLaravelでRedisを使用した構築を用いており、その一環として調査した内容を取り上げました。
同様の方がいらっしゃいましたら参考になりますと幸いです。
 
 
Amazon Web Servicesおよびかかる資料で使用されるその他の AWS 商標 は、米国および/またはその他の諸国における、Amazon.com, Inc. またはその関連会社の商標です。

www.tecotec.co.jp

*1:上記の例だとlaravel_database_laravel_cache:tag:user_subscriptions:key、laravel_database_laravel_cache:tag:user_subscriptions_user_id_ユーザー名:key

*2:上記の例だとlaravel_database_laravel_cache: 202e2eb93be77192890455ea1e7c7769c084f4f5:isSubscribed

*3:「keys」は計算量O(N)

*4:上記の例だとlaravel_database_laravel_cache:tag:user_subscriptions:key、laravel_database_laravel_cache:tag:user_subscriptions_user_id_ユーザー名:key