mcpField
C
cache-strategy

cache-strategy

github.com/ariadoss開発1259 回インストール今日更新
リポジトリ

cache-strategy について

恒久的なキャッシュ優先方式を適用する開発スキルです。読み取り時はキャッシュを参照し、未ヒットならデータベースを読み、データ変更後に無効化する手順を扱います。対象選定、キー規約、任意の事前ウォームアップも含みます。

主な機能

  1. 01既存のキャッシュ基盤を調査
  2. 02キャッシュ対象のデータベース読み取りを特定
  3. 03キャッシュ優先読み取りを実装
  4. 04書き込みや削除後にキャッシュを無効化
  5. 05キャッシュキーの命名規則を定義
  6. 06任意の起動時ウォームアップを追加

活用シーン

  1. 01頻繁に参照され変更の少ないデータのDB読み取りを削減
  2. 02ユーザープロフィールや商品詳細をキャッシュ
  3. 03変更後に関連エントリーや一覧を削除
  4. 04重要データをアプリ起動時に事前ウォームアップ

インストール

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

$npx skills add https://github.com/ariadoss/superskills --skill cache-strategy

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

詳細

/cache-strategy

永続的なキャッシュファースト戦略を実装します。まずキャッシュから読み込み、キャッシュミス時には DB にフォールバックし、キャッシュに書き込みます。無効化するのはデータが変更されたときだけです。期限切れタイマーは使用しません。キャッシュは、基盤となるデータが変更されるまで有効です。

考え方

目標は、(データが変更されるまで)データ項目ごとの DB 読み取りを生涯で 1 回にすることです。

READ:   cache hit? → return cached value
        cache miss? → query DB → write to cache → return value

WRITE:  update DB → invalidate (or update) cache entry → done

これは Play Framework のモデルです。TTL、ポーリング、バックグラウンド更新はありません。ソースが変更されたときに正確に無効化されるため、キャッシュは常にウォームで、常に正確です。

TTL より永続キャッシュを選ぶ理由

  • TTL キャッシュはシンプルさと引き換えに古いデータの読み取りを許容します。データが何も変更されていなくても、期限切れのたびに必ず DB へのアクセスが発生します。
  • 無効化を伴う永続キャッシュなら、不要な DB アクセスも古いデータの読み取りもゼロにできます。
  • TTL が適切なのは、データの変更時期を把握できない場合(サードパーティ API など)に限られます。

ステップ 1:キャッシュ基盤を確認する

# Check for Redis
grep -rn "redis\|Redis\|REDIS_URL" --include="*.json" --include="*.env*" --include="*.yml" --include="*.yaml" --include="*.rb" --include="*.py" --include="*.js" --include="*.ts" . 2>/dev/null | grep -v node_modules | head -10

# Check for Memcached
grep -rn "memcached\|Memcached\|MEMCACHE" --include="*.json" --include="*.env*" --include="*.yml" . 2>/dev/null | grep -v node_modules | head -5

# Check for in-process cache (node-cache, lru-cache, etc.)
grep -rn "lru-cache\|node-cache\|memory-cache\|caffeine\|guava.*cache" --include="*.json" --include="*.js" --include="*.ts" --include="*.java" . 2>/dev/null | grep -v node_modules | head -5

利用可能なものを報告します。キャッシュ層がない場合は、デフォルトとして Redis を推奨し、クライアントライブラリの追加を提案します。

ステップ 2:キャッシュ可能な DB 読み取りを特定する

DB クエリのパターンを調べ、キャッシュ可能性に基づいて分類します。

有力なキャッシュ候補(頻繁に読み取られ、変更頻度が低いデータ):

  • ユーザープロフィール、設定、環境設定
  • 商品やアイテムの詳細
  • 設定テーブル
  • 参照データ(DB に保存されたカテゴリ、タグ、列挙値)
  • 計算コストの高い集計値(件数、合計など)

キャッシュに不向きな候補(対象外にするもの):

  • リアルタイムデータ(ライブ在庫数、チャットメッセージ)
  • セッション単位のデータ
  • 書き込みのたびに変更されるデータ(リクエストごとに増加するカウンターなど)
# Find the most-called query patterns
grep -rn "\.find\b\|\.findById\|\.findOne\|\.where\|SELECT" \
  --include="*.rb" --include="*.py" --include="*.js" --include="*.ts" \
  . 2>/dev/null | grep -v node_modules | grep -v test | grep -v spec | head -30

候補ごとに、以下を記録します。

  • キャッシュキーのパターン(例:user:{id}、product:{slug})
  • 書き込みや更新を行う箇所(無効化を追加する場所を特定するため)

ステップ 3:キャッシュファーストの読み取りを実装する

キャッシュ可能な読み取りごとに、以下のパターンを適用します。

Redis(Node.js / TypeScript)
async function getUser(id: string) {
  const cacheKey = `user:${id}`;
  const cached = await redis.get(cacheKey);
  if (cached) return JSON.parse(cached);

  const user = await db.users.findById(id);
  await redis.set(cacheKey, JSON.stringify(user)); // no TTL — permanent until invalidated
  return user;
}
Redis(Ruby / Rails)
def find_user(id)
  Rails.cache.fetch("user:#{id}") do
    User.find(id)  # only called on cache miss
  end
end
Redis(Python)
def get_user(user_id: int):
    cache_key = f"user:{user_id}"
    cached = redis_client.get(cache_key)
    if cached:
        return json.loads(cached)
    user = db.session.get(User, user_id)
    redis_client.set(cache_key, json.dumps(user.to_dict()))  # no expiry
    return user
Play Framework(Scala)
def getUser(id: Long): Future[User] = {
  cache.getOrElseUpdate(s"user:$id") {
    userRepository.findById(id)
  }
}

ステップ 4:書き込み時のキャッシュ無効化を実装する

キャッシュされたデータを変更する箇所すべてで、DB 書き込みの成功直後に無効化を追加します。

Node.js / TypeScript
async function updateUser(id: string, data: Partial<User>) {
  const user = await db.users.update(id, data);
  await redis.del(`user:${id}`); // invalidate on change
  return user;
}
Ruby / Rails
def update_user(id, attrs)
  user = User.find(id).tap { |u| u.update!(attrs) }
  Rails.cache.delete("user:#{id}")
  user
end

重要: 削除時も無効化してください。

async function deleteUser(id: string) {
  await db.users.delete(id);
  await redis.del(`user:${id}`);
}

リストやコレクションの場合は、項目が変更されるたびにコレクションキーを無効化します。

await redis.del(`user:${id}`);       // the specific item
await redis.del(`users:list`);       // any cached list that included this item

ステップ 5:キャッシュキーの規約

コードベース全体で一貫したキーの命名規則を使用します。

<entity>:<identifier>          → user:123, product:slug-name
<entity>:list:<filter>         → products:list:category-5
<entity>:<id>:<relation>       → user:123:orders
aggregate:<entity>:<filter>    → aggregate:orders:user-123-total

キャッシュモジュールの先頭にコメントを追加し、規約を記載します。

// Cache key conventions:
// user:{id}                — single user record
// users:list               — paginated user list (invalidate on any user change)
// user:{id}:orders         — orders belonging to a user

ステップ 6:キャッシュのウォームアップ(任意)

コールドスタート時のキャッシュミスを絶対に避ける必要がある重要なデータについては、アプリケーションの起動時にウォームアップ処理を追加します。

async function warmCache() {
  const activeUsers = await db.users.findActiveUsers();
  await Promise.all(
    activeUsers.map(u => redis.set(`user:${u.id}`, JSON.stringify(u)))
  );
}

ステップ 7:概要

実装した内容を報告します。

## Cache Strategy Applied

### Cache layer: Redis (permanent, no TTL)

### Reads cached (X total)
- user:{id} — UserService.getUser()
- product:{slug} — ProductService.findBySlug()
- ...

### Invalidation points added (Y total)
- user:{id} — UserService.updateUser(), UserService.deleteUser()
- product:{slug} — ProductService.updateProduct()
- ...

### Estimated DB load reduction
Before: ~X DB queries/min
After:  ~Y DB queries/min (first-read only, then cache)

注:サードパーティ API や、書き込みを捕捉できないソースから取得するデータには、代替策として長い TTL(分単位ではなく時間単位)を使用します。ただし、無効化 Webhook が利用可能になったときに再検討できるよう、これらは別途明記してください。

コメント