LESSON 30分

ストーリー

高橋アーキテクト
同じ商品ページを1日に10万回表示しているのに、毎回データベースに問い合わせている。これは無駄だと思わないか?

高橋アーキテクトがモニタリング画面を示した。

あなた
キャッシュを使えばいいんですよね?
高橋アーキテクト
“キャッシュを使う”のは簡単だ。だが”正しく使う”のは難しい。キャッシュには様々な戦略があり、ユースケースによって最適な戦略が異なる。今日はその使い分けを学ぼう

キャッシュとは

キャッシュとは、アクセス頻度の高いデータを高速なストレージに一時保存し、次回以降のアクセスを高速化する仕組みです。

// キャッシュの基本構造
class SimpleCache<T> {
  private store = new Map<string, { data: T; expiresAt: number }>();

  get(key: string): T | null {
    const entry = this.store.get(key);
    if (!entry) return null;
    if (Date.now() > entry.expiresAt) {
      this.store.delete(key);
      return null; // 期限切れ
    }
    return entry.data;
  }

  set(key: string, data: T, ttlMs: number): void {
    this.store.set(key, {
      data,
      expiresAt: Date.now() + ttlMs,
    });
  }
}

3つの基本戦略

1. Cache-Aside(キャッシュアサイド / Lazy Loading)

最も一般的な戦略。アプリケーションがキャッシュを直接管理します。

class CacheAsideRepository {
  constructor(
    private cache: CacheClient,
    private db: Database,
  ) {}

  async getProduct(id: string): Promise<Product> {
    // 1. キャッシュを確認
    const cached = await this.cache.get(`product:${id}`);
    if (cached) {
      return JSON.parse(cached); // キャッシュヒット
    }

    // 2. キャッシュミス → DBから取得
    const product = await this.db.findProduct(id);

    // 3. キャッシュに保存
    await this.cache.set(
      `product:${id}`,
      JSON.stringify(product),
      3600 // TTL: 1時間
    );

    return product;
  }
}
メリットデメリット
実装がシンプルキャッシュミス時にレイテンシが増加
必要なデータだけキャッシュデータの不整合が発生し得る
障害時にDBフォールバック可能初回アクセスは常にDB

適したユースケース: 読み取りが多い一般的なアプリケーション

2. Write-Through(ライトスルー)

書き込み時にキャッシュとDBを同時に更新します。

class WriteThroughRepository {
  constructor(
    private cache: CacheClient,
    private db: Database,
  ) {}

  async updateProduct(id: string, data: ProductUpdate): Promise<Product> {
    // 1. DBに書き込み
    const product = await this.db.updateProduct(id, data);

    // 2. キャッシュも即座に更新
    await this.cache.set(
      `product:${id}`,
      JSON.stringify(product),
      3600
    );

    return product;
  }

  async getProduct(id: string): Promise<Product> {
    // キャッシュは常に最新なので安心して読める
    const cached = await this.cache.get(`product:${id}`);
    if (cached) {
      return JSON.parse(cached);
    }
    // キャッシュにない場合はDBから取得してキャッシュに保存
    const product = await this.db.findProduct(id);
    await this.cache.set(`product:${id}`, JSON.stringify(product), 3600);
    return product;
  }
}
メリットデメリット
キャッシュが常に最新書き込みのレイテンシが増加
データの整合性が高い読まれないデータもキャッシュに載る

適したユースケース: データ整合性が重要なシステム

3. Write-Behind(ライトビハインド / Write-Back)

キャッシュに先に書き込み、DBへの反映は非同期で行います。

class WriteBehindRepository {
  private writeQueue: WriteOperation[] = [];

  async updateProduct(id: string, data: ProductUpdate): Promise<Product> {
    const product = { ...data, id, updatedAt: new Date() } as Product;

    // 1. キャッシュに即座に書き込み(高速)
    await this.cache.set(`product:${id}`, JSON.stringify(product), 3600);

    // 2. 書き込みキューに追加(非同期でDBに反映)
    this.writeQueue.push({
      operation: 'update',
      entity: 'product',
      id,
      data: product,
      timestamp: Date.now(),
    });

    return product; // すぐにレスポンスを返せる
  }

  // バックグラウンドでキューを処理
  async processWriteQueue(): Promise<void> {
    while (this.writeQueue.length > 0) {
      const op = this.writeQueue.shift()!;
      try {
        await this.db.execute(op);
      } catch (error) {
        // リトライキューに入れる
        this.retryQueue.push(op);
      }
    }
  }
}
メリットデメリット
書き込みが非常に高速データ損失のリスクがある
DBへの書き込み負荷を平準化実装が複雑

適したユースケース: 書き込みが大量で多少のデータ損失が許容できるシステム(ログ、分析データなど)


戦略の比較と選択

基準Cache-AsideWrite-ThroughWrite-Behind
読み取り速度ミス時遅い常に高速常に高速
書き込み速度標準やや遅い非常に高速
データ整合性
実装複雑度
データ損失リスクなしなしあり

まとめ

ポイント内容
Cache-Aside読み取り時に遅延読み込み。最も一般的
Write-Through書き込み時に同期更新。整合性重視
Write-Behind非同期書き込み。スピード重視
選択基準読み書き比率、整合性要件、許容レイテンシで判断

チェックリスト

  • 3つのキャッシュ戦略の違いを説明できる
  • それぞれのメリット・デメリットを理解した
  • ユースケースに応じた戦略選択ができる

次のステップへ

次は「多層キャッシュアーキテクチャ」を学びます。ブラウザからデータベースまで、各層でのキャッシュの役割を理解しましょう。


推定読了時間: 30分