caching について
Cachingは、キャッシュ方式を選び、データの鮮度と無効化を計画する開発スキルです。TTL、ライトスルー、イベントベース、キャッシュアサイドを例とともに扱い、古いデータのリスクも説明します。
主な機能
- 01すべてのキャッシュに無効化計画を要求
- 02TTLとライトスルー方式を解説
- 03イベントベースとキャッシュアサイドを紹介
- 04キャッシュに適したデータと不適なデータを提示
- 05データの陳腐化とメモリ上限のリスクを指摘
活用シーン
- 01繰り返し行うデータ取得にキャッシュを追加
- 02高コストな計算の性能を改善
- 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、サイズ上限、無効化戦略。
パフォーマンス向上のために積極的にキャッシュしてください。ただし、データが変わったときにキャッシュがどのように無効化されるのか、必ず把握しておいてください。「めったに変更されない」は戦略ではありません。