キャッシュ戦略 — 速いほうを読む、ということ
キャッシュは「よく使う答えを、手元のメモに控えておく」仕組みです。DBまで取りに行けば数十ミリ秒かかる値も、メモリ上のメモを見るだけなら1ミリ秒で済みます。
ただし控えを持った瞬間から同じ値が2箇所に存在します。片方だけ書き換われば、当然ズレる。キャッシュの話が難しくなるのは速さの話ではなく、この「ズレをどう扱うか」の話だからです。図1で読み書きしながら、ズレが生まれる瞬間とTTLが消してくれる瞬間を見てください。
読み方はいつも同じ — まずキャッシュ、無ければDB
読み取りの流れはどの戦略でも共通です。キャッシュにあれば即返す(ヒット)。無ければDBまで行き、帰りに控えを置く(ミス)。図1で連続して読むと、1回目だけが遅く、2回目以降が一気に速くなるのが分かります。
キャッシュの効き目はヒット率でほぼ決まります。同じ値が何度も読まれるほど得をするので、読み取りが圧倒的に多く、更新が少ないデータほど向いています。
Cache Aside — 書くのはDBだけ。だからズレる
もっともよく使われる戦略です。アプリがキャッシュを自分で面倒みる形で、書き込みのときはDBだけを更新します。
図1で「在庫を1つ減らす」を押すと、DBの数字だけが変わり、キャッシュは古い数字を持ったままになります。この状態で読むと古い値が返ります。実装がシンプルで、キャッシュが落ちてもアプリは動き続けられるかわりに、この一時的なズレを受け入れるのがCache Asideです。
実務では、書き込みのあとにキャッシュを更新するのではなく「消す」のが定石です(図1の「キャッシュを消す」)。次の読み取りが自然にミスして、DBから正しい値を取り直してくれる。書き換えようとすると、複数の更新が同時に走ったときに古い値で上書きしてしまうことがあるためです。
Write Through — 書くたびに両方。ズレないが、遅い
書き込みのときにキャッシュとDBを一緒に更新するのがWrite Throughです。図1でタブを切り替えて書いてみると、2つの数字が同時に変わり、ズレが一度も起きません。
そのかわり書き込みは必ず両方ぶんの時間がかかります。さらに、読まれるかどうか分からない値まで律儀にキャッシュに載るので、書き込みが多いデータでは使われない控えでメモリを埋めることになります。
TTLは「諦めるまでの時間」
どの戦略でも最後の砦になるのが TTL(有効期限)です。控えに寿命を持たせ、時間が来たら黙って捨てる。図1のキャッシュの下のバーが減っていき、空になると値が消えるのがそれです。
TTLの本質は「最悪でもこの秒数でズレは直る」という保証です。短くすればズレは早く消えますが、その分ミスが増えてDBへの負荷が上がる。長くすれば速いが、古い値を返す時間も延びる。どれだけ古い値なら許せるかで決める数字です。
- ヒット / ミス
- キャッシュに値があった / 無かった。ミスならDBまで取りに行く。
- Cache Aside
- アプリがキャッシュを管理する方式。書き込みはDBだけに行う。
- Write Through
- 書き込み時にキャッシュとDBを両方更新する方式。ズレないが遅い。
- TTL
- 控えの有効期限。過ぎたら捨てられ、次の読み取りはミスになる。
- 無効化(invalidate)
- 更新後にキャッシュを消すこと。次の読み取りが正しい値を持ってくる。
まとめ
キャッシュ戦略の選択は「速さ」と「どれだけ古い値を許すか」の取引です。Cache Asideは速くて単純だがズレる時間がある。Write Throughはズレないが書き込みが重い。そして TTL が、ズレが永遠に続かないことだけは保証してくれます。図1で書いてから読む順番を変えると、この取引がそのまま数字に出ます。