LESSON 30分

ストーリー

高橋アーキテクト
商品の価格を更新したのに、ユーザーに古い価格が表示されています

高橋アーキテクトがうなずいた。

高橋アーキテクト
キャッシュの無効化(Invalidation)の問題だ。Phil Karltonはこう言った — “コンピュータサイエンスで難しいことは2つだけ。キャッシュの無効化と命名だ”。冗談のようだが、真実だ

キャッシュ無効化の3つの戦略

1. TTL(Time To Live)ベース

一定時間が経過したらキャッシュを自動的に無効にします。

// TTLベースのキャッシュ
class TTLCache {
  async set(key: string, value: string, ttlSeconds: number): Promise<void> {
    await this.redis.setex(key, ttlSeconds, value);
  }

  // TTLの設定指針
  static TTL_GUIDELINES = {
    // 高頻度アクセス & 変更少ない → 長いTTL
    productCatalog: 3600,     // 1時間
    categoryList: 86400,      // 24時間
    staticConfig: 604800,     // 7日

    // 頻繁に変更される → 短いTTL
    stockCount: 30,           // 30秒
    searchResults: 60,        // 1分
    userSession: 1800,        // 30分

    // リアルタイム性が必要 → TTL不適
    // cartContents: 0,       // キャッシュしない
  };
}
メリットデメリット
実装が最もシンプルTTL中は古いデータが返される
自動的にメモリが解放される最適なTTL値の決定が難しい

2. イベントベース(Event-Driven Invalidation)

データが変更されたタイミングでキャッシュを無効化します。

// イベントベースのキャッシュ無効化
class EventDrivenCache {
  constructor(
    private cache: CacheClient,
    private eventBus: EventEmitter,
  ) {
    // イベントリスナーを登録
    this.eventBus.on('product:updated', (event) => this.onProductUpdated(event));
    this.eventBus.on('product:deleted', (event) => this.onProductDeleted(event));
  }

  private async onProductUpdated(event: ProductUpdatedEvent): Promise<void> {
    const { productId, newData } = event;

    // 方法1: キャッシュを削除(次回アクセス時に再取得)
    await this.cache.del(`product:${productId}`);

    // 方法2: キャッシュを更新(即座に最新データに置き換え)
    await this.cache.set(
      `product:${productId}`,
      JSON.stringify(newData),
      3600
    );

    // 関連キャッシュも無効化
    await this.cache.del(`product-list:category:${newData.categoryId}`);
    await this.cache.del('popular-products');
  }

  private async onProductDeleted(event: ProductDeletedEvent): Promise<void> {
    await this.cache.del(`product:${event.productId}`);
    // パターンマッチで関連キャッシュを一括削除
    const keys = await this.cache.keys('product-list:*');
    if (keys.length > 0) {
      await this.cache.del(...keys);
    }
  }
}
メリットデメリット
データの即時反映が可能実装が複雑
不要なキャッシュミスが減る関連キャッシュの特定が難しい

3. キャッシュバスティング(Cache Busting)

ファイル名やURLにバージョン情報を含め、変更時にURLを変えることで強制的に新しいデータを取得させます。

// キャッシュバスティングの例
class AssetVersioning {
  // ファイル名にハッシュを付与
  getAssetUrl(filename: string): string {
    const hash = this.calculateFileHash(filename);
    const ext = filename.split('.').pop();
    const base = filename.replace(`.${ext}`, '');
    return `/assets/${base}.${hash}.${ext}`;
    // 例: /assets/style.abc123.css
  }

  // クエリ文字列方式(非推奨だが簡易)
  getAssetUrlSimple(filename: string): string {
    return `/assets/${filename}?v=${Date.now()}`;
    // 例: /assets/style.css?v=1700000000
    // CDNによってはクエリ文字列をキーに含めないため注意
  }
}

// API レスポンスのバージョニング
class VersionedApiCache {
  private version = 1;

  getCacheKey(resource: string): string {
    return `v${this.version}:${resource}`;
  }

  // バージョンを上げると全キャッシュが無効になる
  incrementVersion(): void {
    this.version++;
  }
}
メリットデメリット
確実に最新データを取得できるURLの管理が必要
CDNキャッシュにも有効古いURLのキャッシュがゴミになる

無効化戦略の選択ガイド

データの更新頻度は?

  ├─ ほぼ変更しない(設定値、マスタデータ)
  │   → TTL(長め: 1時間〜1日)

  ├─ 定期的に変更(商品情報、記事)
  │   → TTL(短め: 1〜10分)+ イベントベース

  ├─ 頻繁に変更(在庫数、ランキング)
  │   → イベントベース + 短TTL(フォールバック)

  └─ 静的アセット(CSS, JS, 画像)
      → キャッシュバスティング + 長TTL

よくある失敗パターン

// 失敗1: 関連キャッシュの無効化漏れ
async function updateCategory(id: string, data: CategoryUpdate): Promise<void> {
  await db.updateCategory(id, data);
  await cache.del(`category:${id}`);
  // 忘れがち: この製品カテゴリに属する商品リストのキャッシュも無効化が必要
  // await cache.del(`products:category:${id}`);
}

// 失敗2: キャッシュとDBの更新順序
async function updateProduct(id: string, data: ProductUpdate): Promise<void> {
  // 危険: キャッシュを先に削除すると、DB更新前に別リクエストが古いデータを再キャッシュする可能性
  await cache.del(`product:${id}`);
  await db.updateProduct(id, data); // この間に別リクエストが来ると...

  // 安全: DB更新後にキャッシュを削除または更新
  await db.updateProduct(id, data);
  await cache.del(`product:${id}`);
}

まとめ

ポイント内容
TTLベース時間経過で自動無効化。シンプルだが遅延がある
イベントベースデータ変更時に即座に無効化。正確だが複雑
キャッシュバスティングURL変更で強制更新。静的アセットに最適
組み合わせTTL + イベントベースの併用が実践的

チェックリスト

  • 3つの無効化戦略の違いを説明できる
  • ユースケースに応じた戦略選択ができる
  • 関連キャッシュの無効化漏れリスクを理解した
  • キャッシュとDB更新の順序の重要性を把握した

次のステップへ

次は「分散キャッシュの課題」を学びます。複数サーバー環境でのThundering HerdやCache Stampedeなど、分散環境特有の難問に挑みます。


推定読了時間: 30分