DEV Community

myougaTheAxo
myougaTheAxo

Posted on • Originally published at zenn.dev

Claude Codeでキャッシュスタンピード対策を設計する:確率的早期期限・Mutex Lock・SWR

キャッシュが一斉に失効したとき、大量のリクエストがDBに殺到する「キャッシュスタンピード」。本番環境でこれが起きると、DBが過負荷でダウンするケースがあります。今回はClaude Codeを使って、3つの対策パターンを実装します。

キャッシュスタンピードとは?

通常時:リクエスト → Redis HIT → 即返却
スタンピード時:
  リクエスト1000件 → Redis MISS(TTL切れ)
    → 1000件全てDBへ → DBダウン
Enter fullscreen mode Exit fullscreen mode

TTLを長くすれば発生確率は下がりますが、データ鮮度が犠牲になります。真の解決には専用のアルゴリズムが必要です。

パターン1:PER(確率的早期再計算)

GoogleのXFetch論文で提案されたアルゴリズム。TTL切れの直前に確率的に先回りして再計算します。

import { Redis } from 'ioredis';
const redis = new Redis();

function shouldRecompute(expiry: number, delta: number, beta: number = 1): boolean {
  const now = Date.now() / 1000;
  return now - beta * delta * Math.log(Math.random()) > expiry;
}

async function getWithPER<T>(key: string, fetcher: () => Promise<T>, ttl: number): Promise<T> {
  const cached = await redis.get(key);

  if (cached) {
    const { value, expiry, delta } = JSON.parse(cached);
    if (!shouldRecompute(expiry, delta)) return value;
  }

  const start = Date.now();
  const value = await fetcher();
  const delta = (Date.now() - start) / 1000;
  const expiry = Date.now() / 1000 + ttl;

  await redis.setex(key, ttl, JSON.stringify({ value, expiry, delta }));
  return value;
}
Enter fullscreen mode Exit fullscreen mode

PERの特徴:ロックなし・単一プロセスが自然に先回り再計算・シンプル。

パターン2:Mutex Lock(Redis NX SET)

1リクエストだけDBにアクセスさせ、他は待機させます。

async function getWithMutex<T>(key: string, fetcher: () => Promise<T>, ttl: number): Promise<T> {
  const cached = await redis.get(key);
  if (cached) return JSON.parse(cached);

  const lockKey = `lock:${key}`;
  const lockValue = crypto.randomUUID();

  const acquired = await redis.set(lockKey, lockValue, 'NX', 'EX', 10);

  if (acquired === 'OK') {
    try {
      const value = await fetcher();
      await redis.setex(key, ttl, JSON.stringify(value));
      return value;
    } finally {
      // Luaで安全なロック解放(自分のロックのみ削除)
      const script = `
        if redis.call('get', KEYS[1]) == ARGV[1] then
          return redis.call('del', KEYS[1])
        else
          return 0
        end
      `;
      await redis.eval(script, 1, lockKey, lockValue);
    }
  } else {
    await new Promise(resolve => setTimeout(resolve, 100));
    return getWithMutex(key, fetcher, ttl);
  }
}
Enter fullscreen mode Exit fullscreen mode

Mutex Lockの特徴:確実に1リクエストのみDB接続・Luaスクリプトで安全な解放を保証。

パターン3:Stale-While-Revalidate(SWR)

古い値を即返却しつつ、バックグラウンドで更新します。

interface SWRCacheEntry<T> {
  value: T;
  freshUntil: number;
  staleUntil: number;
}

async function getWithSWR<T>(
  key: string,
  fetcher: () => Promise<T>,
  freshTTL: number,
  staleTTL: number
): Promise<T> {
  const cached = await redis.get(key);
  const now = Date.now();

  if (cached) {
    const entry: SWRCacheEntry<T> = JSON.parse(cached);

    if (now < entry.freshUntil) return entry.value;

    if (now < entry.staleUntil) {
      // 古い値を即返却 + バックグラウンドで更新
      setImmediate(async () => {
        const newValue = await fetcher();
        const newEntry = {
          value: newValue,
          freshUntil: Date.now() + freshTTL * 1000,
          staleUntil: Date.now() + staleTTL * 1000,
        };
        await redis.setex(key, staleTTL, JSON.stringify(newEntry));
      });
      return entry.value;
    }
  }

  const value = await fetcher();
  const entry = { value, freshUntil: now + freshTTL * 1000, staleUntil: now + staleTTL * 1000 };
  await redis.setex(key, staleTTL, JSON.stringify(entry));
  return value;
}

// 60秒新鮮、最大600秒古い値を許容
const result = await getWithSWR('dashboard_stats', () => db.stats.aggregate(), 60, 600);
Enter fullscreen mode Exit fullscreen mode

SWRの特徴:古い値を即返却するためレイテンシが最小・バックグラウンド更新で常に新鮮な状態に近づく。

3パターンの比較

PER Mutex Lock SWR
仕組み 確率的先回り 1台だけアクセス 古い値を即返却
レイテンシ 中(待機あり) 最低
DB負荷 非常に低 最低(1リクエスト)
実装複雑度 高(Lua必須)
適用場面 汎用 クリティカルなDB UXを優先する場面

まとめ

  1. PER(確率的早期再計算) — TTL切れ直前に1リクエストが先回り再計算、スタンピードを自然回避
  2. Mutex Lock(Redis NX SET) — Luaスクリプトで安全なロック解放、確実に1台のみDB接続
  3. Stale-While-Revalidate — 古い値を即返却+でバックグラウンド更新
  4. 組み合わせが最強 — SWR + PERで「古い値即返却+TTL切れ前の先回り更新」を両立

Code Review Pack ¥980 — コードレビュー自動化スキルセット

キャッシュ設計・パフォーマンスボトルネック検出・DBクエリ最適化など、Claudeによるコードレビュー自動化スキルをまとめました。

Code Review Pack(¥980)PromptWorks にて販売中です(product_id: 524)。

みょうが(ウーパールーパーVTuber)が実際のプロジェクトで使っているレビューパターンを凝縮しました。

Top comments (0)