IT/AI救済センター

コラム / インフラ /

多段階キャッシュ(CDN / Redis / アプリ / DB)の設計 ─ どこに何を何秒置くか、TTL とキャッシュ破棄で事故らない実務

📖 約9分 / 公開日: 2026/06/06

表示が遅いというだけで「とりあえず Redis を入れましょう」と提案してくるベンダーは、一度疑ったほうがいい。キャッシュは速度を買う技術ではなく、整合性を差し出して速度を買う取引です。どこに何を何秒置くかを決めないまま層だけ増やすと、速くなったように見えて、古い価格が消えない・在庫が二重に見える・ログアウトできない、という別種の障害を抱え込みます。

多段階キャッシュは、ブラウザ → CDN → アプリ内メモリ(または Redis) → データベース、という順で「手前で答えられたら奥に行かせない」設計です。手前ほど速くて安いが古くなりやすく、奥ほど正確だが遅くて高い。この距離にデータをどう配るかがすべてで、Redis を入れるかどうかは最後の論点にすぎません。

## まず DB のクエリキャッシュという幻想を捨てる

古い記事を読んで「MySQL のクエリキャッシュを有効にしよう」と考えているなら止めてください。MySQL 8.0 でクエリキャッシュは完全に削除されました。書き込みのたびに該当テーブルのキャッシュが丸ごと飛ぶ仕様で、更新の多いテーブルではむしろ足を引っ張る。これが消えたのは正しい判断です。DB の手前で減らすべきは、クエリの結果そのものをアプリ側で持つことであって、DB 内部のキャッシュ機構に期待することではありません。

## 層ごとに役割を分ける

CDN(Cloudflare や CloudFront)に置くのは、画像・CSS・JS・フォントといった、まず変わらない静的ファイルです。ここは Cache-Control: public, max-age=31536000, immutable を付け、ファイル名にハッシュを埋めて(app.9f3c1.js のように)、中身が変わったら URL ごと変える。これだけでオリジンへのリクエストの大半が消えます。

HTML を CDN でキャッシュするのは、効果が大きいぶん事故も大きい。ログイン後のページや在庫表示をうっかりエッジに乗せると、Aさんのカートが Bさんに見える。やるなら、ログイン前の公開ページに限定し、Cookie が付いたリクエストはキャッシュしない、という条件を必ず入れます。

Redis やアプリ内メモリに置くのは、生成にコストがかかる動的な結果です。集計済みのランキング、外部 API のレスポンス、権限計算の結果、セッション。DB に毎回投げると重いが、数秒〜数分は古くても困らないもの。逆に、1リクエストで何度も読む不変の設定値なら、Redis すら経由せずプロセス内のメモリに持つほうが速い。ネットワークを1往復もしないからです。

## TTL は「許容できる古さ」から逆算する

TTL(生存時間)を勘で決めると必ずどちらかに振れます。短すぎてキャッシュが効かないか、長すぎて苦情が来るか。正しい順序は、その data が何秒古くても業務が成立するかを先に決めることです。

トップページのお知らせ欄なら60秒古くても誰も困らない。記事一覧なら5分でいい。これに対して EC の在庫数は、5秒の古さでも売り越しが起きうる。在庫のような確定値は、TTL で時間切れを待つのではなく、更新が発生した瞬間にキャッシュを消す push 型にすべきで、安易に「在庫も30秒キャッシュ」とやると繁忙期に二重販売が起きます。為替や残高のような金額系も同じで、ここはキャッシュしないほうが安いことが多い。

## 本丸はキャッシュ破棄(invalidation)

「コンピュータサイエンスで難しいのはキャッシュ破棄と命名だけだ」という有名な軽口がありますが、現場でも本当にここが難所です。データを置くのは簡単で、いつ正しく捨てるかが難しい。

TTL に任せる時間ベースの失効が一番楽ですが、更新が即時に反映されません。記事を直したのに5分間古いまま、という挙動を許せるかどうか。許せないなら、保存処理の中で該当キーを明示的に DEL するイベントベースの破棄を組みます。問題は、1つのデータが何種類ものキャッシュ(記事単体、一覧、サイトマップ、関連記事)に波及していること。更新時にどのキーを消すかの対応表を持たないと、必ず消し忘れが残り、それが「直したはずなのに直っていない」の正体になります。

## キャッシュスタンピードを甘く見ない

アクセスの多いキーが TTL で同時に切れた瞬間、待っていた全リクエストが一斉に DB へ殺到する。これがキャッシュスタンピード(thundering herd)で、キャッシュを入れた直後にむしろ DB が落ちる典型パターンです。秒間数千リクエストのあるページで、人気キーが一斉失効した瞬間に DB の接続が枯れる、という障害を何度も見てきました。

対策は2つ。1つは、最初の1リクエストだけが再生成し、残りは少し古い値を返して待たせるロック方式(Redis の SET NX で再計算権を1つに絞る)。もう1つは、stale-while-revalidate、つまり期限切れの値をいったん返しつつ裏で更新する方式です。あわせて TTL に±10%程度のゆらぎ(jitter)を持たせ、失効時刻をばらけさせる。これを入れるだけで同時失効の山がならされます。

## キャッシュしてはいけないものを決めておく

速くしたい一心で何でもキャッシュに乗せると、最後に必ず痛い目を見ます。ログイン状態、CSRF トークン、決済の確定情報、在庫の最終値、個人情報を含む画面。これらは原則キャッシュ対象外にする。

最も多い事故が、Set-Cookie の付いたレスポンスを CDN がキャッシュしてしまうケースです。あるユーザーのセッション Cookie がエッジにキャッシュされ、次の人にそのまま配られ、別人としてログインしてしまう。重大なセッション混線で、Cache-Control: private を付け忘れた一行が原因になります。動的ページの既定は「キャッシュしない」にして、安全だと確認できたものだけ明示的に乗せる。順序を逆にしないでください。

## ヒット率を見ていないキャッシュは、効いているか分からない

最後に監視です。キャッシュは入れて終わりではなく、ヒット率を継続して見ないと、効いているのか、ただメモリを食っているだけなのか判断できません。Redis なら INFO の keyspace_hits と keyspace_misses からヒット率を出し、おおむね90%を切るキーは TTL かキー設計を疑う。CDN ならダッシュボードのキャッシュ率を見る。

あわせて、メモリ上限に達したとき何を捨てるかの方針(Redis の maxmemory-policy、多くは allkeys-lru)を決め、p99 のレスポンスと DB の負荷をキャッシュ導入の前後で比べる。数字で前後を比較できないなら、その施策は「速くなった気がする」で止まっています。

層を増やすこと自体が目的化すると、整合性のバグだけが増えて速度は思ったほど出ません。手前で減らし、許容できる古さから TTL を決め、捨て方を設計し、ヒット率で答え合わせをする。この順番を守れるかどうかが、キャッシュで速くなる現場と、キャッシュで障害を増やす現場の分かれ目です。

関連コラム

具体的な状況をご相談ください

記事の内容に該当しそうなら、まずは60分の無料相談でご相談を。

60分無料相談を予約
緊急 AI診断 60分予約