Laravelのキャッシュ機能を利用してパフォーマンスを改善する

今日はLaravelのキャッシュ機能を紹介したいと思います。

Laravelのキャッシュ機能の優れている点は、キャッシュできる内容が非常に柔軟かつプログラマーに寄り添っている点にあります。

基本的な使い方

例えば、1回あたり10秒以上かかるような重い処理だけど何度実行してもそうそう結果が変わらない、という処理は、特に大規模かつ長く続けているサービスでは往々にして発生するかと思います。
そういった処理はキャッシュを利用することでパフォーマンスを大きく改善できます。
重い処理がどこなのか判明している状況でLaravelによるキャッシュ化をする場合、例えば以下のように書くことでキャッシュ化の恩恵が得られます。

$result = tenSecProc(); // 10秒以上かかる重い処理
↓↓↓ 以下のように改善 ↓↓↓
$result = \Cache::remember('tenSecProcResult', 3600, fn() => tenSecProc());

上記の変更をすることで、tenSecProc()の結果を1時間キャッシュすることができます。1時間のうちに同じアクセスがあった場合はtenSecProc()の処理は一切実行されず、キャッシュしておいた結果だけを即座に返すことができます。

1リクエスト限りのキャッシュ

またDBへの無駄なアクセスを削減するためにもLaravelのキャッシュ機能は効果的です。
例えばループ内で何度も同じテーブルへアクセスするような場合、技術理解が足りないとn+1問題のような明確に問題のあるコードを書いてしまうことは意外とよくあるのではないでしょうか?
そのようなn+1問題への暫定対策としてもLaravelのキャッシュ機能は効果を発揮します。

例えば以下のようなコードを例に考えてみます。

foreach ($request->items as $item) {
    $shop = Shop::find($item['shop_id']); // 同じshop_idが複数回リクエストされた場合、無駄なSQLが実行されてしまう
    if (!empty($shop)) {
        $item['note'] = "販売店:{$shop->name}";
    }
    Item::create($item);
}
↓↓↓ 以下のように改善 ↓↓↓
foreach ($request->items as $item) {
    $shop = \Cache::store('array')
        ->rememberForever("shop.{$item['shop_id']}", fn() => Shop::find($item['shop_id'])); // 同じshop_idが複数回リクエストされた場合はキャッシュを返すことで無駄なSQLを回避
    if (!empty($shop)) {
        $item['note'] = "販売店:{$shop->name}";
    }
    Item::create($item);
}

上記の改修では、’array’というキャッシュドライバを利用しています。このドライバはメモリ領域にキャッシュを保存し、リクエストが完了したらキャッシュはクリアされます。
arrayキャッシュドライバを利用ことで、無駄なSQLの多重実行を回避しつつ、リクエストのたびに最新のデータを参照する事ができます。
上記のようなコードの場合、最初に必要な店舗を全てSQLで取得するのがベストとは思いますが、改修範囲が限定的という点で手軽に改善できるメリットはあるかと思います。

なお、rememberForever関数はキャッシュの有効期限が無期限という特徴があります。これは使い方を誤ると「更新したはずなのに更新前のデータが返される」というキャッシュによくある問題に発展するのでその点は注意が必要です。

キャッシュによるメリットだけを享受する

上で述べた通り、キャッシュにはパフォーマンスを改善できるメリットと、更新前の古いデータを参照してしまうリスクがあります。しかし、そのリスクを避けてメリットだけを享受できるソリューションがあります。
それはCache::flexibleを利用することです。
最初にCache::rememberを用いて改善した処理を、Cache::flexibleを使って改善すると以下のようになります。

$result = \Cache::flexible('tenSecProcResult', [60, 3600*24], fn() => tenSecProc());

上記の処理は、キャッシュしてからの時間経過によって以下の3パターンに分岐します。
・キャッシュしてから1分以内ならキャッシュを即座に返す。
・キャッシュしてから1分~24時間以内ならキャッシュを即座に返すが、"裏で最新のデータを取り直してキャッシュする"。
・キャッシュしてから24時間以上経っていたら、古いキャッシュを無視して最新のデータを取り直し、キャッシュすると共にそれを返す。

例えば、少なくとも1日1回はアクセスされるページで重い処理が走っている場合、上記のようにCache::flexibleを利用することでキャッシュによるパフォーマンス改善の恩恵を受けつつ、古いデータが返されるリスクをほぼ0にできます。

さらに、Cache::flexibleの優れている点は「キャッシュ期限切れによるパフォーマンス劣化をユーザーに気づかせない」点にあります。
Cache::rememberを使う場合、有効期限が切れた状態でアクセスすればそのリクエストに限っては愚直に重い処理が行われてレスポンスタイムが悪化します。しかしCache::flexibleなら裏でキャッシュのリフレッシュがされ、有効期限も延長され続けます。よって「愚直に重い処理が行われる事によるレスポンスタイムの悪化」をユーザーが感じるタイミングを無くすことができます。

ただし、Cache::flexibleを利用する場合は1つ注意点があります。
「裏でキャッシュ更新する処理」を走らせるためにLaravelのキューの設定をしなければなりません。この設定が漏れていると、裏でのキャッシュ更新リクエストがたまり続けるがキャッシュが更新されない、という事になってしまいます。
Cache::flexibleを使っても古いデータがレスポンスされたりパフォーマンスの悪化を感じるなら、まずはキューの設定を見直す事をおすすめします。

まとめ

Laravelのキャッシュは非常に強力で、正しく利用するとパフォーマンス改善に大きく貢献してくれます。

弊社ではWebアプリケーションの開発案件を承っております。お問い合わせフォームよりお気軽にお問い合わせ下さい。