<?xml version="1.0" encoding="utf-8" standalone="yes"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
  <channel>
    <title>Caching on AI2CORE - AI技術ブログ</title>
    <link>https://www.ai2core.com/tags/caching/</link>
    <description>Recent content in Caching on AI2CORE - AI技術ブログ</description>
    <generator>Hugo -- 0.146.4</generator>
    <language>ja</language>
    <lastBuildDate>Mon, 02 Mar 2026 09:16:00 +0900</lastBuildDate>
    <atom:link href="https://www.ai2core.com/tags/caching/index.xml" rel="self" type="application/rss+xml" />
    <item>
      <title>Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</title>
      <link>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</link>
      <pubDate>Mon, 02 Mar 2026 09:16:00 +0900</pubDate>
      <guid>https://www.ai2core.com/posts/2026-03-02-redis-cache-stampede-mitigation-guide/</guid>
      <description>Redisで発生するキャッシュスタンピードを防ぐための実践ガイド。singleflight、TTLジッター、非同期再生成を具体コード付きで解説。</description>
      <content:encoded><![CDATA[<h1 id="redisキャッシュスタンピード対策ガイド高負荷時にdbを守る設計と実装">Redisキャッシュスタンピード対策ガイド：高負荷時にDBを守る設計と実装</h1>
<p>Redis を使っていても、ピークトラフィック時に DB が突然落ちることがあります。原因の多くはキャッシュスタンピードです。人気キーの TTL が同時に切れると、大量リクエストが一斉に DB へ流れ、接続プールが飽和します。</p>
<p>「Redis を入れたのに遅い」「ピーク時だけ 500 が増える」という現象は、このパターンで説明できることが非常に多いです。</p>
<p>本記事では、キャッシュスタンピードを実運用で防ぐために、<strong>設計原則・実装パターン・監視方法</strong>を順に解説します。</p>
<h2 id="1-キャッシュスタンピードとは何か">1. キャッシュスタンピードとは何か</h2>
<p>典型シナリオ:</p>
<ol>
<li>商品ランキング API が <code>ranking:daily</code> を Redis に 300 秒で保存</li>
<li>300 秒後、人気時間帯にキー期限切れ</li>
<li>同時に 1000 リクエストが miss</li>
<li>1000 回 DB 集計が走ってレイテンシ急増</li>
</ol>
<p>このとき Redis 自体は正常でも、背後の DB が壊れます。つまり、問題はキャッシュ障害ではなく「再生成の同時実行制御」です。</p>
<h2 id="2-防御の基本は三層構え">2. 防御の基本は三層構え</h2>
<p>スタンピード対策は単一施策では不十分です。次の三層を組み合わせると安定します。</p>
<ol>
<li><strong>同時再生成の抑制</strong>（singleflight / 分散ロック）</li>
<li><strong>期限切れの分散</strong>（TTL ジッター）</li>
<li><strong>期限切れ後の挙動制御</strong>（stale-while-revalidate）</li>
</ol>
<h2 id="3-パターン1-singleflight-で同時再生成を止める">3. パターン1: singleflight で同時再生成を止める</h2>
<p>同一キーの miss が同時発生しても、1 リクエストだけ再生成し、他は待つ設計です。</p>
<p>TypeScript 例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">15
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">16
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">17
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">18
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">19
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">20
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">21
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">inflight</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">new</span> <span style="color:#a6e22e">Map</span>&lt;<span style="color:#f92672">string</span>, <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;&gt;();
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">async</span> <span style="color:#66d9ef">function</span> <span style="color:#a6e22e">getOrCompute</span>(<span style="color:#a6e22e">key</span>: <span style="color:#66d9ef">string</span>, <span style="color:#a6e22e">ttlSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">compute</span><span style="color:#f92672">:</span> () <span style="color:#f92672">=&gt;</span> <span style="color:#a6e22e">Promise</span>&lt;<span style="color:#f92672">string</span>&gt;) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">cached</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">cached</span>) <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">cached</span>;
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">if</span> (<span style="color:#f92672">!</span><span style="color:#a6e22e">inflight</span>.<span style="color:#a6e22e">has</span>(<span style="color:#a6e22e">key</span>)) {
</span></span><span style="display:flex;"><span>    <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">p</span> <span style="color:#f92672">=</span> (<span style="color:#66d9ef">async</span> () <span style="color:#f92672">=&gt;</span> {
</span></span><span style="display:flex;"><span>      <span style="color:#66d9ef">try</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>        <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</span>;
</span></span><span style="display:flex;"><span>      } <span style="color:#66d9ef">finally</span> {
</span></span><span style="display:flex;"><span>        <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">delete</span>(<span style="color:#a6e22e">key</span>);
</span></span><span style="display:flex;"><span>      }
</span></span><span style="display:flex;"><span>    })();
</span></span><span style="display:flex;"><span>    <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">p</span>);
</span></span><span style="display:flex;"><span>  }
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">inflight</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>)<span style="color:#f92672">!</span>;
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><p>単一プロセスではこれで十分ですが、複数インスタンス構成では分散ロックも必要です。</p>
<h2 id="4-パターン2-分散ロックset-nx-ex">4. パターン2: 分散ロック（SET NX EX）</h2>
<p>複数 Pod で同時 miss が起きる場合、Redis ロックで再生成担当を 1 つに制限します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 5
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 6
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 7
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 8
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f"> 9
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">10
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">11
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">12
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">13
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">14
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lockKey</span> <span style="color:#f92672">=</span> <span style="color:#e6db74">`lock:</span><span style="color:#e6db74">${</span><span style="color:#a6e22e">key</span><span style="color:#e6db74">}</span><span style="color:#e6db74">`</span>;
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">const</span> <span style="color:#a6e22e">lock</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">lockKey</span>, <span style="color:#a6e22e">instanceId</span>, { <span style="color:#a6e22e">NX</span>: <span style="color:#66d9ef">true</span>, <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">10</span> });
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">if</span> (<span style="color:#a6e22e">lock</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#75715e">// ロック獲得: 再生成担当
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span>  <span style="color:#66d9ef">const</span> <span style="color:#a6e22e">value</span> <span style="color:#f92672">=</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">compute</span>();
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlSec</span> });
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#a6e22e">del</span>(<span style="color:#a6e22e">lockKey</span>);
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">value</span>;
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#75715e">// ロック未獲得: 少し待って再取得
</span></span></span><span style="display:flex;"><span><span style="color:#75715e"></span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">sleep</span>(<span style="color:#ae81ff">40</span>);
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">return</span> <span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">get</span>(<span style="color:#a6e22e">key</span>);
</span></span></code></pre></td></tr></table>
</div>
</div><p>注意点は、ロック TTL が短すぎると再生成中に失効し、二重計算になることです。処理時間の p99 を見て余裕を持って設定してください。</p>
<h2 id="5-パターン3-ttl-ジッターで期限切れを分散">5. パターン3: TTL ジッターで期限切れを分散</h2>
<p>同系統キーが同時に切れると負荷波形が尖ります。TTL にランダム幅を持たせるだけでかなり改善します。</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-ts" data-lang="ts"><span style="display:flex;"><span><span style="color:#66d9ef">function</span> <span style="color:#a6e22e">ttlWithJitter</span>(<span style="color:#a6e22e">baseSec</span>: <span style="color:#66d9ef">number</span>, <span style="color:#a6e22e">jitterSec</span>: <span style="color:#66d9ef">number</span>) {
</span></span><span style="display:flex;"><span>  <span style="color:#66d9ef">return</span> <span style="color:#a6e22e">baseSec</span> <span style="color:#f92672">+</span> Math.<span style="color:#a6e22e">floor</span>(Math.<span style="color:#a6e22e">random</span>() <span style="color:#f92672">*</span> <span style="color:#a6e22e">jitterSec</span>);
</span></span><span style="display:flex;"><span>}
</span></span><span style="display:flex;"><span>
</span></span><span style="display:flex;"><span><span style="color:#66d9ef">await</span> <span style="color:#a6e22e">redis</span>.<span style="color:#66d9ef">set</span>(<span style="color:#a6e22e">key</span>, <span style="color:#a6e22e">value</span>, { <span style="color:#a6e22e">EX</span>: <span style="color:#66d9ef">ttlWithJitter</span>(<span style="color:#ae81ff">300</span>, <span style="color:#ae81ff">90</span>) });
</span></span></code></pre></td></tr></table>
</div>
</div><p>300 秒固定より、300〜390 秒の分布にすると、同時失効が目に見えて減ります。</p>
<h2 id="6-パターン4-stale-while-revalidate">6. パターン4: stale-while-revalidate</h2>
<p>高可用性が重要な API では、期限切れ直後でも古い値を短時間返しつつ、裏で更新する手法が有効です。</p>
<p>データ構造例:</p>
<div class="highlight"><div style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;">
<table style="border-spacing:0;padding:0;margin:0;border:0;"><tr><td style="vertical-align:top;padding:0;margin:0;border:0;">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">1
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">2
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">3
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">4
</span><span style="white-space:pre;-webkit-user-select:none;user-select:none;margin-right:0.4em;padding:0 0.4em 0 0.4em;color:#7f7f7f">5
</span></code></pre></td>
<td style="vertical-align:top;padding:0;margin:0;border:0;;width:100%">
<pre tabindex="0" style="color:#f8f8f2;background-color:#272822;-moz-tab-size:4;-o-tab-size:4;tab-size:4;"><code class="language-json" data-lang="json"><span style="display:flex;"><span>{
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;value&#34;</span>: {<span style="color:#f92672">&#34;items&#34;</span>: [<span style="color:#960050;background-color:#1e0010">...</span>]},
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;hardExpireAt&#34;</span>: <span style="color:#ae81ff">1700000000</span>,
</span></span><span style="display:flex;"><span>  <span style="color:#f92672">&#34;softExpireAt&#34;</span>: <span style="color:#ae81ff">1699999700</span>
</span></span><span style="display:flex;"><span>}
</span></span></code></pre></td></tr></table>
</div>
</div><ul>
<li><code>now &lt; softExpireAt</code>: 新鮮データ</li>
<li><code>softExpireAt &lt;= now &lt; hardExpireAt</code>: 古い値を返しつつ非同期更新</li>
<li><code>hardExpireAt &lt;= now</code>: 同期再生成</li>
</ul>
<p>この方式はピーク帯の体感性能を維持しやすく、DB 保護にも強いです。</p>
<h2 id="7-失敗時フォールバックを設計する">7. 失敗時フォールバックを設計する</h2>
<p>キャッシュ再生成が失敗したときの動作を明確にしておくと、障害時の揺れが減ります。</p>
<p>推奨順序:</p>
<ol>
<li>stale データがあれば返す</li>
<li>stale もなければ軽量な代替レスポンス</li>
<li>最後に明示エラー（再試行可能）</li>
</ol>
<p>「毎回 DB にフォールバック」は危険です。障害時に DB をさらに追い込むため、回路遮断（circuit breaker）とセットで設計すべきです。</p>
<h2 id="8-監視で見るべき指標">8. 監視で見るべき指標</h2>
<p>対策の効果はメトリクスで評価します。</p>
<ul>
<li>cache hit ratio</li>
<li>key 単位の miss burst（短時間 miss 件数）</li>
<li>再生成処理の同時実行数</li>
<li>DB クエリ QPS と接続待ち時間</li>
</ul>
<p>Prometheus を使う場合、以下のようなカウンタを実装すると分析しやすいです。</p>
<ul>
<li><code>cache_requests_total{key_group, result=&quot;hit|miss|stale&quot;}</code></li>
<li><code>cache_rebuild_total{key_group, result=&quot;ok|error&quot;}</code></li>
<li><code>cache_lock_contention_total{key_group}</code></li>
</ul>
<h2 id="9-導入手順既存システム向け">9. 導入手順（既存システム向け）</h2>
<h3 id="step-1-熱いキーを特定">Step 1: 熱いキーを特定</h3>
<p>アクセス上位 20 キーを抽出し、まずそこだけ対策します。</p>
<h3 id="step-2-singleflight--ジッター導入">Step 2: singleflight + ジッター導入</h3>
<p>実装コストが低く、効果が高い組み合わせです。</p>
<h3 id="step-3-分散ロック導入">Step 3: 分散ロック導入</h3>
<p>複数インスタンス環境で必須。ロック競合率も計測する。</p>
<h3 id="step-4-stale-while-revalidate-追加">Step 4: stale-while-revalidate 追加</h3>
<p>高トラフィック API へ段階適用。UX と DB 安定性が両立しやすい。</p>
<h2 id="10-よくある失敗例">10. よくある失敗例</h2>
<h3 id="失敗1-ttl-を長くしてごまかす">失敗1: TTL を長くしてごまかす</h3>
<p>データ鮮度要件を満たせず、別の問題が出ます。期限延長は応急処置に留める。</p>
<h3 id="失敗2-分散ロックだけで安心する">失敗2: 分散ロックだけで安心する</h3>
<p>ロックが取れない側の待機戦略がないと、スパイクは残ります。待機・再取得・stale 応答まで設計が必要です。</p>
<h3 id="失敗3-miss-率しか見ない">失敗3: miss 率しか見ない</h3>
<p>miss が少なくても、特定キーへの burst が強ければ障害は起きます。キーグループ単位で観測してください。</p>
<h2 id="11-実運用チェックリスト">11. 実運用チェックリスト</h2>
<ul>
<li><input disabled="" type="checkbox"> 熱いキーを定義し、キーグループで計測している</li>
<li><input disabled="" type="checkbox"> singleflight もしくは同等の同時実行制御がある</li>
<li><input disabled="" type="checkbox"> TTL にジッターを導入している</li>
<li><input disabled="" type="checkbox"> 分散ロックの TTL が処理時間 p99 を上回る</li>
<li><input disabled="" type="checkbox"> stale-while-revalidate の返却条件が明確</li>
<li><input disabled="" type="checkbox"> 障害時フォールバック（回路遮断含む）がある</li>
</ul>
<h2 id="まとめ">まとめ</h2>
<p>Redis キャッシュスタンピード対策は、キャッシュを置くことではなく「miss 後の世界を設計する」ことです。</p>
<ul>
<li>同時再生成を止める</li>
<li>失効タイミングを分散する</li>
<li>期限切れ直後の応答を制御する</li>
</ul>
<p>この3点を実装すれば、ピーク時の DB 崩壊リスクは大幅に下げられます。まずはアクセス上位キーから段階導入し、メトリクスで効果を確認してください。仕組みとして回り始めると、トラフィック増加に対する耐性が目に見えて改善します。</p>
<h2 id="12-キャッシュキー設計の実務ポイント">12. キャッシュキー設計の実務ポイント</h2>
<p>スタンピード対策では、アルゴリズム以前にキー設計が重要です。<code>user:123:timeline</code> のような粒度が粗すぎるキーは、人気ユーザーにアクセスが集中したとき一気にホットスポットになります。可能なら <code>page</code> や <code>segment</code> を分割し、巨大レスポンスを小さく分けると miss 時の再生成コストを抑えられます。</p>
<p>また、キーの命名規約をチームで統一しておくと観測しやすくなります。<code>service:domain:resource:variant</code> 形式で揃えれば、メトリクス集計時に <code>key_group</code> を自動分類しやすく、どの領域で burst が起きているかを短時間で判断できます。運用性を上げるキー設計は、性能改善と同じくらい価値があります。</p>
]]></content:encoded>
      <category>Tech</category>
      <category>Redis</category>
      <category>Caching</category>
      <category>Backend</category>
      <category>Performance</category>
    </item>
  </channel>
</rss>
