コピーは原本が変わった瞬間に古くなる
一言でいうと
キャッシュ戦略は、ステールとミスの綱渡りです。正解はなく、データの性質に合ったバランス点があるだけです。
なぜ必要なのか
キャッシュがうまく働くのは、2つの性質のおかげです。時間的局所性は、いま使ったデータはまたすぐ使われる可能性が高いという性質です。空間的局所性は、あるデータを使うと、その近くのデータもすぐ使われる可能性が高いという性質です。
問題は、キャッシュが原本のコピーだというところから生まれます。コピーは、原本が変わった瞬間に古くなります。ここで2つの悪い状況を区別する必要があります。ステールは、キャッシュが原本より古い値を持っている状態で、ユーザーが間違った値を見ます。ミスは、キャッシュに値がなく、原本まで往復しなければならない状態で、遅くなります。
ステールを減らそうとして頻繁に捨てるとミスが増え、ミスを減らそうとして長く保持するとステールが増えます。この綱渡りが、キャッシュのあらゆる難しさの根源です。
どう動くのか
最も単純な無効化はTTLです。各項目に寿命を付け、過ぎたら捨てます。魅力は単純さと予測可能性です。無効化のロジックを別に書く必要がなく、最悪の場合でもステールはTTLを超えません。そのため、少し古くても構わないデータによく合います。ニュース一覧、人気の投稿、為替レートなどです。逆に口座残高のようなものはTTLだけでは足りず、明示的な無効化が必要です。
明示的な無効化には2つの流れがあります。イベントベースは、原本が変わったら関連するキャッシュをすぐ消します。鮮度は高いですが、依存関係の追跡が難しいです。「商品42」は、商品詳細のキャッシュにも、カテゴリ一覧のキャッシュにも、検索結果のキャッシュにもあるかもしれません。見逃した依存関係1つが、静かなステールバグになります。
バージョンキーは、はるかに堅牢で実用的な手法です。キー自体にバージョンを埋め込み、無効化を削除ではなく移動として扱います。user:42:v7:profileを使っていて、データが変わったらバージョンをv8に上げます。v7のキーは誰も探さないので、TTLで自然に片付きます。削除を自分でしなくてもよく、無効化と再取得がすれ違う競合状態にも強いです。古いキーと新しいキーが物理的に別物だからです。
現場での姿
無効化が難しい本当の理由は3つです。1つ目は分散です。キャッシュは複数のサーバー、複数の階層に散らばっているので、アトミックに無効化することが分散合意の問題になります。2つ目は競合状態です。キャッシュを消す瞬間と、ほかのリクエストが古い値を読み直して入れる瞬間がすれ違うと、いま消した値が復活します。3つ目は依存関係の複雑さです。
そのため実務では、完全な無効化を諦め、「どれだけ長くステールに耐えられるか」を決めて、TTLと明示的な無効化を混ぜます。
キャッシュパターン4つ
読み書きを誰が行うかで分かれます。名前を知っておくと、チームの会話が短くなります。
| パターン | 読み取り | 書き込み | 特徴 |
|---|---|---|---|
| Cache-aside | アプリがキャッシュを確認 → なければDB → キャッシュに入れる | アプリがDBに書き、キャッシュを無効化 | 最も一般的。最初のリクエストは必ず遅い |
| Read-through | キャッシュがDBの代わりに読む | — | アプリのコードが単純になる |
| Write-through | — | キャッシュとDBに一緒に書く | キャッシュが常に最新。書き込みが遅い |
| Write-behind | — | キャッシュに書き、あとでDBへ | 書き込みが速い。消失リスク |
実務の95%は最初の行です。残りは、キャッシュ層がDBアクセスをラップするライブラリを使うときに出てきます。Write-behindは、キャッシュが落ちるとまだDBに届いていない書き込みが消えるため、金銭データには使いません。
何をキャッシュしてはいけないのか
- 頻繁に変わるのに正確でなければならないもの: 在庫数量、残高。ずれると事故になります。
- ユーザーごとに違う大きなオブジェクト: ヒット率が低く、メモリを食うだけです。
- 計算がもともと安いもの: キャッシュの往復(0.5ms)が計算より高くつくことがあります。
- 個人情報: キャッシュはたいてい暗号化されておらず、保持ポリシーが緩いです。
3つ目をよく見落とします。インメモリ計算で1μsのものをRedisに入れると、500倍遅くなります。 キャッシュは、遅いもの(DB参照、外部API、重い計算)にだけ価値があります。
キャッシュキーを設計する規則
labhub:v3:user:42:profile
│ │ │ │ └ 무엇
│ │ │ └ 식별자
│ │ └ 종류
│ └ 스키마 버전 ← 형식이 바뀌면 올린다. 옛 키는 TTL 로 사라진다
└ 애플리케이션 접두 ← 같은 Redis 를 여러 앱이 쓸 때 충돌을 막는다
バージョンを入れることが核心です。キャッシュに入れるオブジェクトの構造が変わると、古い値を読んで壊れますが、バージョンを上げると新しいキーを使うようになり、古いキーはTTLで自然に消えます。FLUSHALLで全部消すより安全です。それはすべてのキャッシュを同時に空にして、DBにスタンピードを起こします。
ワイルドカードでの削除にも注意が必要です。KEYS user:*は全体を走査するO(n)の操作なので、Redisを止めます。SCANを使うか、そもそもタグ用の集合(Set)を一緒に管理します。
次の確認で見ること
まずクイズで、ステールとミスのトレードオフ、TTLが許されるデータの条件を確認します。次のモジュールからRedisのデータ構造を1つずつ触ってみて、そのあと、ランキング、キャッシュパターン、スタンピード防御をそれぞれラボで行います。