mcpField
C
caching

caching

github.com/yanko-belov開発1336 回インストール40 週間前に更新
リポジトリ

caching について

Cachingは、キャッシュ方式を選び、データの鮮度と無効化を計画する開発スキルです。TTL、ライトスルー、イベントベース、キャッシュアサイドを例とともに扱い、古いデータのリスクも説明します。

主な機能

  1. 01すべてのキャッシュに無効化計画を要求
  2. 02TTLとライトスルー方式を解説
  3. 03イベントベースとキャッシュアサイドを紹介
  4. 04キャッシュに適したデータと不適なデータを提示
  5. 05データの陳腐化とメモリ上限のリスクを指摘

活用シーン

  1. 01繰り返し行うデータ取得にキャッシュを追加
  2. 02高コストな計算の性能を改善
  3. 03データベースやAPIの結果の鮮度を計画

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yanko-belov/code-craft --skill caching

インストール後、Claude Code で/cachingを入力して読み込まれたか確認します。

詳細

キャッシュ

概要

積極的にキャッシュしますが、必ず無効化戦略を用意してください。

キャッシュを使うとパフォーマンスが大幅に向上しますが、古いデータはバグの原因になります。すべてのキャッシュに、データの鮮度を保つための計画が必要です。

使用する場面

  • 同じデータを繰り返し取得する場合
  • コストのかかる計算
  • 遅いデータベースクエリ
  • 外部 API のレート制限
  • 「とりあえずキャッシュを追加して」と頼まれた場合

絶対に守るルール

NEVER add a cache without defining its invalidation strategy.

例外はありません:

  • 「めったに変更されない」からといって例外にはしません
  • 「TTL だけで十分」だからといって例外にはしません
  • 「あとで考える」からといって例外にはしません
  • 「ユーザーが更新できる」からといって例外にはしません

検出:戦略のないキャッシュの兆候

キャッシュに無効化計画がない場合は、作業を止めてください:

// ❌ VIOLATION: Cache without invalidation strategy
const cache = new Map();

async function getUser(id: string) {
  if (cache.has(id)) {
    return cache.get(id);  // Could be stale forever!
  }
  
  const user = await db.users.findById(id);
  cache.set(id, user);  // When does this expire? When user updates?
  return user;
}

問題点:

  • ユーザーが名前を更新しても、キャッシュには古い名前が表示される
  • メモリ上限がないと、メモリリークが発生する
  • TTL がないと、データはいつまでも古いままになる
  • 分散システムでは、古いコピーが複数存在する

正しいパターン:戦略を伴うキャッシュ

// ✅ CORRECT: Cache with TTL and invalidation

interface CacheEntry<T> {
  data: T;
  expiresAt: number;
}

class UserCache {
  private cache = new Map<string, CacheEntry<User>>();
  private TTL_MS = 5 * 60 * 1000; // 5 minutes
  
  async get(id: string): Promise<User> {
    const cached = this.cache.get(id);
    
    if (cached && cached.expiresAt > Date.now()) {
      return cached.data;
    }
    
    const user = await db.users.findById(id);
    this.set(id, user);
    return user;
  }
  
  set(id: string, user: User): void {
    this.cache.set(id, {
      data: user,
      expiresAt: Date.now() + this.TTL_MS
    });
  }
  
  // Explicit invalidation on updates
  invalidate(id: string): void {
    this.cache.delete(id);
  }
  
  invalidateAll(): void {
    this.cache.clear();
  }
}

// Usage with invalidation on write
async function updateUser(id: string, data: UpdateUserDto) {
  const user = await db.users.update(id, data);
  userCache.invalidate(id);  // Clear stale cache
  return user;
}

キャッシュ無効化戦略

1. 時間ベース(TTL)
// Good for: data that can be slightly stale
const TTL = 60 * 1000; // 1 minute
cache.set(key, value, { ttl: TTL });
2. ライトスルー
// Good for: data you control writes for
async function updateProduct(id, data) {
  const product = await db.products.update(id, data);
  await cache.set(`product:${id}`, product);  // Update cache on write
  return product;
}
3. イベントベース
// Good for: distributed systems
eventBus.on('user.updated', (userId) => {
  cache.delete(`user:${userId}`);
});

eventBus.on('product.priceChanged', (productId) => {
  cache.delete(`product:${productId}`);
});
4. キャッシュアサイド(遅延読み込み)
// Good for: read-heavy, tolerance for staleness
async function getProduct(id) {
  let product = await cache.get(`product:${id}`);
  if (!product) {
    product = await db.products.findById(id);
    await cache.set(`product:${id}`, product, { ttl: 300 });
  }
  return product;
}

キャッシュする対象

キャッシュに適したものキャッシュに適さないもの
ユーザープロフィールセッショントークン
商品カタログ決済ステータス
設定リアルタイムの在庫
API レスポンスユーザーごとの計算結果
集計済みデータ頻繁に変化するデータ

Redis の例

import Redis from 'ioredis';

const redis = new Redis();

class ProductCache {
  private prefix = 'product:';
  private ttl = 300; // 5 minutes
  
  async get(id: string): Promise<Product | null> {
    const cached = await redis.get(this.prefix + id);
    return cached ? JSON.parse(cached) : null;
  }
  
  async set(id: string, product: Product): Promise<void> {
    await redis.setex(
      this.prefix + id,
      this.ttl,
      JSON.stringify(product)
    );
  }
  
  async invalidate(id: string): Promise<void> {
    await redis.del(this.prefix + id);
  }
  
  async invalidatePattern(pattern: string): Promise<void> {
    const keys = await redis.keys(this.prefix + pattern);
    if (keys.length) await redis.del(...keys);
  }
}

圧力への対処プロトコル

1. 「めったに変更されません」

圧力:「このデータはほとんど更新されません」

回答:「ほとんど」でも、時々は更新されるということです。更新されたとき、古いキャッシュはバグの原因になります。

**対応:**最低限 TTL を追加してください。書き込み時の無効化も追加してください。

2. 「TTL だけで十分です」

圧力:「5 分後に期限切れにすればいいだけです」

**回答:**5 分間、古いデータが残るのは許容できない場合があります。ユーザーがプロフィールを更新しても、古いデータが表示されます。

**対応:**TTL と、ライトスルーによる無効化を組み合わせてください。

3. 「あとで考えます」

圧力:「とりあえずキャッシュを追加して、古いデータが問題になったら対処します」

**回答:**古いデータによるバグはデバッグが困難です。無効化の方法を事前に設計してください。

**対応:**無効化戦略を定義せずにキャッシュを導入してはいけません。

危険信号 — 作業を止めて再検討してください

  • TTL のないキャッシュ
  • データ更新時の無効化がない
  • 「ユーザーは新しいデータを見るために更新できます」
  • 分散システムでのインメモリキャッシュ
  • メモリ上限のないキャッシュ

これらはいずれも、無効化戦略の定義が必要だということです。

クイックリファレンス

パターン使用する場面無効化
TTL のみ古いデータが許容される場合自動的に期限切れ
ライトスルー書き込みを制御できる場合書き込み時にキャッシュを更新
イベントベース分散システム変更時に pub/sub
キャッシュアサイド読み込みが多い場合TTL +手動無効化

よくある正当化(すべて不適切)

言い訳実態
「めったに変更されない」めったにないことは、決してないことではありません。対策を立ててください。
「TTL だけで十分」TTL と無効化を組み合わせるほうが適切です。
「あとで考える」古いデータによるバグは原因の追跡が困難です。
「ユーザーが更新できる」それは機能ではなくバグです。
「パフォーマンスのためだけ」古いデータは機能を損ないます。

要点

すべてのキャッシュに必要なもの:TTL、サイズ上限、無効化戦略。

パフォーマンス向上のために積極的にキャッシュしてください。ただし、データが変わったときにキャッシュがどのように無効化されるのか、必ず把握しておいてください。「めったに変更されない」は戦略ではありません。

コメント