分散キャッシュ整合性(write-through/back/around)
キャッシュとDBの二重書きで生じる不整合の原因を断てます。write-through/back/aroundの耐久性差、TTLと明示無効化、CDN・アプリ・DBの多層整合性を原理から理解し、古い値の漂着を設計で防げます。
- 1.書き込み戦略はwrite-through(同期で両方書く・遅いが整合的)、write-back(先にキャッシュへ書き遅延で永続化・速いが障害でデータ喪失)、write-around(DBだけ書きキャッシュは書かない・キャッシュ汚染を避ける)に分かれ、レイテンシ・耐久性・整合性のトレードオフを取る。
- 2.キャッシュとDBは別々のストアであり、2つを跨ぐ書き込みは原子的でない。クラッシュや並行実行の隙で順序が崩れ、古い値がキャッシュに焼き付く。cache-aside+更新時に値を書くのではなく無効化するのが定石。
- 3.TTLは整合性ではなく不整合の寿命の上限でしかない。確実な反映には明示無効化が要るが、CDN・アプリ・DBの多層では各層のTTLと無効化伝播が掛かり合い、最も遅い層が古い値を保持し続ける。
なぜキャッシュは古い値を返し続けるのか
キャッシュは「速くするための仕組み」と語られますが、その速さの正体は データの複製を、本来の持ち主(データベース)とは別の場所に置くこと です。複製を持った瞬間、システムは同じ事実を表す2つ以上のコピーを抱えることになり、片方を更新したらもう片方も合わせて更新する責務が生まれます。これを怠ると、キャッシュは平然と古い値(stale value)を返し続けます。本記事は、キャッシュへの書き込み戦略(write-through / write-back / write-around)が耐久性と整合性をどう左右するか、キャッシュとDBの二重書きがなぜ不整合を生むか、そしてTTLと明示無効化、CDN・アプリ・DBの多層キャッシュの整合性問題を、原理から解きほぐします。
3つの書き込み戦略:レイテンシと耐久性のトレードオフ
書き込みがキャッシュとバックエンドのデータストアにどう波及するかで、戦略は3つに分かれます。読み出しは多くの場合 cache-aside(lazy loading)——ミス時にDBから読んでキャッシュに詰める——を併用しますが、ここでは書き込み経路に注目します。
| 戦略 | 書き込みの流れ | 速さ | 耐久性・整合性の性質 |
|---|---|---|---|
| write-through | キャッシュとDBへ同期的に両方書く。両方成功で完了 | 書きは遅い | キャッシュは常にDBと一致。読みは速い。書きレイテンシがDB分かかる |
| write-back(write-behind) | まずキャッシュへ書いて即完了。DBへは後で非同期にまとめて書く | 書きは速い | 未永続データがキャッシュにのみ存在し、ノード障害で喪失しうる。バッチ化で書き負荷を吸収 |
| write-around | DBへだけ書き、キャッシュは更新しない(または無効化) | 書きは中庸 | 書いた直後に読まれないデータでキャッシュを汚さない。書いた値の初回読みは必ずミス |
決定的な違いは 耐久性(durability)が何によって担保されるか です。write-through は書き込み完了の時点でDBに値が載っているため、キャッシュが消し飛んでもデータは失われません。代償は、書きレイテンシがDBの書き込み時間に律速されること。
write-back が速いのは、書き込みをキャッシュ(多くは揮発性のメモリ)に置いた段階で「完了」と返すからです。DBへの永続化は後回しなので、その隙にキャッシュノードがクラッシュすると、まだ書かれていない更新は そのまま消えます。耐久性をレイテンシと引き換えに借りている状態であり、金額や在庫のような失ってはならないデータには原則向きません。使うなら、ログ先行書き込み(WAL)でフラッシュ前の更新を保護する、複数ノードへ複製してから完了を返す、といった補強が前提になります。
write-around は、書いたデータがすぐには読まれないワークロード(書き込み主体のログ等)で、めったに読まれない値がキャッシュを占有して有用なデータを追い出す「キャッシュ汚染」を避けるのが狙いです。代わりに、書いた値を直後に読むパターンでは必ずキャッシュミスが発生します。
二重書きの罠:キャッシュとDBは原子的に更新できない
ここからが整合性の核心です。キャッシュとDBは 物理的に別のストア であり、両方を1つのトランザクションで原子的に更新する手立ては通常ありません。つまり「DBを書く」と「キャッシュを書く(または消す)」の2操作の間には必ず隙間があり、その隙間にクラッシュや別の操作が割り込むと不整合が生まれます。
まず避けるべきアンチパターンは、更新時に キャッシュへ新しい値を書き戻す ことです。並行する2つの更新で、この書き戻しの順序がDBへの書き込み順序と逆転すると、古い値がキャッシュに焼き付きます。
書き戻し方式の競合(値をキャッシュに書く):
リクエストA: DBにv1を書く ……(ここで一時停止)
リクエストB: DBにv2を書く → キャッシュにv2を書く
リクエストA: 再開してキャッシュにv1を書く ← キャッシュにv1が居座る
結果: DB=v2、キャッシュ=v1(恒久的に不整合、TTL切れまで直らない)
そこで定石は 「更新時はキャッシュに書くのではなく、無効化(削除)する」——いわゆる cache-aside の write-invalidate です。次の読みでミスが起き、DBから最新値を読み直してキャッシュに詰め直すので、古い値が居座りにくくなります。
無効化方式(推奨ベース):
更新時: DBを書く → キャッシュのキーを削除
読み時: ミスならDBから読み、キャッシュに詰める
ただしこれも完璧ではありません。読みと書きが交錯すると、無効化方式でも古い値が入りうる read-then-write のレース が残ります。
無効化方式でも残るレース:
読みリクエスト: キャッシュがミス → DBから旧値v1を読む(まだ詰めていない)
書きリクエスト: DBにv2を書く → キャッシュを削除(まだ空なので実質no-op)
読みリクエスト: 旧値v1をキャッシュに詰める ← v1が居座る
結果: DB=v2、キャッシュ=v1
上のレースが示すのは、キャッシュとDBという2つの独立ストアへの操作を、ロックなしで並行に行う限り、どんな単純な順序づけでも一時的な不整合の窓を完全には消せないという事実です。実務的な緩和策は、(1) 削除を二度行う delayed double delete(更新直後に消し、短い遅延後にもう一度消してレース窓に入った旧値を一掃する)、(2) DBの変更ログ(binlog)から確実に無効化を駆動する CDC ベースの無効化(/devops/change-data-capture/)、(3) 更新の重要部分を分散ロックで直列化する(/devops/distributed-locking/)。いずれも「窓を狭める/確実に閉じる」方向の対処で、無効化メッセージの確実な配送が要になります。
無効化の確実な配送:もう一つの二重書き問題
「DBを書いてからキャッシュを消す」も、実は2つのストアへの書き込みであり、DBコミット後・無効化送信前にプロセスが落ちれば、DBは新値・キャッシュは旧値のまま残ります。これは更新通知を確実に届ける問題そのものなので、メッセージ駆動の整合性で使う transactional outbox(/devops/transactional-outbox/)と同型です。DB更新と「無効化すべき」という事実を同一トランザクションで記録し、その記録を確実に処理して無効化を発火させれば、無効化の取りこぼしを防げます。CDC で binlog から無効化を駆動するのは、この outbox を専用基盤に肩代わりさせた形と見なせます。
TTLは整合性ではなく「不整合の寿命の上限」
多くの設計は TTL(time to live)に頼ります。しかし TTL の本質を誤解してはいけません。TTL は キャッシュが古い値を保持しうる時間の上限 を与えるだけで、整合性そのものを保証しません。TTL が10分なら、無効化に失敗しても最悪10分後には期限切れで再取得され、古い値の寿命は10分で打ち切られる——つまり TTL は「不整合の最大持続時間」を縛る安全網です。
TTLが保証するのは「いつかは直る」だけ:
- 短いTTL: 不整合の寿命は短いが、ミス頻発でDB負荷↑・ヒット率↓
- 長いTTL: ヒット率は高いが、無効化漏れ時に古い値が長く残る
- TTL=0(無し)相当: 常にDB直、整合的だがキャッシュの意味が薄い
確実な反映が要るデータは、TTL に頼り切らず 明示無効化(active invalidation) を併用します。TTL は無効化が漏れたときの最終防衛線、明示無効化は速やかな反映、という役割分担です。なお TTL の一斉失効はミスの集中(thundering herd)やキャッシュスタンピードを招くため、失効時刻をばらす jitter や、再計算を1リクエストに絞る仕組みが要ります(/devops/cache-invalidation-stampede/)。
多層キャッシュ:CDN・アプリ・DBが整合性を掛け合わせる
実システムでは、同じ値が複数の層に同時に複製されます。ブラウザ → CDN/エッジ → アプリ内キャッシュ(プロセス内やRedis)→ DBのバッファプールと、層ごとにコピーが存在し、それぞれが独立した寿命と無効化経路を持ちます。
ブラウザ CDN/エッジ アプリ層キャッシュ DB
[ copy ] → [ copy ] → [ copy ] → [ source of truth ]
TTL/ETag 長めのTTL TTL+明示無効化 永続・正本
ここでの根本問題は、整合性が層をまたいで掛け算になる ことです。アプリ層を無効化しても、その手前の CDN がまだ旧値を TTL いっぱい保持していれば、利用者にはやはり古い値が届きます。全層が新値に揃うまでの時間は、各層の無効化伝播と TTL のうち 最も遅い層 に律速されます。
各層を個別に TTL 任せにすると、上位(クライアントに近い)層ほど古い値が長く残ります。確実に更新を見せたい場合は、更新時にアプリ層だけでなく CDN のパージ(purge)API を叩いて上位層も無効化 し、伝播を貫通させます。さらに、CDN では時間ベースの TTL ではなく ETag / If-None-Match による検証で「変わっていなければ再利用、変わっていれば取り直す」を使うと、無駄な再取得を避けつつ鮮度を上げられます。層が増えるほど無効化の経路設計が複雑になるため、本当にその層が必要かを問うことも設計の一部です。
この多層の不整合は、突き詰めると分散システムの整合性モデルの話に行き着きます。キャッシュが返すのは「ある時点のスナップショット」であり、書き込み直後に必ず最新が読める強い整合性ではなく、放っておけばいつか揃う 結果整合性(eventual consistency) に落ち着くのが普通です。どの程度の古さ(staleness)を許容できるかをSLOとして決め、それに見合うTTLと無効化戦略を選ぶ——整合性モデルの選択そのものが設計判断です(/devops/consistency-models/)。
書き込み戦略の核は「耐久性が何で担保されるか」。write-through は書いた時点でDBに載るので耐久的だが書きが遅い。write-back は先にキャッシュへ書いて即完了するため速いが、永続化前のクラッシュで未書き込みデータを失う。write-around はキャッシュ汚染を避けるが書いた値の初回読みは必ずミス。整合性の核は「キャッシュとDBは原子的に更新できない」こと——更新時は値を書き戻すのではなく無効化(削除)するのが定石で、それでも read-then-write のレースは残る。TTLは整合性ではなく不整合の寿命の上限にすぎず、確実な反映には明示無効化を併用する。多層(CDN/アプリ/DB)では整合性が掛け算になり、最も遅い層が古い値を保持し続けるため、無効化を上位層まで貫通させる。
まとめ
- 書き込み戦略は耐久性とレイテンシのトレードオフ。write-through は同期で両方書き整合的だが遅い、write-back は先にキャッシュへ書き速いが障害で未永続データを失う、write-around はキャッシュ汚染を避けるが初回読みは必ずミスする。
- キャッシュとDBは別ストアで原子的に更新できない。更新時に値を書き戻すと並行更新で古い値が焼き付くため、無効化(削除) が定石。それでも read-then-write のレースは残り、delayed double delete や CDC・ロックで窓を閉じる。
- 「DBを書いてからキャッシュを消す」自体が二重書き問題であり、無効化の確実な配送には transactional outbox や CDC が有効。
- TTL は整合性を保証せず、不整合の寿命の上限を与える安全網。確実な反映には明示無効化を併用し、一斉失効によるスタンピードは jitter 等で散らす。
- 多層キャッシュでは整合性が層をまたいで掛け算になり、最も遅い層が律速する。無効化を CDN まで貫通させ、ETag 検証を併用し、許容する古さを整合性モデル・SLOとして明示的に決める。
DevOps/インフラ Article
分散キャッシュ整合性(write-through/back/around)を実務で読む
TL;DRは入口です。実際に選ぶ・使う段階では、何を解決するか、何と比較するか、導入後にどこで詰まるかまで見る必要があります。
解決すること
分散キャッシュ
比較で見る軸
難易度: advanced / カテゴリ: DevOps/インフラ / タグ数: 6
導入後に効く点
キャッシュとDBは別々のストアであり、2つを跨ぐ書き込みは原子的でない。クラッシュや並行実行の隙で順序が崩れ、古い値がキャッシュに焼き付く。cache-aside+更新時に値を書くのではなく無効化するのが定石。
先に潰すリスク
用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。
- 難易度
- advanced
- カテゴリ
- DevOps/インフラ
- タグ数
- 6
判断チェックリスト
- 自社の用途が「分散キャッシュ / キャッシュ整合性」に近いか確認する。
- 強みである「書き込み戦略はwrite-through(同期で両方書く・遅いが整合的)、write-back(先にキャッシュへ書き遅延で永続化・速いが障害でデータ喪失)、write-around(DBだけ書きキャッシュは書かない・キャッシュ汚染を避ける)に分かれ、レイテンシ・耐久性・整合性のトレードオフを取る。」が本当に評価軸になるか確認する。
- 注意点の「用語だけ覚えても、設計・実装・運用でどこに効くかを確認しないと判断を誤る。」を運用で吸収できるか確認する。
- 公開値や仕様値は、対象プラン・対象機種・対象リージョンまで確認する。
- 既存システム、ID、ネットワーク、監視、バックアップとの接続方法を先に洗い出す。
- 小さく試してから、本番移行、権限設計、障害時手順、コスト監視を決める。