Cache
キャッシュはレスポンス高速化とバックエンド負荷軽減の要です。 キャッシュ戦略の選択、TTL設計、キャッシュ無効化の方法を理解することが重要です。
キャッシュレイヤーの配置
┌─────────────────────────────────────────────────────────────────────────┐ │ Caching Layers │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Client │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Browser Cache │ │ │ │ - HTTP Cache-Control ヘッダ制御 │ │ │ │ - Service Worker │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ CDN / Edge Cache │ │ │ │ - CloudFront, Cloudflare, Fastly │ │ │ │ - 静的コンテンツ、API応答キャッシュ │ │ │ │ - 地理的分散、DDoS防御 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Reverse Proxy Cache │ │ │ │ - Varnish, Nginx proxy_cache │ │ │ │ - 動的コンテンツキャッシュ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Application Cache │ │ │ │ - Redis, Memcached │ │ │ │ - セッション、APIレスポンス、計算結果 │ │ │ └─────────────────────────────────────────────────────────────────┘ │ │ │ │ │ ▼ │ │ ┌─────────────────────────────────────────────────────────────────┐ │ │ │ Database Cache │ │ │ │ - Query Cache, Buffer Pool │ │ │ │ - インデックス、ページキャッシュ │ │ │ └─────────────────────────────────────────────────────────────────┘ │ └─────────────────────────────────────────────────────────────────────────┘
キャッシュ戦略(Cache Patterns)
┌─────────────────────────────────────────────────────────────────────────┐ │ Cache-Aside (Lazy Loading) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Application │ │ │ │ │ ├─1→ Cache GET ─────→ [Cache] │ │ │ │ │ │ │ Cache Hit? ───Yes──→ Return │ │ │ │ │ │ │ No │ │ │ │ │ │ ├─2→ DB Query ──────→ [Database] │ │ │ │ │ │ └─3→ Cache SET ─────→ [Cache] │ │ │ │ 特徴: 読み取り時にキャッシュ。初回アクセスは遅い(Cold Start) │ │ 用途: 汎用的、最も一般的なパターン │ └─────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐ │ Write-Through │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Application │ │ │ │ │ ├─1→ Cache SET ─────→ [Cache] │ │ │ │ │ │ └─2→ DB Write ──────────┴────→ [Database] │ │ (同期的に両方更新) │ │ │ │ 特徴: 書き込み時に同時更新。データ一貫性高い。書き込み遅延あり │ │ 用途: 一貫性重視、読み取り多いワークロード │ └─────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐ │ Write-Behind (Write-Back) │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Application │ │ │ │ │ └─1→ Cache SET ─────→ [Cache] │ │ │ │ │ (非同期) │ │ │ │ │ ─2→ [Database] │ │ │ │ 特徴: 書き込み高速。データ損失リスクあり。バッチ書き込み可能 │ │ 用途: 高頻度書き込み、一時的なデータ損失許容 │ └─────────────────────────────────────────────────────────────────────────┘ ┌─────────────────────────────────────────────────────────────────────────┐ │ Read-Through │ ├─────────────────────────────────────────────────────────────────────────┤ │ │ │ Application ──→ [Cache] ──(miss)──→ [Database] │ │ │ │ │ │ └─────(自動ロード)────┘ │ │ │ │ 特徴: キャッシュが自動的にDBからロード。アプリロジック簡素化 │ │ 用途: キャッシュライブラリがサポートする場合 │ └─────────────────────────────────────────────────────────────────────────┘
| パターン | 読み取り | 書き込み | 一貫性 | 用途 |
|---|---|---|---|---|
| Cache-Aside | Cache Miss時DB | DBのみ | 結果整合性 | 汎用 |
| Write-Through | Cache優先 | Cache+DB同期 | 強一貫性 | 一貫性重視 |
| Write-Behind | Cache優先 | Cache→DB非同期 | 弱一貫性 | 高速書込 |
Redis キャッシュ実践
Cache-Aside実装例
import redis
import json
from functools import wraps
redis_client = redis.Redis(host='localhost', port=6379, db=0, decode_responses=True)
def cache_aside(ttl=300, prefix="cache"):
"""Cache-Asideデコレータ"""
def decorator(func):
@wraps(func)
def wrapper(*args, **kwargs):
# キャッシュキー生成
cache_key = f"{prefix}:{func.__name__}:{hash(str(args) + str(kwargs))}"
# 1. キャッシュ確認
cached = redis_client.get(cache_key)
if cached:
return json.loads(cached)
# 2. Cache Miss: DBから取得
result = func(*args, **kwargs)
# 3. キャッシュに保存
if result is not None:
redis_client.setex(cache_key, ttl, json.dumps(result))
return result
return wrapper
return decorator
# 使用例
@cache_aside(ttl=600, prefix="user")
def get_user(user_id: int) -> dict:
"""DBからユーザー取得"""
return db.query(User).filter(User.id == user_id).first()
# キャッシュ無効化
def invalidate_user_cache(user_id: int):
pattern = f"user:get_user:*"
keys = redis_client.keys(pattern)
if keys:
redis_client.delete(*keys)キャッシュキー設計
# キー命名規則
{service}:{entity}:{identifier}:{version}
# 良い例
user:profile:12345:v1
api:product:sku-abc:detail
session:user:uuid-xxx
# 悪い例
key1 # 意味不明
user_12345 # 階層がない
cache:user:profile:... # cacheは冗長
# バージョニングでキャッシュ一括無効化
CACHE_VERSION = "v2"
cache_key = f"user:profile:{user_id}:{CACHE_VERSION}"Redis設定
# /etc/redis/redis.conf # メモリ上限 maxmemory 4gb # メモリ上限時の動作 maxmemory-policy allkeys-lru # LRUで削除(キャッシュ用途推奨) # maxmemory-policy volatile-lru # TTL設定キーのみLRU削除 # maxmemory-policy noeviction # 書き込み拒否 # 永続化(キャッシュ用途では無効化も検討) save "" # RDB無効化 appendonly no # AOF無効化 # 接続設定 maxclients 10000 timeout 0 # 接続タイムアウト無効 # TCP設定 tcp-backlog 511 tcp-keepalive 300 # クラスタ設定(Redis Cluster) cluster-enabled yes cluster-config-file nodes.conf cluster-node-timeout 5000
キャッシュ無効化戦略
「キャッシュ無効化は難しい」 - Phil Karlton
コンピュータサイエンスで難しい問題は2つ: キャッシュ無効化と命名規則
1. TTL (Time-To-Live)
最もシンプル。一定時間後に自動失効
redis_client.setex("user:123", 3600, data) # 1時間後失効
# TTL設計の考慮点
# - 短すぎ: キャッシュヒット率低下
# - 長すぎ: 古いデータが残る
# - 目安: 更新頻度とデータ鮮度要件から決定2. 明示的削除
データ更新時にキャッシュを削除
def update_user(user_id, data):
db.update(user_id, data)
redis_client.delete(f"user:{user_id}") # キャッシュ削除
# パターンマッチで一括削除
keys = redis_client.keys("user:*:profile")
if keys:
redis_client.delete(*keys) # 注意: keysは本番で重い3. バージョニング
キーにバージョンを含め、バージョンアップで無効化
# キャッシュバージョンをRedisに保存
def get_cache_version():
return redis_client.get("cache:version") or "v1"
def invalidate_all_cache():
# バージョンをインクリメント
redis_client.incr("cache:version")
# 古いキーは自然にTTLで消える(メモリ回収)4. イベント駆動無効化
DB変更イベントをトリガーにキャッシュ削除
# CDC (Change Data Capture) パターン
# MySQL binlog → Debezium → Kafka → Cache Invalidator
# アプリケーションイベント
@event.listens_for(User, 'after_update')
def invalidate_user_cache(mapper, connection, target):
redis_client.delete(f"user:{target.id}")キャッシュの問題と対策
Cache Stampede(Thundering Herd)
問題: キャッシュ失効時に大量リクエストが同時にDBへ
# 対策1: Lock機構(Mutex)
def get_with_lock(key):
data = redis_client.get(key)
if data:
return json.loads(data)
lock_key = f"lock:{key}"
if redis_client.setnx(lock_key, 1):
redis_client.expire(lock_key, 10)
try:
data = fetch_from_db()
redis_client.setex(key, 300, json.dumps(data))
finally:
redis_client.delete(lock_key)
return data
else:
time.sleep(0.1) # 他のリクエストがセット完了を待つ
return get_with_lock(key)
# 対策2: TTLずらし(Jitter)
ttl = 300 + random.randint(-30, 30) # 270-330秒
# 対策3: 事前更新(Background Refresh)
# TTLの80%経過時点でバックグラウンド更新Cache Penetration
問題: 存在しないキーへのアクセスで毎回DBクエリ
# 対策1: Null Caching(空結果もキャッシュ)
def get_user(user_id):
cached = redis_client.get(f"user:{user_id}")
if cached == "NULL":
return None
if cached:
return json.loads(cached)
user = db.get_user(user_id)
if user:
redis_client.setex(f"user:{user_id}", 300, json.dumps(user))
else:
redis_client.setex(f"user:{user_id}", 60, "NULL") # 短TTLで空キャッシュ
return user
# 対策2: Bloom Filter(大量の不正キーを効率的にフィルタ)
from pybloom_live import BloomFilter
valid_ids = BloomFilter(capacity=1000000, error_rate=0.001)Hot Key問題
問題: 特定キーへのアクセス集中でRedisノード過負荷
# 対策1: ローカルキャッシュ併用
import cachetools
local_cache = cachetools.TTLCache(maxsize=1000, ttl=10)
def get_hot_data(key):
if key in local_cache:
return local_cache[key]
data = redis_client.get(key)
local_cache[key] = data
return data
# 対策2: キーの分散(Read Replica)
# Redis Clusterで複数レプリカからリード
# 対策3: キーの分割
# hot_key → hot_key:0, hot_key:1, ... (ランダム選択)Redis vs Memcached
| 特性 | Redis | Memcached |
|---|---|---|
| データ構造 | String, Hash, List, Set, Sorted Set | Key-Valueのみ |
| 永続化 | RDB, AOF | なし |
| クラスタ | Redis Cluster, Sentinel | クライアント側分散 |
| スレッドモデル | シングルスレッド(I/Oマルチプレックス) | マルチスレッド |
| メモリ効率 | オーバーヘッドあり | スラブアロケータ |
| 推奨用途 | セッション、ランキング、Pub/Sub | シンプルキャッシュ、大量小データ |
選定の目安
- Redis選択: データ構造活用、永続化必要、Pub/Sub、複雑な操作
- Memcached選択: シンプルなKey-Value、マルチスレッド活用、メモリ効率重視
キャッシュ監視
| メトリクス | 確認方法 | 目標値 |
|---|---|---|
| ヒット率 | INFO stats: keyspace_hits / (hits + misses) | > 90% |
| メモリ使用量 | INFO memory: used_memory | < maxmemory 80% |
| エビクション数 | INFO stats: evicted_keys | 増加傾向なし |
| 接続数 | INFO clients: connected_clients | < maxclients |
| レイテンシ | redis-cli --latency | < 1ms (avg) |
# Redis INFO コマンド redis-cli INFO stats | grep -E "keyspace|evicted" redis-cli INFO memory | grep used_memory_human # スロークエリ確認 redis-cli SLOWLOG GET 10 # リアルタイムモニター redis-cli MONITOR # 注意: 本番では短時間のみ