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-AsideCache Miss時DBDBのみ結果整合性汎用
Write-ThroughCache優先Cache+DB同期強一貫性一貫性重視
Write-BehindCache優先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

特性RedisMemcached
データ構造String, Hash, List, Set, Sorted SetKey-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  # 注意: 本番では短時間のみ