ストーリー
高橋アーキテクトがうなずいた。
キャッシュ無効化の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分