ストーリー
セール開始のカウントダウンが始まった。0時0分0秒、数万人のユーザーが一斉にトップページにアクセス。
高橋アーキテクトの声は冷静だった。
Thundering Herd(サンダリングハード)
キャッシュの期限が切れた瞬間に大量のリクエストが同時にDBに問い合わせてしまう現象です。
// 問題: TTL切れ → 全リクエストがDBに殺到
// 1000 RPS の状態でキャッシュが切れると、1000リクエストが同時にDBへ
// 対策1: ロックを使って1つのリクエストだけがDBに問い合わせる
class ThunderingHerdProtection {
constructor(
private cache: CacheClient,
private db: Database,
private lock: DistributedLock,
) {}
async getWithLock(key: string, fetchFn: () => Promise<string>): Promise<string> {
// 1. キャッシュ確認
const cached = await this.cache.get(key);
if (cached) return cached;
// 2. ロック取得を試みる
const lockKey = `lock:${key}`;
const acquired = await this.lock.acquire(lockKey, 5000); // 5秒タイムアウト
if (acquired) {
try {
// 3. ダブルチェック(ロック取得中に別リクエストがキャッシュを更新した可能性)
const rechecked = await this.cache.get(key);
if (rechecked) return rechecked;
// 4. DBから取得してキャッシュに保存
const result = await fetchFn();
await this.cache.setex(key, 3600, result);
return result;
} finally {
await this.lock.release(lockKey);
}
} else {
// 5. ロック取得失敗 → 少し待ってリトライ
await sleep(100);
return this.getWithLock(key, fetchFn);
}
}
}
対策2: 確率的早期更新(Probabilistic Early Recomputation)
// TTL切れ前に確率的にキャッシュを更新する
class ProbabilisticCache {
async getWithEarlyRecompute(
key: string,
fetchFn: () => Promise<string>,
ttlSeconds: number,
beta: number = 1.0 // 更新の早さを制御するパラメータ
): Promise<string> {
const cached = await this.cache.getWithTTL(key);
if (cached) {
const { value, remainingTTL } = cached;
// 残りTTLが少ないほど、再計算する確率が上がる
const shouldRecompute =
remainingTTL - beta * Math.log(Math.random()) * ttlSeconds <= 0;
if (!shouldRecompute) {
return value; // まだ十分なTTLがある
}
// バックグラウンドで再計算(現在のキャッシュはそのまま返す)
this.recomputeInBackground(key, fetchFn, ttlSeconds);
return value;
}
// キャッシュミス → 通常取得
const result = await fetchFn();
await this.cache.setex(key, ttlSeconds, result);
return result;
}
}
Cache Stampede(キャッシュスタンピード)
人気のあるキーのキャッシュが失われたときに、同時に大量のリクエストがバックエンドに押し寄せる現象です。Thundering Herdと似ていますが、こちらはキャッシュの削除やサーバー障害が原因です。
// 対策: Stale-While-Revalidate パターン
class StaleWhileRevalidateCache {
async get(
key: string,
fetchFn: () => Promise<string>,
ttlSeconds: number,
staleTTLSeconds: number = 300 // 古いデータを返せる猶予期間
): Promise<string> {
const entry = await this.cache.getEntry(key);
if (entry) {
if (!entry.isExpired) {
return entry.value; // 新鮮なキャッシュ
}
if (!entry.isStale) {
// 期限切れだがまだ stale 期間内 → 古いデータを返しつつ裏で更新
this.revalidateInBackground(key, fetchFn, ttlSeconds, staleTTLSeconds);
return entry.value;
}
}
// 完全にキャッシュがない → 通常取得
const result = await fetchFn();
await this.setWithStaleTTL(key, result, ttlSeconds, staleTTLSeconds);
return result;
}
}
ホットキー問題
特定のキーに大量のアクセスが集中し、キャッシュサーバーの1ノードに負荷が偏る問題です。
// 対策: キーのレプリケーション
class HotKeyProtection {
private readonly REPLICA_COUNT = 5;
async get(key: string): Promise<string | null> {
if (await this.isHotKey(key)) {
// ホットキーはランダムなレプリカから読む
const replicaIndex = Math.floor(Math.random() * this.REPLICA_COUNT);
return this.cache.get(`${key}:replica:${replicaIndex}`);
}
return this.cache.get(key);
}
async set(key: string, value: string, ttl: number): Promise<void> {
await this.cache.setex(key, ttl, value);
if (await this.isHotKey(key)) {
// ホットキーは複数レプリカに書き込む
const promises = Array.from({ length: this.REPLICA_COUNT }, (_, i) =>
this.cache.setex(`${key}:replica:${i}`, ttl, value)
);
await Promise.all(promises);
}
}
private async isHotKey(key: string): Promise<boolean> {
// アクセス頻度を監視して判定
const count = await this.accessCounter.increment(key);
return count > 10000; // 閾値を超えたらホットキー
}
}
キャッシュの一貫性
分散環境では、複数のキャッシュノード間でデータの一貫性を保つことが課題になります。
| 一貫性モデル | 説明 | ユースケース |
|---|---|---|
| 強い一貫性 | 常に最新データを返す | 決済、在庫管理 |
| 結果整合性 | 時間差で最終的に一致 | 商品情報、プロフィール |
| 読み取り一貫性 | 自分の書き込みは即座に見える | ユーザー設定 |
// Read-Your-Writes 一貫性の実装例
class ReadYourWritesCache {
async update(userId: string, key: string, value: string): Promise<void> {
await this.db.update(key, value);
await this.cache.set(key, value, 3600);
// このユーザーのローカルキャッシュも更新
await this.localCache.set(`${userId}:${key}`, value, 60);
}
async get(userId: string, key: string): Promise<string> {
// まず自分のローカルキャッシュを確認
const local = await this.localCache.get(`${userId}:${key}`);
if (local) return local; // 自分の書き込みが反映されている
// 通常のキャッシュから取得
return this.cache.get(key) || this.db.get(key);
}
}
まとめ
| ポイント | 内容 |
|---|---|
| Thundering Herd | TTL切れに大量リクエストがDB殺到 → ロック or 確率的早期更新 |
| Cache Stampede | キャッシュ消失で負荷集中 → Stale-While-Revalidate |
| ホットキー | 特定キーにアクセス集中 → レプリケーション |
| 一貫性 | 分散環境でのデータ整合性 → 要件に応じた一貫性モデル選択 |
チェックリスト
- Thundering Herdの原因と対策を説明できる
- Cache Stampedeの原因と対策を説明できる
- ホットキー問題の対策を理解した
- 一貫性モデルの選択基準を把握した
次のステップへ
次は演習です。ECサイトのキャッシュ戦略を設計し、ここまで学んだ知識を実践に活かしましょう。
推定読了時間: 30分