ストーリー
高橋アーキテクトが設計書を広げた。
ミッション概要
ECサイト “SpeedShop” のキャッシュアーキテクチャを設計する6つのミッションに挑戦します。
| ミッション | テーマ | 難易度 |
|---|---|---|
| Mission 1 | キャッシュ対象の選定 | 初級 |
| Mission 2 | Cache-Aside の実装 | 初級 |
| Mission 3 | 多層キャッシュの設計 | 中級 |
| Mission 4 | キャッシュ無効化戦略 | 中級 |
| Mission 5 | Thundering Herd 対策 | 上級 |
| Mission 6 | 総合設計ドキュメント | 上級 |
Mission 1: キャッシュ対象の選定(10分)
以下のSpeedShopのデータについて、キャッシュすべきかどうか、すべき場合のTTLを決定してください。
| データ | アクセス頻度 | 更新頻度 | リアルタイム性 |
|---|---|---|---|
| 商品一覧(カテゴリ別) | 非常に高い | 1日数回 | 中 |
| 商品詳細 | 高い | 数日に1回 | 中 |
| 在庫数 | 高い | リアルタイム | 高 |
| カート内容 | 中 | 随時 | 高 |
| ユーザーセッション | 高い | 随時 | 高 |
| 注文履歴 | 低い | ほぼなし | 低 |
| ランキング | 非常に高い | 1時間ごと | 低 |
解答例
| データ | キャッシュ | TTL | 戦略 | 理由 |
|---|---|---|---|---|
| 商品一覧 | する | 5分 | Cache-Aside + イベント無効化 | 高アクセス、変更時に即座に反映 |
| 商品詳細 | する | 30分 | Cache-Aside + イベント無効化 | 高アクセス、変更頻度低い |
| 在庫数 | する | 10秒 | Write-Through | リアルタイム性が必要だが短TTL |
| カート内容 | する | 30分 | Write-Through | セッション紐付き、整合性重要 |
| ユーザーセッション | する | 30分 | Write-Through | 認証に必要、整合性重要 |
| 注文履歴 | する | 1時間 | Cache-Aside | 低アクセスだが変更もない |
| ランキング | する | 10分 | Cache-Aside | 高アクセス、1時間更新なら10分TTLで十分 |
Mission 2: Cache-Aside の実装(15分)
商品詳細のキャッシュをCache-Asideパターンで実装してください。
要件
- Redisをキャッシュストアとして使用
- TTLは30分
- キャッシュミス時はDBから取得
- エラーハンドリング(Redis障害時はDBフォールバック)
// この関数を完成させてください
class ProductCacheRepository {
constructor(
private redis: RedisClient,
private db: Database,
) {}
async getProduct(id: string): Promise<Product> {
// TODO: Cache-Aside パターンを実装
}
}
解答例
class ProductCacheRepository {
private readonly TTL_SECONDS = 1800; // 30分
constructor(
private redis: RedisClient,
private db: Database,
) {}
async getProduct(id: string): Promise<Product> {
const cacheKey = `product:${id}`;
// 1. キャッシュから取得を試みる
try {
const cached = await this.redis.get(cacheKey);
if (cached) {
return JSON.parse(cached) as Product;
}
} catch (error) {
// Redis障害時はログを出してDBフォールバック
console.warn(`Cache read failed for ${cacheKey}:`, error);
}
// 2. DBから取得
const product = await this.db.findProduct(id);
if (!product) {
throw new NotFoundError(`Product ${id} not found`);
}
// 3. キャッシュに保存(失敗してもDBからの結果は返す)
try {
await this.redis.setex(
cacheKey,
this.TTL_SECONDS,
JSON.stringify(product)
);
} catch (error) {
console.warn(`Cache write failed for ${cacheKey}:`, error);
}
return product;
}
}
ポイント:
- Redis障害時にもサービスが継続できるようtry-catchでラップ
- キャッシュはあくまで高速化の手段であり、障害時はDBにフォールバック
Mission 3: 多層キャッシュの設計(20分)
SpeedShopの商品ページに対して、4層のキャッシュ設計を行ってください。
要件
各層(ブラウザ、CDN、アプリケーション、DB)について以下を定義:
- 何をキャッシュするか
- TTLをいくらにするか
- Cache-Controlヘッダーの設定
解答例
// Layer 1: ブラウザキャッシュ設定
const browserCacheConfig = {
// 静的アセット(CSS, JS, 画像)
staticAssets: {
cacheControl: 'public, max-age=31536000, immutable',
// ファイル名にハッシュを含める: style.abc123.css
},
// 商品ページHTML
productPage: {
cacheControl: 'private, max-age=0, must-revalidate',
etag: true, // ETagで変更確認
},
// API レスポンス(商品詳細)
productApi: {
cacheControl: 'private, max-age=60, stale-while-revalidate=300',
// 1分キャッシュ、5分間は古いデータを返しつつ裏で更新
},
};
// Layer 2: CDNキャッシュ設定
const cdnConfig = {
staticAssets: {
ttl: 86400 * 365, // 1年(キャッシュバスティングで管理)
compress: true,
},
productImages: {
ttl: 86400, // 24時間
compress: true,
imageOptimization: true,
},
productPageHtml: {
ttl: 60, // 1分
varyHeaders: ['Accept-Encoding'],
},
};
// Layer 3: アプリケーションキャッシュ(Redis)
const appCacheConfig = {
productDetail: { ttl: 1800 }, // 30分
productList: { ttl: 300 }, // 5分
ranking: { ttl: 600 }, // 10分
userSession: { ttl: 1800 }, // 30分
};
// Layer 4: データベースキャッシュ
const dbConfig = {
sharedBuffers: '4GB',
effectiveCacheSize: '12GB',
// 商品テーブルのインデックスがバッファに乗るように設計
};
Mission 4: キャッシュ無効化戦略(15分)
商品情報が更新された場合のキャッシュ無効化フローを設計してください。
シナリオ
管理者が商品の価格を変更した場合:
- どのキャッシュを無効化するか
- どの順序で無効化するか
- CDNキャッシュはどう扱うか
解答例
class ProductCacheInvalidator {
async onProductUpdated(event: ProductUpdatedEvent): Promise<void> {
const { productId, categoryId, oldPrice, newPrice } = event;
// Step 1: アプリケーションキャッシュの無効化(即座に)
await Promise.all([
this.redis.del(`product:${productId}`),
this.redis.del(`product-list:category:${categoryId}`),
this.redis.del('ranking:sales'),
this.redis.del('ranking:popular'),
]);
// Step 2: CDNキャッシュの無効化
await this.cdn.invalidate([
`/products/${productId}`,
`/api/products/${productId}`,
`/categories/${categoryId}`,
]);
// Step 3: ブラウザキャッシュは直接制御できない
// → ETagの更新で次回アクセス時に検証
// → WebSocket/SSEで接続中のユーザーに通知(オプション)
// Step 4: 監査ログ
await this.auditLog.record({
action: 'cache_invalidation',
trigger: 'product_price_change',
productId,
oldPrice,
newPrice,
timestamp: new Date(),
});
}
}
無効化の順序:
- アプリケーションキャッシュ(Redis) → 最も制御しやすく即座に反映
- CDNキャッシュ → API経由で無効化リクエスト(数秒かかる)
- ブラウザキャッシュ → 直接制御不可、ETag/Cache-Controlで間接制御
Mission 5: Thundering Herd 対策(15分)
セール開始時(0時0分)に予想される Thundering Herd に対する防御策を実装してください。
要件
- ランキングページのキャッシュTTLが0時にちょうど切れる
- ピーク時3000 RPSが予想される
- DBへの同時クエリは最大10に制限したい
解答例
class SaleEventCacheProtection {
private locks = new Map<string, Promise<string>>();
// Singleflight パターン: 同じキーへの同時リクエストを1つにまとめる
async getWithSingleflight(
key: string,
fetchFn: () => Promise<string>,
ttlSeconds: number
): Promise<string> {
// 1. キャッシュ確認
const cached = await this.redis.get(key);
if (cached) return cached;
// 2. 既に同じキーのフェッチが進行中なら、その結果を待つ
if (this.locks.has(key)) {
return this.locks.get(key)!;
}
// 3. フェッチを開始し、他のリクエストと共有
const fetchPromise = (async () => {
try {
const result = await fetchFn();
await this.redis.setex(key, ttlSeconds, result);
return result;
} finally {
this.locks.delete(key);
}
})();
this.locks.set(key, fetchPromise);
return fetchPromise;
}
// セール前のキャッシュウォームアップ
async warmupBeforeSale(): Promise<void> {
const popularKeys = [
'ranking:sales',
'ranking:popular',
'sale-products:page1',
'sale-products:page2',
'sale-products:page3',
];
for (const key of popularKeys) {
const data = await this.fetchFromDB(key);
// TTLをセール開始後も持続するよう長めに設定
await this.redis.setex(key, 7200, data); // 2時間
}
console.log(`Warmed up ${popularKeys.length} cache keys before sale`);
}
}
対策のまとめ:
- 事前ウォームアップ — セール開始前にキャッシュを温めておく
- Singleflight — 同じキーのリクエストを1つにまとめる
- TTLのジッター — 全キーが同時に切れないようランダムなオフセットを追加
Mission 6: 総合設計ドキュメント(15分)
これまでの設計をまとめた1ページの設計ドキュメントを書いてください。
記載項目
- アーキテクチャ図(テキストで可)
- 各層のキャッシュ設定一覧
- 無効化戦略
- 障害時のフォールバック方針
- モニタリング指標
解答例
# SpeedShop キャッシュ設計書
## 1. アーキテクチャ
ユーザー → [ブラウザキャッシュ] → [CDN (CloudFront)]
→ [ALB] → [App Server × 3] → [Redis Cluster]
→ [PostgreSQL (Primary + Read Replica)]
## 2. キャッシュ設定
| 層 | 対象 | TTL | 戦略 |
|----|------|-----|------|
| ブラウザ | 静的アセット | 1年(immutable) | キャッシュバスティング |
| ブラウザ | API | 1分 | stale-while-revalidate |
| CDN | 画像 | 24時間 | パス無効化 |
| CDN | HTML | 1分 | パス無効化 |
| Redis | 商品詳細 | 30分 | Cache-Aside + イベント無効化 |
| Redis | 一覧/ランキング | 5-10分 | Cache-Aside |
| Redis | セッション | 30分 | Write-Through |
| DB | クエリキャッシュ | 自動 | PostgreSQL設定 |
## 3. 無効化戦略
- 商品更新: イベントベース(Redis即削除 → CDN無効化)
- 在庫変更: Write-Through + 短TTL(10秒)
- ランキング: TTLベース(10分) + 定期バッチ更新
## 4. 障害時フォールバック
- Redis障害: DBフォールバック + アラート
- CDN障害: オリジン直接アクセス
- DB障害: キャッシュのみで読み取り継続(書き込み不可)
## 5. モニタリング指標
- キャッシュヒット率: 目標95%以上
- p99レイテンシ: 目標500ms以下
- Redis メモリ使用率: 閾値80%でアラート
- DB接続数: 閾値80%でアラート
達成度チェック
| ミッション | テーマ | 完了 |
|---|---|---|
| Mission 1 | キャッシュ対象の選定 | [ ] |
| Mission 2 | Cache-Aside の実装 | [ ] |
| Mission 3 | 多層キャッシュの設計 | [ ] |
| Mission 4 | キャッシュ無効化戦略 | [ ] |
| Mission 5 | Thundering Herd 対策 | [ ] |
| Mission 6 | 総合設計ドキュメント | [ ] |
チェックリスト
- データの特性に応じてキャッシュ戦略を選択できる
- Cache-Asideパターンをエラーハンドリング付きで実装できる
- 多層キャッシュの各層の設定を定義できる
- 無効化フローを設計できる
- Thundering Herd対策を実装できる
次のステップへ
お疲れさまでした。キャッシュ設計の実践力が身についたはずです。
次のセクションでは、Step 2の理解度チェックです。
推定所要時間: 90分