開発・技術選定

キャッシュをどこに置くか — 効く場所と、事故る場所

キャッシュは効果が大きい一方、古い情報が残り続けるという固有の事故を生みます。
層ごとの使い分けと、消し方の設計を整理します。

層ごとの役割

ブラウザ         … 静的ファイル(CSS/JS/画像)
CDN              … 静的ファイル、公開ページのHTML
アプリ(Redis等)… クエリ結果、集計値、外部API応答
DB               … クエリプランなど(自動)

それぞれ無効化の方法が違うので、混同すると事故ります。

静的ファイル:内容ハッシュを付ける

site.css        → site.a3f9c21b.css

ファイル名が内容で変わるので、無効化そのものが不要になります。
Django なら ManifestStaticFilesStorage がこれをやってくれます。

STORAGES = {"staticfiles": {
    "BACKEND": "whitenoise.storage.CompressedManifestStaticFilesStorage"}}

この方式なら、キャッシュ期間を長く取って問題ありません。

location /static/ { expires 1y; add_header Cache-Control "public, immutable"; }

逆に、ハッシュを付けずに expires 1y は絶対に避けます。
1年間、修正が反映されない状態になります。

HTML:短くするか、キャッシュしない

ログイン状態で内容が変わるページをキャッシュすると、
他人の情報が表示されるという最悪の事故になります。

@never_cache
def mypage(request): ...

公開ページでも、キャッシュするなら短く設定します。

アプリ層:キーの設計がすべて

key = f"stats:{shop_id}:{date:%Y%m%d}:v2"
  • 対象を特定できる要素をすべて含める
  • バージョン番号を入れる(構造を変えたとき一括で無効化できる)

v2v3 にするだけで、古いキャッシュが自動的に使われなくなります。
個別に消して回るより遥かに確実でした。

更新時に消す

from django.db.models.signals import post_save

@receiver(post_save, sender=Order)
def clear_stats(sender, instance, **kwargs):
    cache.delete(f"stats:{instance.shop_id}:{instance.created_at:%Y%m%d}:v2")

キャッシュを作る場所と消す場所を1つのモジュールにまとめるのが要点です。
離れていると、必ず消し忘れが発生します。

消すより「短くする」ほうが安全なことも

厳密な無効化が難しい場合、短い有効期限で妥協するほうが安全です。

cache.set(key, value, 60)     # 1分。多少古くても実害が無いなら

「常に正確」を目指して複雑な無効化を組むより、
多少の遅れを許容するほうが事故が少なくなります。

キャッシュを入れる前に

そもそも遅い原因がクエリなら、インデックスや N+1 の解消が先です。
遅い処理にキャッシュを被せるのは、問題を隠すだけになります。

まとめ

  • 静的ファイルは内容ハッシュ。無効化不要にする
  • 個人向けページはキャッシュしない(@never_cache
  • キーにバージョン番号を入れると一括無効化できる
  • 作る場所と消す場所を同じモジュールに置く
  • 厳密な無効化が難しいなら短い期限で妥協する
  • キャッシュの前に、まず遅い原因を直す