第20回: Cache Stampede対策 — キャッシュ失効の瞬間、DBは大丈夫ですか?

章: 第8章: パフォーマンスと本番運用実践

キャッシュが消えた瞬間に何が起きるか

人気ランキングのキャッシュが失効した瞬間、100件のリクエストが同時にDBへ走ったとしたら——あなたのアプリは耐えられますか?

この現象を Cache Stampede(キャッシュスタンピード)と呼びます。同一キーのキャッシュが失効したタイミングで、複数のリクエストが一斉に再計算を試みることで、DBに集中アクセスが発生し、応答遅延やダウンの原因になります。

キャッシュを導入しているにもかかわらず、ピーク時だけDBが高負荷になる——そういう症状が出ているなら、Cache Stampede が原因の可能性が高いです。

問題の本質と解決策

問題: キャッシュ失効時、ロック制御がないと全リクエストが同時に再計算を開始します。

解決策: Cache::lock() によるアトミックロック+ Jitter(ランダムな有効期限のゆらぎ) の2点セットで対策します。ロックを取得できたリクエストだけが再計算し、他は待機してキャッシュを再利用します。Jitter により同時失効そのものを分散できます。

対策あり・なし 比較

観点 対策なし 対策あり(ロック+Jitter)
失効時のDB負荷 全リクエスト数分のクエリが走る 1回だけクエリが走る
同時失効 全キーが同タイミングで失効しうる 失効タイミングが分散される
実装コスト 低い 低〜中(数十行で実装可能)
障害リスク 高い(DBダウンに直結) 低い

チェックポイント: Cache::lock()addSeconds(random_int(0, 30)) のジッターはセットで使っていますか?片方だけでは効果が半減します。

実装サンプル


<?php
$key = 'ranking:daily';
$data = Cache::get($key);

if ($data === null) {
    $lock = Cache::lock('lock:' . $key, 10);
    if ($lock->get()) {
        try {
            $data = buildDailyRanking();
            Cache::put($key, $data, now()->addMinutes(5)->addSeconds(random_int(0, 30)));
        } finally {
            $lock->release();
        }
    } else {
        usleep(100000);
        $data = Cache::get($key, []);
    }
}

まとめ & 次のステップ

  • キャッシュ失効時に複数リクエストがDBへ殺到する現象が Cache Stampede です
  • Cache::lock() でアトミックロックを取り、再計算は1リクエストのみに制限します
  • random_int() で有効期限にジッターを加え、同時失効そのものを防ぎます
  • ロックとジッターは必ずセットで使うことが安定運用のポイントです
  • 既存のキャッシュコードに数十行追加するだけで対策できます

次回は Octane実践チューニング を学びます。アプリを常駐プロセス化したときのパフォーマンス最大化手法を解説します。

Related Articles