キャッシュは効果が大きい一方、古い情報が残り続けるという固有の事故を生みます。
層ごとの使い分けと、消し方の設計を整理します。
層ごとの役割
ブラウザ … 静的ファイル(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"
- 対象を特定できる要素をすべて含める
- バージョン番号を入れる(構造を変えたとき一括で無効化できる)
v2 を v3 にするだけで、古いキャッシュが自動的に使われなくなります。
個別に消して回るより遥かに確実でした。
更新時に消す
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) - キーにバージョン番号を入れると一括無効化できる
- 作る場所と消す場所を同じモジュールに置く
- 厳密な無効化が難しいなら短い期限で妥協する
- キャッシュの前に、まず遅い原因を直す