章: 第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実践チューニング を学びます。アプリを常駐プロセス化したときのパフォーマンス最大化手法を解説します。