Redisキャッシュスタンピードを防ぐ:分散ロックとジッターによる高負荷対策

大規模なプロモーションやイベントの開始直後、キャッシュの有効期限が一斉に切れた瞬間にデータベース(DB)が応答不能になった経験はありませんか?

この記事では、Redisの運用で最も危険な「キャッシュスタンピード(ドッグパイル現象)」を、分散ロックとジッター(Jitter)を活用して確実に防ぐ手法を解説します。

TL;DR — キャッシュスタンピードは、分散ロックでDBへの問い合わせを1つに制限し、TTLにランダムな「ジッター」を加えて有効期限の重複を避けることで解決できます。

1. キャッシュスタンピードとは

💡 イメージで理解する: 人気ショップのセール開始時に、唯一の入り口(キャッシュ)が閉まってしまい、数千人の客(リクエスト)が一気に裏口(DB)へ押し寄せ、建物が崩壊する状態です。

キャッシュスタンピード(Cache Stampede)は、高頻度でアクセスされるキャッシュキーが期限切れ(TTL切れ)になった際、複数のアプリケーションスレッドが同時にDBへクエリを投げることで発生します。最新のRedis 7.x環境でも、アプリケーション側のロジックが不適切であれば防げません。

この現象の恐ろしい点は、DBの負荷が急上昇し、応答が遅延することでさらに多くのリクエストが滞留し、システム全体が連鎖的にダウンする「カスケード故障」を引き起こすことです。

2. 実務でこの対策が必要な状況

プロモーションメールを数百万人に一斉送信した直後など、特定のキーにトラフィックが集中する状況で必須となります。LSIキーワードである「ドッグパイル(Dogpile)」とも呼ばれるこの現象は、読み取り専用のトラフィックであってもDBを物理的に破壊するパワーを持っています。

また、マイクロサービスアーキテクチャにおいて、一つの共通キャッシュキーを複数のサービスが参照している場合、一つのサービスがDBを詰まらせることで、無関係なサービスまで巻き添えになるケースで特に有効です。

3. 実装ガイド:分散ロックとジッター

分散ロックで「誰がDBを見に行くか」を決め、ジッターで「いつ期限切れにするか」を散らします。

ステップ 1. TTLにジッターを追加する

すべてのキャッシュの有効期限を「3600秒」と固定せず、数%のランダムな時間を加えます。これにより、期限切れのタイミングを分散させます。

// Java/Jedis example: TTLにランダム性を付与
int baseTTL = 3600;
double jitterRange = 0.1; // 10%
int finalTTL = baseTTL + (int)(Math.random() * baseTTL * jitterRange);

redis.setex(cacheKey, finalTTL, value);

ステップ 2. Redissonによる分散ロックの実装

キャッシュミスが発生した際、最初にロックを取得した1つのスレッドだけがDBへアクセスし、他のスレッドは待機またはキャッシュの更新を待ちます。

// Redissonを使用した分散ロックパターン
RLock lock = redisson.getLock("lock:" + cacheKey);
try {
    // 10秒間待機し、取得後は2秒で自動解放
    if (lock.tryLock(10, 2, TimeUnit.SECONDS)) {
        // ロック取得成功:DBからデータを取得してキャッシュ更新
        String data = database.fetch(cacheKey);
        redis.setex(cacheKey, finalTTL, data);
    } else {
        // ロック取得失敗:少し待機してからキャッシュを再読み込み(Fallback)
        Thread.sleep(100);
        return redis.get(cacheKey);
    }
} finally {
    if (lock.isHeldByCurrentThread()) {
        lock.unlock();
    }
}

ステップ 3. 動作検証

JMeterなどのツールで同時実行数を100以上に設定し、DBへのクエリログを確認します。キャッシュが切れた瞬間に、クエリが1回しか発行されていなければ成功です。

4. 分散ロック vs ジッター vs 確率的早期再計算

それぞれの特性を理解し、システムの重要度に応じて使い分けます。

基準分散ロックジッター (Jitter)確率的早期再計算 (PER)
実装難易度中(Redlock等の理解が必要)低(乱数のみ)高(アルゴリズム実装)
DB保護性能非常に高い中(ピークを抑える)高い
応答遅延ロック待機による若干の遅延なしなし(バックグラウンド更新)
適した場面在庫情報など正確性が重要な時一般的な記事、静的コンテンツ超大規模トラフィック

「DBへの負荷をゼロに近づけたいなら分散ロック、手軽にリスクを下げたいならジッター」を選択してください。

5. 注意事項

⚠️ よくあるミス: ロックの解放漏れ(デッドロック)が発生すると、キャッシュが更新されずDBも参照できない「ゾンビ状態」になります。

ロックの有効期限(Lease Time)は、必ずDBクエリの最大想定時間よりも長く設定してください。DBが重いからといってロックを短くしすぎると、ロックが自動解放された後に別のスレッドが再度ロックを取得し、結局スタンピードが発生します。

エラー別の対処法

// Redisson: Lock watch dog timeout
// 原因: DB処理が長引き、ロックの自動延長が追いついていない
// 対策: DBクエリのタイムアウトを設定し、LockのLeaseTimeを明示的に指定する

6. 実戦的な最適化チップス

ジッターの範囲は5%〜10%程度が推奨されます。これ以上大きくするとキャッシュ効率が落ち、小さすぎると分散効果が薄れます。また、Redisのメモリが100%に近い状態では、LRUポリシーによってTTLに関わらずキーが削除されるため、メモリ監視もセットで行ってください。

プロモーション開始の10分前に、スクリプトで意図的にジッターを乗せたキャッシュを事前生成(ウォームアップ)しておくことで、開始直後のスパイクを20%以上軽減できます。

📌 まとめ

  • キャッシュスタンピードは分散ロックで物理的に防ぐ。
  • TTLには必ずジッターを加え、期限切れのタイミングを散らす。
  • ロックの実装時は、必ずtry-finallyで解放を保証する。

よくある質問

Q. ジッターだけでスタンピードは防げますか?

A. 負荷は分散されますが、完全な同時アクセスは防げないため分散ロックとの併用が安全です。

Q. 分散ロックを使うとパフォーマンスが落ちませんか?

A. ロック取得はRedis上で行われるため数msです。DBがダウンするリスクに比べれば無視できます。

Q. Redis Cluster環境でも同じ実装で大丈夫ですか?

A. はい、Redissonなどのライブラリを使えばCluster環境でも透過的に分散ロックを利用可能です。

Post a Comment