TT Lab
はじめる
学ぶ 学習パス コース

Redisとキャッシュ

コピーは原本が変わった瞬間に古くなる

TT Labで続きを見る

一言でいうと

キャッシュ戦略は、ステールとミスの綱渡りです。正解はなく、データの性質に合ったバランス点があるだけです。

なぜ必要なのか

キャッシュがうまく働くのは、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に届いていない書き込みが消えるため、金銭データには使いません。

何をキャッシュしてはいけないのか

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つずつ触ってみて、そのあと、ランキング、キャッシュパターン、スタンピード防御をそれぞれラボで行います。