キャッシュの更新と無効化
値と期限、SQLの実行回数を対応させ、更新時にどのキャッシュを無効にする必要があるか説明します。
説明
キャッシュアサイドでは、アプリが先にキャッシュを調べ、なければ原データを取得して保存します。検索回数を減らせますが、原データを更新した後も、期限内の古いキャッシュが残る場合があります。
SQLiteの商品価格と60秒のキャッシュを使います。キャッシュのヒット、原データだけの更新、期限切れ、明示的な無効化を順に確認します。最後に「商品がない」という結果を保存する負のキャッシュが、新規登録後にも残る例を調べます。
値と期限、SQLの実行回数を対応させ、更新時にどのキャッシュを無効にする必要があるか説明します。
1原データとキャッシュを用意
CacheAsideは商品1の価格100を持つSQLite、空のキャッシュ、経過時刻0を用意します。時刻はTTLの条件を確認するため明示的に進めます。
from cache import CacheAside
store = CacheAside()価格100・空のキャッシュ・時刻0を用意する
print("now", store.clock, "cache", store.cache現在のキャッシュ内容を読む, "SQLs", store.queries原データを検索した回数を読む)キャッシュは空、検索SQLは0回です。原データを置いたことと、キャッシュへ値を入れたことは別の状態です。
2最初の検索でキャッシュへ入れる
商品1を取得します。
store.get(1)商品1をキャッシュ優先で取得するMISSで価格100を取得し、SQLs=1、expires=60になります。保存した時刻0から60秒で期限になる条件です。
3同じ値をキャッシュから取得
時刻を変えずに同じ商品を読みます。
store.get(1)同じ商品を再度取得するHITで価格100、SQLs=1のままです。原データへの検索を繰り返さず、期限内の保存値を使っています。
4原データだけを更新する
SQLiteの価格を200へ更新して確定し、キャッシュ経由で読みます。
store.update(1, 200)原データの価格を200へ更新する
store.get(1)キャッシュ経由で返る値を確認するHITで100のままです。原データのUPDATEは成功していても、キャッシュの値と期限は自動的には変わっていません。
5期限を過ぎて原データを取り直す
経過時刻を61へ進めます。
store.clock = 6160秒の期限を過ぎる時刻へ進める
store.get(1)期限切れによる再取得を確認するMISSで200を取得し、SQLs=2、次のexpires=121になります。TTLは古い値が残る期間を制限しますが、更新直後に新しい値が見えることを保証する仕組みではありません。
6更新に合わせて該当キーを無効にする
原データを300へ更新し、商品1のキャッシュを取り除いてから読みます。
store.update(1, 300)原データを更新して確定する
store.invalidate(1)商品1の保存値を無効にする
store.get(1)新しい値を取得するMISSで300、SQLs=3です。期限を待たずに新しい値を取得できました。更新処理と無効化の順序や失敗時の再実行も、運用時には合わせて設計します。
7未登録という結果が残る場合
未登録の商品2を検索してから、原データへ価格400を登録します。その後もう一度キャッシュ経由で読みます。
store.get(2)未登録の検索結果を保存する
store.update(2, 400)原データへ商品2を登録する
store.get(2)登録後に返るキャッシュ結果を確認する最初はMISSでNone、その後はHITでNoneです。値がないという結果も保存しているため、新しい商品を登録しても、そのキャッシュが残る間は未登録に見えます。
8未登録のキャッシュも無効にする
商品2を無効化し、再取得します。
store.invalidate(2)未登録という保存結果を無効にする
store.get(2)登録された価格を読み直す
print(store.cache)商品ごとの値と期限を確認する商品2は400になります。キャッシュには値とexpiresがあり、商品1も別のキーで保持されています。更新だけでなく新規登録や削除でも、関連する保存結果を考える必要があります。
まとめ
原データの更新だけでは期限内のキャッシュが変わらない場合があります。
TTLと無効化を使い、許容する古さと更新時の動作を設計します。
未登録の結果を保存する場合、新規登録後の無効化も必要です。
操作履歴と解説を振り返れます。
Python 3 / SQLite
from cache import CacheAside
store = CacheAside()
print("now", store.clock, "cache", store.cache, "SQLs", store.queries)now 0 cache {} SQLs 0store.get(1)id=1 source=MISS value=100 expires=60 now=0 SQLs=1
store.get(1)id=1 source=HIT value=100 expires=60 now=0 SQLs=1
store.update(1, 200)
store.get(1)id=1 source=HIT value=100 expires=60 now=0 SQLs=1
store.clock = 61
store.get(1)id=1 source=MISS value=200 expires=121 now=61 SQLs=2
store.update(1, 300)
store.invalidate(1)
store.get(1)id=1 source=MISS value=300 expires=121 now=61 SQLs=3
store.get(2)
store.update(2, 400)
store.get(2)id=2 source=MISS value=None expires=121 now=61 SQLs=4 id=2 source=HIT value=None expires=121 now=61 SQLs=4
store.invalidate(2)
store.get(2)
print(store.cache)id=2 source=MISS value=400 expires=121 now=61 SQLs=5
{1: {'value': 300, 'until': 121}, 2: {'value': 400, 'until': 121}}演習完了