Dev Study
AWSサービスの内部原理 コース

96. キャッシュは局所性への賭けである — 遅延読み込みとライトスルー、2大戦略の対価表

キャッシュとは「よく使うデータのコピーを、速い場所にあらかじめ置いておく」賭けだ——その賭けをどう管理するかで生まれる2大戦略(遅延読み込みとライトスルー)と、両者の欠点を埋めるTTLを、公式の擬似コードを箱と矢印に翻訳しながら図で追っていきます。

① キャッシュの正体 — コピーへの超高速アクセスを買う

アプリケーション
だから手前に速い層を置く
DBに問い合わせJOIN・計算は特に高価
「常に」遅くて高い
インメモリKVSコピーをキーで引くだけ = サブミリ秒
JOIN/計算の代金は一度だけ
  • 公式の言い回し(裏取り済み): インメモリkey-valueストアの主目的は、データのコピーへの ultrafast(submillisecond latency)で安価なアクセスを提供すること。
  • 「querying a database is always slower and more expensive than locating a key in a key-value pair cache(DBへの問い合わせは、key-valueキャッシュでキーを引くより常に遅く高価)」。
  • 特に「joins across multiple tables(複数テーブルのJOIN)」や「intensive calculations(重い計算)」を伴うクエリは高価。結果をキャッシュすれば「クエリの代金は一度だけ払い、以後は再実行せず何度でも取り出せる」。

キャッシュは魔法ではなく、遅くて高い層(DB)の手前に、速くて安い層(メモリ)を置く「配置」の工夫。JOINや計算の答えは一度払えば使い回せる——これがキャッシュが成立する経済的な理由です。

② 賭けてよいデータの3条件

載せるべきか?このデータをキャッシュしていい?
3つとも当てはまる = 良い候補
取得が遅い/高価JOIN・計算など
頻繁にアクセスよく読まれる
比較的静的速く変わるなら古さ許容が条件
常にstale扱い定義上、キャッシュは古いデータ
  • 公式の3条件(裏取り済み): データが「(1)キャッシュ取得に比べて遅い/高価」「(2)ユーザーがよくアクセスする」「(3)比較的変わらない、あるいは速く変わっても古さが大きな問題にならない」。
  • staleの宣言(原文): 「By definition, cached data is stale data.(定義上、キャッシュされたデータは古いデータである)」——たとえ古くない状況でも、常に古いものとして扱え。
  • DynamoDB編で見た「結果整合性(書いた直後に読んでも最新が返るとは限らない)」は、まさにこの「コピーは古くなりうる」の別名。コピーを置いた瞬間から、原本(DB)とのズレが始まる。

キャッシュは「速さ」と引き換えに「新しさ」を差し出す取引。だから何を載せるかは、そのデータが古くなっても許せるか——アプリの「古さへの許容度」で決まります。

③ 遅延読み込み — 必要になって初めて載せる

アプリが要求
ミスなら ②DBに問い合わせ
ヒットあって期限内
即座に返す(1往復)
ミス無い or 期限切れ
①nullが返る
③結果をキャッシュに書き戻す
DBから取得
アプリに返すミス時は合計3往復
  • 公式のミス経路(裏取り済み): (1)アプリがキャッシュに要求 →(2)無いので null が返る →(3)アプリがDBから取得 →(4)アプリがキャッシュを更新。
  • 擬似コード get_customer(公式ほぼそのまま): customer_record = cache.get(customer_id) → nullなら db.query("SELECT * FROM Customers WHERE id = {0}", customer_id) → cache.set(customer_id, customer_record) → return customer_record。
  • 利点: 要求されたデータしかキャッシュされない(大半のデータは要求されないので、無駄で埋まらない)/ノード障害は致命的でない——新しい空のノードに置き換わっても、ミスするたびDBから取り直すので、遅くなるだけで動き続ける。
  • 欠点: cache miss penalty = ミスのたびに3往復(キャッシュへ→DBへ→キャッシュへ書き戻し)で目立つ遅延/stale data = ミス時しか書かないので、DBが更新されてもキャッシュは知らず古くなる。

遅延読み込みは「使われたものだけ置く」倹約家。空のノードでも壊れず動く頑丈さが最大の美点ですが、ミスの3往復ペナルティと「DBの更新に気づけない古さ」が対価です。

④ ライトスルー — 書くたびキャッシュも更新

アプリが保存
②続けてキャッシュへ
DBに書き込む
書くたび同期
キャッシュも更新
毎回2回書く対価
常に最新データが決して古くならない
  • 擬似コード save_customer(公式ほぼそのまま): customer_record = db.query("UPDATE Customers WHERE id = {0}", customer_id, values) → cache.set(customer_id, customer_record) → return success。
  • 利点: データが決して古くならない——DBに書くたびにキャッシュも更新するので常に最新。書き込みは2往復(キャッシュへ+DBへ)だが、ユーザーは更新時の遅延には比較的寛容(取得時より我慢しやすい)。
  • 欠点: missing data = 新しい(空の)ノードには、次に更新されるまで存在しないデータがある(ノード障害やスケールアウトで発生)/cache churn = 大半のデータは読まれないのに書き込まれ、資源の無駄(ほとんど読まれないデータでキャッシュが埋まる)。

ライトスルーは「常に最新」を買う代わりに、毎回2回書く手間と、読まれもしないデータでキャッシュが太る(cache churn)対価を払います。遅延読み込みとちょうど鏡像——欠点も逆向きです。

⑤ TTL — 「期限切れ=存在しない」で穴を埋める

寿命付きで書くcache.set(key, value, {300})
300秒後に期限切れ
期限切れのキーを引く
次の読み取り
DBから取り直し
存在しない扱いキャッシュミスと同じ経路に落ちる
キャッシュを更新新しい寿命付きで書き戻す
  • TTL(裏取り済み): 「キーが期限切れになるまでの秒数を指定する整数値」。Valkey / Redis OSS は秒またはミリ秒を指定できる。Memcached は秒で指定する。
  • 挙動(原文): 「When an application attempts to read an expired key, it is treated as though the key is not found.(期限切れキーを読もうとすると、見つからないものとして扱われる)」→ DBに問い合わせてキャッシュを更新。
  • 効果: 古くないことを保証はしないが、古くなりすぎるのを防ぎ、キャッシュ上の値を時々DBから更新させる。遅延読み込みの「更新に気づけない古さ」と、ライトスルーの「cache churn」を、両方まとめて和らげる。
  • 遅延読み込み+TTLの擬似コードでは、cache.set(customer_id, customer_record, {300}) のように寿命付きで書き戻す(300秒=5分)。

TTLは「どのデータもいつかは自動で期限切れ=DBから取り直し」を仕込む安全弁。古さの上限を秒単位で設計者が決められるので、遅延読み込みの古さもライトスルーの膨張も、この1つの数字で調律できます。

⑥ 対価表 — 速さ⇄新しさの配分

遅延読み込み古さ:あり / churn:なし
ミスで3往復・空ノードは平気
ライトスルー古さ:なし / churn:あり
書きが毎回2往復・空ノードは穴あき
+TTL古さの上限を秒/ミリ秒で調律
両者の欠点を中央に寄せる
速さ ⇄ 新しさ優劣ではなく配分の違い
  • この表はすべて①〜⑤で裏取り済みの利点・欠点の要約。新しい事実は足していない。
  • 「遅い層の手前に速い層を置く」原理は、これまでの各編で断片的に登場していた——Lambda編のウォームスタート(実行環境の再利用)、DynamoDB編のB木(ディスクの遅さと戦う構造)。今回それに正面から「キャッシュ」という名前を付けた。
  • CS基礎との接続: 参照の局所性(一度使ったデータ・その近くはまた使われる)とメモリ階層(レジスタ→キャッシュ→DRAM→SSD/HDD、下ほど大きく遅い)。キャッシュとは、この階層で「賭けが当たれば速い層で済む」ことに賭ける行為。

戦略の違いは優劣ではなく配分の違い——遅延読み込みは新しさを、ライトスルーは書き込みの速さを差し出し、TTLはその天秤の目盛りを設計者の手に渡す。「速さを取るか、新しさを取るか」——この天秤が、本編(キャッシュ編)全体を貫きます。

公式ドキュメントで詳しく ↗