<?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>Cache on Tarragon</title><link>https://tarrragon.github.io/blog/tags/cache/</link><description>Recent content in Cache on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Mon, 22 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/cache/index.xml" rel="self" type="application/rss+xml"/><item><title>2.C1 Meta：Cache Consistency 升級</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/</guid><description>&lt;p>這個案例的核心責任是說明快取轉換不只在容量與速度，還包括一致性治理能力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta 指出快取在 promotion、shard move、故障恢復時容易引入不一致，單靠傳統 invalidation 很難在大規模系統維持穩定。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當快取已是核心路徑，資料新鮮度問題會直接變成服務正確性問題。這時候轉換重點是把一致性追蹤與異常定位制度化，改一個 TTL 解決不了結構問題。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>先定義 inconsistency 來源點與觀測點。&lt;/li>
&lt;li>將 mutation tracing 納入治理，而不是只看命中率。&lt;/li>
&lt;li>把一致性指標接到告警與回退條件。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>先回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction&lt;/a>，再接 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/">Cache made consistent&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取轉換不只在容量與速度，還包括一致性治理能力。</p>
<h2 id="觀察">觀察</h2>
<p>Meta 指出快取在 promotion、shard move、故障恢復時容易引入不一致，單靠傳統 invalidation 很難在大規模系統維持穩定。</p>
<h2 id="判讀">判讀</h2>
<p>當快取已是核心路徑，資料新鮮度問題會直接變成服務正確性問題。這時候轉換重點是把一致性追蹤與異常定位制度化，改一個 TTL 解決不了結構問題。</p>
<h2 id="策略">策略</h2>
<ol>
<li>先定義 inconsistency 來源點與觀測點。</li>
<li>將 mutation tracing 納入治理，而不是只看命中率。</li>
<li>把一致性指標接到告警與回退條件。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>先回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction</a>，再接 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2022/06/08/core-infra/cache-made-consistent/">Cache made consistent</a></li>
</ul>
]]></content:encoded></item><item><title>Redis</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/</guid><description>&lt;p>Redis 是 in-memory data structure store、承擔三個責任：cache serving layer（with eviction）、data structure operation（string / hash / list / sorted set / stream / hyperloglog / geo）、輕量持久化（AOF / RDB）。設計取捨偏向「記憶體優先 + data type rich + 可選持久化」、cache 是主用場、但 data type 讓它跨入 session store / counter / leaderboard / lock 等場景。2024 起授權變動為 RSALv2 / SSPL（OSI 不認）、引發 Valkey fork。&lt;/p>
&lt;p>對「通用快取、session store、rate limit counter、leaderboard、distributed lock」這條路徑、Redis 是事實標準。本頁先給最短路徑、再展開日常 CLI / API 與 key 設計、最後進階治理（cluster / persistence / modules）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 docker 跑起 Redis、用 redis-cli 驗證&lt;/li>
&lt;li>用 SET / GET / EXPIRE / DEL / KEYS 操作、區分 6 大 data types 適用場景&lt;/li>
&lt;li>設計 key naming + TTL + eviction policy 對齊 cache miss 行為&lt;/li>
&lt;li>看懂 hit rate / memory pressure / eviction / replication lag 訊號&lt;/li>
&lt;li>評估 Cluster vs Sentinel、AOF/RDB、modules、授權變動下的選擇&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-redis-跑起來">最短路徑：5 分鐘把 Redis 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 Redis&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker run -d --name redis -p 6379:6379 redis:7&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 連線&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: docker exec -it redis redis-cli&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 驗證 SET / GET / EXPIRE&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">&lt;span class="c1"># TODO: SET foo bar / GET foo / EXPIRE foo 60 / TTL foo&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Redis 起來、能讀寫 + TTL」。實際應用見&lt;a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>redis-cli 指令對照表（SET / GET / DEL / EXPIRE / TTL / KEYS / SCAN / MGET / MSET）&lt;/li>
&lt;li>Client library 配置：connection pool / timeout / pipeline / cluster mode&lt;/li>
&lt;li>Pub/Sub vs Streams 的選用判讀&lt;/li>
&lt;li>對應指令範例：&lt;code>INFO replication&lt;/code>、&lt;code>CLIENT LIST&lt;/code>、&lt;code>SLOWLOG GET&lt;/code>&lt;/li>
&lt;/ul>
&lt;h3 id="key-design-與-data-types">Key design 與 data types&lt;/h3>
&lt;p>不同 data type 對應不同&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">資料形狀&lt;/a>。子議題：&lt;/p></description><content:encoded><![CDATA[<p>Redis 是 in-memory data structure store、承擔三個責任：cache serving layer（with eviction）、data structure operation（string / hash / list / sorted set / stream / hyperloglog / geo）、輕量持久化（AOF / RDB）。設計取捨偏向「記憶體優先 + data type rich + 可選持久化」、cache 是主用場、但 data type 讓它跨入 session store / counter / leaderboard / lock 等場景。2024 起授權變動為 RSALv2 / SSPL（OSI 不認）、引發 Valkey fork。</p>
<p>對「通用快取、session store、rate limit counter、leaderboard、distributed lock」這條路徑、Redis 是事實標準。本頁先給最短路徑、再展開日常 CLI / API 與 key 設計、最後進階治理（cluster / persistence / modules）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 docker 跑起 Redis、用 redis-cli 驗證</li>
<li>用 SET / GET / EXPIRE / DEL / KEYS 操作、區分 6 大 data types 適用場景</li>
<li>設計 key naming + TTL + eviction policy 對齊 cache miss 行為</li>
<li>看懂 hit rate / memory pressure / eviction / replication lag 訊號</li>
<li>評估 Cluster vs Sentinel、AOF/RDB、modules、授權變動下的選擇</li>
</ol>
<h2 id="最短路徑5-分鐘把-redis-跑起來">最短路徑：5 分鐘把 Redis 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 啟動 Redis</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># TODO: docker run -d --name redis -p 6379:6379 redis:7</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2. 連線</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># TODO: docker exec -it redis redis-cli</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 3. 驗證 SET / GET / EXPIRE</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1"># TODO: SET foo bar / GET foo / EXPIRE foo 60 / TTL foo</span></span></span></code></pre></div><p>最短路徑驗證「Redis 起來、能讀寫 + TTL」。實際應用見<a href="#%e6%97%a5%e5%b8%b8%e6%93%8d%e4%bd%9c%e8%88%87%e6%b1%ba%e7%ad%96%e5%bd%a2%e7%8b%80">日常操作</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>redis-cli 指令對照表（SET / GET / DEL / EXPIRE / TTL / KEYS / SCAN / MGET / MSET）</li>
<li>Client library 配置：connection pool / timeout / pipeline / cluster mode</li>
<li>Pub/Sub vs Streams 的選用判讀</li>
<li>對應指令範例：<code>INFO replication</code>、<code>CLIENT LIST</code>、<code>SLOWLOG GET</code></li>
</ul>
<h3 id="key-design-與-data-types">Key design 與 data types</h3>
<p>不同 data type 對應不同<a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">資料形狀</a>。子議題：</p>
<ul>
<li>String：cache / counter / config flag</li>
<li>Hash：object cache（避免反覆 serialize）</li>
<li>List：queue / activity feed（小規模）</li>
<li>Set：membership / tag</li>
<li>Sorted set：leaderboard / time-series sliding window</li>
<li>Stream：log-style queue / event stream</li>
<li>HyperLogLog / Geo：approximate count / 地理座標</li>
</ul>
<p>Key naming 規範：<code>&lt;service&gt;:&lt;entity&gt;:&lt;id&gt;:&lt;field&gt;</code>、用 <code>:</code> 分層、避免大 key（單 key &gt; 10KB / list 長度 &gt; 10K）。</p>
<h3 id="ttl-與-eviction-策略">TTL 與 eviction 策略</h3>
<p><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 跟 eviction</a> 是 cache 行為的核心旋鈕。子議題：</p>
<ul>
<li>顯式 EXPIRE vs SET EX 設 TTL</li>
<li>maxmemory + maxmemory-policy（allkeys-lru / allkeys-lfu / volatile-lru / volatile-ttl / noeviction）</li>
<li>TTL 設計：固定 TTL vs 動態 TTL vs 不設 TTL</li>
<li>對應指令：<code>CONFIG SET maxmemory 2gb</code>、<code>CONFIG SET maxmemory-policy allkeys-lfu</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="cluster-vs-sentinel">Cluster vs Sentinel</h3>
<p>子議題：</p>
<ul>
<li>Sentinel：HA 模式、無 sharding、適合單 master 容量足夠</li>
<li>Cluster：sharding 模式、16384 hash slot、橫向擴展容量</li>
<li>Hash tag <code>{...}</code> 強制 multi-key 同 shard</li>
<li>Cluster failover 對 PEL（Streams）跟 distributed lock 的影響</li>
</ul>
<h3 id="aof--rdb-持久化策略">AOF / RDB 持久化策略</h3>
<p>子議題：</p>
<ul>
<li>AOF（append-only file）：fsync 策略（always / everysec / no）、rewrite</li>
<li>RDB（snapshot）：save 策略、backup 還原</li>
<li>混合模式：AOF + RDB</li>
<li>持久化在 cache 場景的取捨（持久化是回填還是 source-of-truth）</li>
</ul>
<h3 id="eviction-policy-詳細">Eviction policy 詳細</h3>
<p>子議題：</p>
<ul>
<li>LRU vs LFU：access pattern 對選擇的影響</li>
<li>volatile-* vs allkeys-*：只淘汰有 TTL 的 vs 全 key</li>
<li>approximate LRU 的 sampling 影響</li>
<li>對應 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
</ul>
<h3 id="distributed-lock">Distributed lock</h3>
<p>子議題：</p>
<ul>
<li>SETNX + EXPIRE 模式</li>
<li>Redlock 算法（多 master quorum）+ 取捨爭議</li>
<li>Redlock 何時不夠：fence token / lease renewal</li>
<li>對應 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a></li>
</ul>
<h3 id="pubsub-vs-streams">Pub/Sub vs Streams</h3>
<p>子議題：</p>
<ul>
<li>Pub/Sub：fire-and-forget、訂閱者離線會錯過</li>
<li>Streams：append-only log、consumer group + PEL</li>
<li>何時用 Streams 取代 Pub/Sub</li>
<li>Redis Streams 細節見 <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">03 messaging 模組 Redis Streams vendor</a></li>
</ul>
<h3 id="redis-modules">Redis Modules</h3>
<p>子議題：</p>
<ul>
<li>RedisJSON / RedisSearch / RedisTimeSeries / RedisBloom / RedisGraph</li>
<li>Module 隨授權變動受影響、Valkey 部分 fork</li>
<li>Module 在 ElastiCache 的支援限制</li>
</ul>
<h3 id="授權變動與選型影響">授權變動與選型影響</h3>
<p>子議題：</p>
<ul>
<li>2024 RSALv2 / SSPL 變動的影響範圍</li>
<li>對 managed service（ElastiCache 改 default 為 Valkey）的衝擊</li>
<li>從 Redis 遷 Valkey 的相容性路徑</li>
<li>商業 vs OSS 邊界</li>
</ul>
<h3 id="hot-key-處理">Hot key 處理</h3>
<p>子議題：</p>
<ul>
<li>Hot key 偵測（redis-cli &ndash;hotkeys、<code>MONITOR</code> 慎用）</li>
<li>Hot key 解法：local cache + Redis 兩層、key 拆分（讀多寫少場景）</li>
<li>對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="hit-rate-下降">Hit rate 下降</h3>
<p>操作原則：先看 cache pattern 是否變（新功能 / TTL 變短）、再看 origin 壓力是否擴大。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: INFO stats（看 keyspace_hits / keyspace_misses 比例）</span></span></span></code></pre></div><p>判讀路徑：TTL 太短 → eviction 太積極 → key 命名變動造成 cache miss → origin 失敗 retry storm。</p>
<h3 id="memory-pressure--eviction-異常">Memory pressure / eviction 異常</h3>
<p>操作原則：先看 maxmemory + maxmemory-policy 設定、再看 key size 分布。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># TODO: INFO memory / MEMORY USAGE &lt;key&gt; / --bigkeys</span></span></span></code></pre></div><h3 id="hot-key">Hot key</h3>
<p>對應案例 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify Write-Through</a>。判讀路徑：某 key 的 QPS 遠高於其他、單 shard CPU 接近 100%、其他 shard 閒置。</p>
<h3 id="replication-lag">Replication lag</h3>
<p>操作原則：replica 跟 master 差距、看 <code>INFO replication</code> 的 master_repl_offset vs slave_repl_offset。對 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency</a> 的對照。</p>
<h3 id="cache-stampede雷霆崩潰">Cache stampede（雷霆崩潰）</h3>
<p>對應反例 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede Rollout</a>。判讀路徑：TTL 同時過期 → 大量 cache miss → origin 被打爆 → 連鎖失敗。修法：jitter TTL、early refresh、singleflight 模式。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 OSI 認可開源授權</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>純 cache、不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>極高 throughput / 多核</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS 生態 managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></td>
      </tr>
      <tr>
          <td>Durable Redis-compatible</td>
          <td>AWS MemoryDB（介於 cache 與 DB）</td>
      </tr>
      <tr>
          <td>大規模 event stream</td>
          <td><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka</a> / <a href="/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams</a></td>
      </tr>
      <tr>
          <td>Process-local cache</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> / Guava Cache（JVM 內、無網路）</td>
      </tr>
      <tr>
          <td>Search / full-text</td>
          <td>Elasticsearch / OpenSearch（不在本模組）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Redis client 完整 API</li>
<li>Redis command 百科（詳查 redis.io/commands）</li>
<li>Redis Stack 商業 modules 細節</li>
<li>AOF / RDB 內部 binary format</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Redis 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Shopify Redis 上做 Marshal → MessagePack 雙軌遷移、payload 編碼演進</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Shopify 在 read-heavy 路徑用 Redis 做 write-through、對應 hot key / 命中率治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>invalidation / shard move 一致性議題、Redis Cluster 與 replica 場景共用判讀框架</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Redis 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>Redis TTL 切換 / key rename 都會觸發 stampede、需 jitter / singleflight / early refresh</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single instance + AOF / 中型 Sentinel + replica / 大型 Cluster + hash tag</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>Memcached 路由層案例、Redis 對應為 Cluster + proxy（Envoy / Twemproxy）或 client-side routing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>分層 cache（DRAM + flash）對照、Redis on flash（RoF / Speedb）的成本決策參考</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 基於 Memcached + 跨 AZ replication、Redis 對應為 active-active CRDB / Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>Graph cache 演進案例、Redis 對應為 RedisGraph（已 deprecated）或自建 graph 索引</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a></td>
          <td>Edge tiered（HTTP cache）對照、Redis 對應為 hot tier + S3 cold tier 自建分層</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>、<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a>、<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ul>
]]></content:encoded></item><item><title>2.1 高併發下的 Redis 讀寫邊界</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/</guid><description>&lt;p>Redis 在後端服務裡常扮演 cache、session、counter、dedup、presence 或輕量協調層。它通常比 SQL 更適合高併發短操作，但前提是 client、連線池、pipeline 與 key 設計都受控。高併發下的 Redis 仍然會遇到 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a>、快取穿透、stampede、過大 pipeline 與不當鎖設計。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>學完本章後，你將能夠：&lt;/p>
&lt;ol>
&lt;li>理解為什麼 Redis client 應該共用&lt;/li>
&lt;li>分辨單鍵操作、pipeline、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction&lt;/a> 與 Lua 的邊界&lt;/li>
&lt;li>了解高併發下的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede&lt;/a> 與 hot key 問題&lt;/li>
&lt;li>用 &lt;code>context&lt;/code> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout&lt;/a> 保護 Redis 呼叫&lt;/li>
&lt;li>把 Redis 用在適合的資料角色，並保留正式狀態來源&lt;/li>
&lt;/ol>
&lt;hr>
&lt;h2 id="觀察redis-呼叫大多是短網路-io">【觀察】Redis 呼叫大多是短網路 I/O&lt;/h2>
&lt;p>應用端對 Redis 的操作通常是短小但頻繁的網路請求。這代表真正影響效能的往往是 RTT、連線重用、批次送出與 key 設計。&lt;/p>
&lt;p>所以高併發時，重點是控制 Redis 邊界：&lt;/p>
&lt;ul>
&lt;li>用同一個 client 共用連線池&lt;/li>
&lt;li>對獨立操作使用合理的 pipeline&lt;/li>
&lt;li>熱門資料要避免集中到單一 key&lt;/li>
&lt;/ul>
&lt;h2 id="判讀client-共用比每次建立更重要">【判讀】client 共用比每次建立更重要&lt;/h2>
&lt;p>Redis client 的核心設計通常就是讓應用共用同一個實例。每個 request 都 new client，會把連線管理成本、握手成本與資源回收問題全部放大。&lt;/p>
&lt;p>高併發服務通常會採用：&lt;/p>
&lt;ul>
&lt;li>process 啟動時建立一個 Redis client&lt;/li>
&lt;li>request handler、worker、service layer 共用它&lt;/li>
&lt;li>所有操作都帶 &lt;code>context&lt;/code>&lt;/li>
&lt;li>timeout 與取消由上層傳入&lt;/li>
&lt;/ul>
&lt;h2 id="策略pipeline-用來節省-rtt">【策略】pipeline 用來節省 RTT&lt;/h2>
&lt;p>pipeline 的價值是把多個獨立命令一次送出，減少往返次數。它很適合：&lt;/p>
&lt;ul>
&lt;li>多個彼此獨立的讀取&lt;/li>
&lt;li>批次寫入&lt;/li>
&lt;li>一次更新多個 cache key&lt;/li>
&lt;/ul>
&lt;p>pipeline 的核心限制是批次大小仍要受控。太大的 pipeline 會帶來：&lt;/p>
&lt;ul>
&lt;li>內存壓力&lt;/li>
&lt;li>回應延遲變大&lt;/li>
&lt;li>單次失敗影響更多操作&lt;/li>
&lt;/ul>
&lt;h2 id="判讀原子性需求要分清楚">【判讀】原子性需求要分清楚&lt;/h2>
&lt;p>Redis 的很多操作本身就可以很快，但原子性與一致性需要額外設計。當需求需要多個資料變更形成同一個結果時，才應該考慮：&lt;/p>
&lt;ul>
&lt;li>單鍵原子操作&lt;/li>
&lt;li>transaction&lt;/li>
&lt;li>Lua script&lt;/li>
&lt;li>由上層做去重或補償&lt;/li>
&lt;/ul>
&lt;p>transaction 應服務明確的一致性需求，cache 寫入也應維持輔助狀態定位。Redis 很常是輔助狀態，真正的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 通常還是在 SQL 或 domain store。&lt;/p>
&lt;h2 id="策略cache-stampede-與-hot-key-要先處理">【策略】cache stampede 與 hot key 要先處理&lt;/h2>
&lt;p>高併發快取最常見的兩個問題，是大量 goroutine 同時 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a> 同一筆資料，以及大量流量打到同一個 key。&lt;/p>
&lt;h3 id="cache-stampede">cache stampede&lt;/h3>
&lt;p>當 cache miss 發生時，如果每個 request 都直接回源查 DB，會把後端放大成更大的壓力。常見的處理方式包括：&lt;/p>
&lt;ul>
&lt;li>設定合理 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a>&lt;/li>
&lt;li>加 single-flight 類型的去重&lt;/li>
&lt;li>讓部分請求等待同一批重建結果&lt;/li>
&lt;li>對重建失敗設退避或短暫保護&lt;/li>
&lt;/ul>
&lt;h3 id="hot-key">hot key&lt;/h3>
&lt;p>如果某些 key 過度熱門，壓力會集中到 Redis 甚至單一 shard。處理方式通常是：&lt;/p>
&lt;ul>
&lt;li>拆 key 或拆資料粒度&lt;/li>
&lt;li>讓讀取走多層 cache&lt;/li>
&lt;li>降低單點依賴&lt;/li>
&lt;li>在應用端做短暫本地快取或節流&lt;/li>
&lt;/ul>
&lt;h2 id="cache-在規模化服務的角色光譜主寫於-_index">Cache 在規模化服務的角色光譜（主寫於 _index）&lt;/h2>
&lt;p>Cache 在規模化服務的角色從「DB 補救」逐步轉變到「主要服務面」再到「資料平面」、是橫跨整個 02 模組的入門 frame。完整光譜跟判讀條件主寫於 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組入口的「規模化下 cache 的角色光譜」段&lt;/a>；本章從 &lt;em>高併發讀寫&lt;/em> 角度補充：當 cache 已落在「主要服務面」或「資料平面」角色、cache lookup 是 critical path、容量規劃跟 stampede 防護要按本章「Cache 容量規劃跟 DB 不一樣」段執行。&lt;/p></description><content:encoded><![CDATA[<p>Redis 在後端服務裡常扮演 cache、session、counter、dedup、presence 或輕量協調層。它通常比 SQL 更適合高併發短操作，但前提是 client、連線池、pipeline 與 key 設計都受控。高併發下的 Redis 仍然會遇到 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、快取穿透、stampede、過大 pipeline 與不當鎖設計。</p>
<h2 id="本章目標">本章目標</h2>
<p>學完本章後，你將能夠：</p>
<ol>
<li>理解為什麼 Redis client 應該共用</li>
<li>分辨單鍵操作、pipeline、<a href="/blog/backend/knowledge-cards/transaction/" data-link-title="Transaction" data-link-desc="說明 transaction 如何讓一組資料變更一起成功或一起回復">transaction</a> 與 Lua 的邊界</li>
<li>了解高併發下的 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a> 與 hot key 問題</li>
<li>用 <code>context</code> 與 <a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 保護 Redis 呼叫</li>
<li>把 Redis 用在適合的資料角色，並保留正式狀態來源</li>
</ol>
<hr>
<h2 id="觀察redis-呼叫大多是短網路-io">【觀察】Redis 呼叫大多是短網路 I/O</h2>
<p>應用端對 Redis 的操作通常是短小但頻繁的網路請求。這代表真正影響效能的往往是 RTT、連線重用、批次送出與 key 設計。</p>
<p>所以高併發時，重點是控制 Redis 邊界：</p>
<ul>
<li>用同一個 client 共用連線池</li>
<li>對獨立操作使用合理的 pipeline</li>
<li>熱門資料要避免集中到單一 key</li>
</ul>
<h2 id="判讀client-共用比每次建立更重要">【判讀】client 共用比每次建立更重要</h2>
<p>Redis client 的核心設計通常就是讓應用共用同一個實例。每個 request 都 new client，會把連線管理成本、握手成本與資源回收問題全部放大。</p>
<p>高併發服務通常會採用：</p>
<ul>
<li>process 啟動時建立一個 Redis client</li>
<li>request handler、worker、service layer 共用它</li>
<li>所有操作都帶 <code>context</code></li>
<li>timeout 與取消由上層傳入</li>
</ul>
<h2 id="策略pipeline-用來節省-rtt">【策略】pipeline 用來節省 RTT</h2>
<p>pipeline 的價值是把多個獨立命令一次送出，減少往返次數。它很適合：</p>
<ul>
<li>多個彼此獨立的讀取</li>
<li>批次寫入</li>
<li>一次更新多個 cache key</li>
</ul>
<p>pipeline 的核心限制是批次大小仍要受控。太大的 pipeline 會帶來：</p>
<ul>
<li>內存壓力</li>
<li>回應延遲變大</li>
<li>單次失敗影響更多操作</li>
</ul>
<h2 id="判讀原子性需求要分清楚">【判讀】原子性需求要分清楚</h2>
<p>Redis 的很多操作本身就可以很快，但原子性與一致性需要額外設計。當需求需要多個資料變更形成同一個結果時，才應該考慮：</p>
<ul>
<li>單鍵原子操作</li>
<li>transaction</li>
<li>Lua script</li>
<li>由上層做去重或補償</li>
</ul>
<p>transaction 應服務明確的一致性需求，cache 寫入也應維持輔助狀態定位。Redis 很常是輔助狀態，真正的 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 通常還是在 SQL 或 domain store。</p>
<h2 id="策略cache-stampede-與-hot-key-要先處理">【策略】cache stampede 與 hot key 要先處理</h2>
<p>高併發快取最常見的兩個問題，是大量 goroutine 同時 <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a> 同一筆資料，以及大量流量打到同一個 key。</p>
<h3 id="cache-stampede">cache stampede</h3>
<p>當 cache miss 發生時，如果每個 request 都直接回源查 DB，會把後端放大成更大的壓力。常見的處理方式包括：</p>
<ul>
<li>設定合理 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a></li>
<li>加 single-flight 類型的去重</li>
<li>讓部分請求等待同一批重建結果</li>
<li>對重建失敗設退避或短暫保護</li>
</ul>
<h3 id="hot-key">hot key</h3>
<p>如果某些 key 過度熱門，壓力會集中到 Redis 甚至單一 shard。處理方式通常是：</p>
<ul>
<li>拆 key 或拆資料粒度</li>
<li>讓讀取走多層 cache</li>
<li>降低單點依賴</li>
<li>在應用端做短暫本地快取或節流</li>
</ul>
<h2 id="cache-在規模化服務的角色光譜主寫於-_index">Cache 在規模化服務的角色光譜（主寫於 _index）</h2>
<p>Cache 在規模化服務的角色從「DB 補救」逐步轉變到「主要服務面」再到「資料平面」、是橫跨整個 02 模組的入門 frame。完整光譜跟判讀條件主寫於 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">模組入口的「規模化下 cache 的角色光譜」段</a>；本章從 <em>高併發讀寫</em> 角度補充：當 cache 已落在「主要服務面」或「資料平面」角色、cache lookup 是 critical path、容量規劃跟 stampede 防護要按本章「Cache 容量規劃跟 DB 不一樣」段執行。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> — 4700 萬 MAU 配對引擎、每次滑動查多個 cache（用戶 profile、距離、偏好過濾、推薦池）、cache lookup 屬 critical path。詳細 cache vs persistent store 取捨見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>。</p>
<h2 id="cache-容量規劃跟-db-不一樣">Cache 容量規劃跟 DB 不一樣</h2>
<p>容量規劃基準在 cache 跟 DB 有本質差異：DB 容量受 <em>total dataset size</em> 影響（要存所有資料）；cache 容量受 <em>working set size</em> 影響（只存熱資料）。兩者的擴容邏輯、成本曲線、評估指標都不同、不能套用相同規劃模板。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> — 47M MAU sustained growth、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo</a> — 當熱資料超過 DRAM 經濟範圍、單層 cache 同時遇到成本跟命中率瓶頸、要分層（DRAM + flash、詳見 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 ttl-eviction 分層快取段</a>）。</p>
<p><strong>Cache 容量規劃的三個維度</strong>：</p>
<ul>
<li><strong>Working set size</strong>：熱資料大小決定 cache 需要多少 RAM。監控指標是 <em>hot key 分布</em> 跟 <em>resident set growth</em>。working set 估算方式因 workload 不同、要靠實測得出。</li>
<li><strong>命中率目標</strong>：命中率目標決定 cache 大小的成長曲線。90% / 95% / 99% 對應不同 cache 大小、每加一個 9 需要的 cache size 通常顯著增加（具體倍數依 access pattern 分布、Zipfian 分布越平倍數越高）。</li>
<li><strong>回源 budget</strong>：cache miss 後 origin（DB / 重算）能承受多少 QPS、決定 cache 命中率下限。命中率掉幾個 percentage point 可能讓 origin QPS 翻數倍、容量規劃要按命中率敏感度反推 origin headroom。</li>
</ul>
<p><strong>判讀重點</strong>：cache 命中率變化是 <em>業務變化訊號</em>、可能是新功能影響 access pattern（推薦演算法改、查詢條件擴大、tenant 結構變化）、應先看業務側、再考慮加 cache capacity。</p>
<h2 id="redis-規模化的單執行緒邊界">Redis 規模化的單執行緒邊界</h2>
<p>Redis command 執行至今仍 single-threaded、單實例 command 吞吐受 CPU 單核限制。6.0+ 起可開啟 I/O thread 提升 I/O 吞吐、但 command 執行仍序列化。規模化服務遇到這個邊界時、四個選項各自適合不同壓力：</p>
<p><strong>1. 拆 cluster（應用層分散 key）</strong>：Redis Cluster 自帶分片、適合 key 數量多、單 key 不熱的場景。每 shard 仍 single-threaded、但總吞吐線性擴展。典型壓力是「KV 種類多、每種 key 不算熱、整體流量大」、跟 Tinder 47M MAU 同類 — 用戶 profile 跨大量 key 分散、每個 key 流量不極端、cluster 切片足夠。</p>
<p><strong>2. Redis 6.0+ I/O thread</strong>：保留 Redis protocol、I/O 處理 multi-threaded、command 執行仍 single-threaded。提升 read-heavy 場景吞吐、實測倍數依 workload 跟 thread 數而定。適合「主要瓶頸在 I/O syscall 不在 command CPU」的場景、是低改動量的階段性升級、不換 broker。</p>
<p><strong>3. <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> / <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">Dragonfly</a>（multi-threaded fork）</strong>：command 執行也 multi-threaded。對應 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB</a> — Snap 採用 KeyDB 在 GCP 上替代原生 Redis、9.C35 判讀段提出「單實例 throughput 提升 5-10x」（屬案例 derived 推論、實測倍數依 workload）。適合「單 key 極熱、cluster 切不開、需要單實例多執行緒撐單 partition」的壓力。代價是 vendor lock-in、fork 治理走向不確定（KeyDB 公司被收購後策略未明）。</p>
<p><strong>4. Memcached（multi-threaded、功能少）</strong>：純 KV 不支援複雜資料結構（hash / sorted set / stream）、適合「資料形狀單純、要 multi-threaded」的 cache-only 場景。如果 application 不需要 Redis 的進階資料結構、Memcached 通常單實例吞吐更高、運維更簡單。</p>
<p><strong>規模化常用組合</strong>：ElastiCache for Redis 7.1 在 r7g.4xlarge 上的 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS 公布上限</a>（單節點百萬級 RPS、單 cluster 5 億 RPS）+ Cluster 模式 + 應用層 connection multiplexing。實際配置依工作量跟成本邊界決定、不是「規模化必然全配滿」。對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> 的設計方向。</p>
<p>判讀順序：先確認瓶頸是不是單實例 command 吞吐（CPU 單核滿載 vs 整體 RAM / network 是否還有 headroom）、再選方案。應用層 key 分布不均（hot key）跟 single-threaded 限制是兩個獨立議題、混在一起會誤選方案。</p>
<h2 id="執行把-redis-用在對的角色">【執行】把 Redis 用在對的角色</h2>
<p>Redis 在高併發場景常見角色有：</p>
<ul>
<li>cache</li>
<li>session store</li>
<li>counter / <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a></li>
<li>presence / online state</li>
<li>dedup / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> key</li>
<li>lightweight <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">queue</a> / stream</li>
</ul>
<p>每一種角色都有不同容錯方式。counter、presence 和 cache 的失敗語意各自不同，因此需要依資料角色選擇處理策略。</p>
<h2 id="策略分散式-lock-要謹慎使用">【策略】分散式 lock 要謹慎使用</h2>
<p>Redis 常被拿來做 distributed lock，但這類機制要非常清楚 lease、過期、持有者與失效風險。高併發下最怕的是鎖住之後沒有安全釋放，或以為鎖保證了完整業務一致性。</p>
<p>原則上：</p>
<ul>
<li>鎖應該短</li>
<li>鎖持有者要可辨識</li>
<li>鎖過期要可接受</li>
<li>業務上若能不用分散式鎖，通常應優先考慮更簡單的設計</li>
</ul>
<h2 id="延伸語言端仍然要負責限流與取消">【延伸】語言端仍然要負責限流與取消</h2>
<p>Redis 很快，但應用端仍然要設計邊界。語言端應使用 timeout、cancellation、<a href="/blog/backend/knowledge-cards/worker-pool/" data-link-title="Worker Pool" data-link-desc="說明一組 worker 如何限制同時處理量並保護下游資源">worker pool</a>、rate limit 或 <a href="/blog/backend/knowledge-cards/backpressure/" data-link-title="Backpressure" data-link-desc="說明下游處理速度不足時系統如何讓上游依下游能力送出工作">backpressure</a> 把壓力收斂起來；否則排隊等待 Redis 回應的工作會越堆越多。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>Redis 高併發邊界會受語言 runtime 影響。Thread-based runtime 要管理 client pool 與 blocking command；async runtime 要確認 Redis client 不會阻塞 event loop；輕量 task runtime 要限制同時呼叫 Redis 的工作數量。動態語言要特別控制 cache value schema 與序列化格式；強型別語言要避免把內部型別直接當成跨服務 cache <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a>。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>高併發 cache 場景重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a></td>
          <td>47M MAU 配對引擎、cache 是主要服務面、sustained growth 成本曲線</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a></td>
          <td>ML inference 之前 feature lookup、p99 &lt; 10ms 是業務 KPI</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB</a></td>
          <td>KeyDB multi-threaded fork、跨 cloud 部署</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>cache 成為資料層能力、社交圖查詢的快取治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>跨區分散式 cache、平台層基礎設施</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>client 散落邏輯收斂到路由層、跨叢集 cache 路由</td>
      </tr>
  </tbody>
</table>
<p>這六個案例可以分成兩群讀。<strong>規模化容量群</strong>（Tinder、Tubi、Snap）的共同訊號是「sustained growth 下 cache 變主要服務面、容量規劃跟單實例邊界要重新設計」、本章「Cache 容量規劃跟 DB 不一樣」跟「Redis 規模化的單執行緒邊界」段直接對應；<strong>跨區資料平面群</strong>（Meta TAO、Netflix EVCache、Meta mcrouter）的共同訊號是「cache 變成跨區資料層、需要路由治理跟一致性窗口」、詳細展開在 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的跨區一致性窗口</a> 跟 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape</a>。兩群讀法切入點不同、本章先處理前者的高併發 / 容量議題、後者跨章節讀。</p>
<h2 id="小結">小結</h2>
<p>高併發服務處理 Redis 的核心原則：client 共用、操作要短、pipeline 要有節制、熱點 key 要設計、cache miss 要防 stampede、鎖要保守使用。</p>
<p><strong>規模化補充</strong>：cache 角色變化（DB 補救 → 主要服務面 → 資料平面）主寫於 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">_index 規模化下 cache 的角色光譜</a>、本章在角色已落「主要服務面」或「資料平面」時提供高併發判讀。Redis 規模化的單執行緒邊界有四個選項（cluster / I/O thread / KeyDB 等 fork / Memcached）、判讀順序是先確認瓶頸再選方案。</p>
]]></content:encoded></item><item><title>2.C2 Meta：mcrouter 與跨區快取路由</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/</guid><description>&lt;p>這個案例的核心責任是說明快取規模變大後，路由層本身會成為選型主題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>mcrouter 被用來統一處理大量 memcached 流量與跨叢集路由，代表快取已從局部優化變成平台層能力。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當快取服務跨區、跨叢集且請求量極高時，應把路由策略、故障切換與運維一致性視為主議題。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把 client 端散落邏輯收斂到路由層。&lt;/li>
&lt;li>把跨區路由與故障策略標準化。&lt;/li>
&lt;li>用可觀測訊號監控路由品質與新鮮度。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發 Redis 邊界&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 service discovery&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2014/09/15/web/introducing-mcrouter-a-memcached-protocol-router-for-scaling-memcached-deployments/">Introducing mcrouter&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取規模變大後，路由層本身會成為選型主題。</p>
<h2 id="觀察">觀察</h2>
<p>mcrouter 被用來統一處理大量 memcached 流量與跨叢集路由，代表快取已從局部優化變成平台層能力。</p>
<h2 id="判讀">判讀</h2>
<p>當快取服務跨區、跨叢集且請求量極高時，應把路由策略、故障切換與運維一致性視為主議題。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把 client 端散落邏輯收斂到路由層。</li>
<li>把跨區路由與故障策略標準化。</li>
<li>用可觀測訊號監控路由品質與新鮮度。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發 Redis 邊界</a> 與 <a href="/blog/backend/05-deployment-platform/service-discovery/" data-link-title="5.4 service discovery" data-link-desc="整理 endpoint discovery 與 DNS">5.4 service discovery</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2014/09/15/web/introducing-mcrouter-a-memcached-protocol-router-for-scaling-memcached-deployments/">Introducing mcrouter</a></li>
</ul>
]]></content:encoded></item><item><title>Valkey</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/</guid><description>&lt;p>Valkey 是 2024 年從 Redis 7.2.4 fork 的開源專案、承擔三個責任：維持 Redis API 相容（drop-in 替換）、提供 OSI 認可的開源授權（BSD 3-clause）、由 Linux Foundation 託管避免單一公司控制。設計取捨偏向「相容 Redis 既有 client / 工具 + 開源治理透明 + 多雲廠商共同維護」、不追求功能超越 Redis Inc。&lt;/p>
&lt;p>對「既有 Redis 部署、需要 OSI 認可授權、多雲避免 vendor lock-in、合規敏感」這條路徑、Valkey 是 Redis 的替代首選。AWS / Google / Oracle / Ericsson 等共同支援、AWS ElastiCache 已把 Valkey 設為 default engine。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Valkey、用 redis-cli 驗證 API 相容性&lt;/li>
&lt;li>評估從 Redis 遷移到 Valkey 的相容性風險（module / Stack 功能）&lt;/li>
&lt;li>看懂 Valkey vs Redis Inc 的版本對應跟功能差距&lt;/li>
&lt;li>評估管雲端 managed Valkey（ElastiCache）的選用判斷&lt;/li>
&lt;li>區分 Valkey 跟 Redis 商業版本對你的合規 / 採購 / SLA 影響&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-valkey-跑起來">最短路徑：5 分鐘把 Valkey 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 Valkey（Redis API 相容、可直接用 redis-cli）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name valkey -p 6379:6379 valkey/valkey:8
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 驗證讀寫（valkey-cli 與 redis-cli 命令一致）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本：Valkey 同時回報相容的 redis_version 與自身 valkey_version&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version|server_name&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.2.4 ← client library 以此判斷相容性（fork 自 Redis 7.2.4）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># server_name:valkey&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自身版本&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步是相容性的關鍵證據：既有 Redis client library 看到 &lt;code>redis_version:7.2.4&lt;/code> 就以 Redis 7.2.4 的行為運作、無需改 code；&lt;code>valkey_version&lt;/code> 才是 Valkey 自身的演進線。實機驗證於 valkey/valkey:8 image、最後檢查日 2026-06-16。實際遷移路徑見&lt;a href="#%e5%be%9e-redis-%e9%81%b7%e7%a7%bb">進階主題：從 Redis 遷移&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>valkey-cli vs redis-cli：兩個 binary 都可連 Valkey、命令一致&lt;/li>
&lt;li>Client library 配置：所有 Redis client 自動相容（無需 Valkey-specific client）&lt;/li>
&lt;li>對應指令範例：&lt;code>INFO server&lt;/code> 顯示 valkey_version 而非 redis_version&lt;/li>
&lt;/ul>
&lt;h3 id="跟-redis-的相容邊界">跟 Redis 的相容邊界&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Valkey 是 2024 年從 Redis 7.2.4 fork 的開源專案、承擔三個責任：維持 Redis API 相容（drop-in 替換）、提供 OSI 認可的開源授權（BSD 3-clause）、由 Linux Foundation 託管避免單一公司控制。設計取捨偏向「相容 Redis 既有 client / 工具 + 開源治理透明 + 多雲廠商共同維護」、不追求功能超越 Redis Inc。</p>
<p>對「既有 Redis 部署、需要 OSI 認可授權、多雲避免 vendor lock-in、合規敏感」這條路徑、Valkey 是 Redis 的替代首選。AWS / Google / Oracle / Ericsson 等共同支援、AWS ElastiCache 已把 Valkey 設為 default engine。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Valkey、用 redis-cli 驗證 API 相容性</li>
<li>評估從 Redis 遷移到 Valkey 的相容性風險（module / Stack 功能）</li>
<li>看懂 Valkey vs Redis Inc 的版本對應跟功能差距</li>
<li>評估管雲端 managed Valkey（ElastiCache）的選用判斷</li>
<li>區分 Valkey 跟 Redis 商業版本對你的合規 / 採購 / SLA 影響</li>
</ol>
<h2 id="最短路徑5-分鐘把-valkey-跑起來">最短路徑：5 分鐘把 Valkey 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 Valkey（Redis API 相容、可直接用 redis-cli）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name valkey -p 6379:6379 valkey/valkey:8
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 2. 驗證讀寫（valkey-cli 與 redis-cli 命令一致）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli SET foo bar   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli GET foo       <span class="c1"># → bar</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 3. 確認版本：Valkey 同時回報相容的 redis_version 與自身 valkey_version</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version|server_name&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 以此判斷相容性（fork 自 Redis 7.2.4）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># server_name:valkey</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自身版本</span></span></span></code></pre></div><p>第三步是相容性的關鍵證據：既有 Redis client library 看到 <code>redis_version:7.2.4</code> 就以 Redis 7.2.4 的行為運作、無需改 code；<code>valkey_version</code> 才是 Valkey 自身的演進線。實機驗證於 valkey/valkey:8 image、最後檢查日 2026-06-16。實際遷移路徑見<a href="#%e5%be%9e-redis-%e9%81%b7%e7%a7%bb">進階主題：從 Redis 遷移</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>valkey-cli vs redis-cli：兩個 binary 都可連 Valkey、命令一致</li>
<li>Client library 配置：所有 Redis client 自動相容（無需 Valkey-specific client）</li>
<li>對應指令範例：<code>INFO server</code> 顯示 valkey_version 而非 redis_version</li>
</ul>
<h3 id="跟-redis-的相容邊界">跟 Redis 的相容邊界</h3>
<p>子議題：</p>
<ul>
<li>Core data types / commands：100% 相容（fork 自 Redis 7.2.4）</li>
<li>Eviction / persistence / cluster：相容</li>
<li>Pub/Sub / Streams：相容</li>
<li><strong>不相容</strong>：Redis 7.4+ 引入的功能、Redis Stack 商業 modules</li>
</ul>
<h3 id="遷移評估">遷移評估</h3>
<p>子議題：</p>
<ul>
<li>AOF / RDB 文件格式相容、可直接拷貝資料目錄</li>
<li>Client library 完全相容、無需改 code</li>
<li>監控工具相容（RedisInsight 雖偏 Redis Inc、但基本命令通用）</li>
<li>需確認 modules 使用狀況（Stack modules 未必有 Valkey fork）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="從-redis-遷移">從 Redis 遷移</h3>
<p>子議題：</p>
<ul>
<li>評估 module 使用：列出當前使用的 Redis modules、確認 Valkey 對應替代</li>
<li>評估 Redis 7.4+ 功能使用（Functions、CLIENT NO-TOUCH 等）</li>
<li>遷移路徑：rolling restart with replica swap / 雙寫 / 直接 cutover</li>
<li>對應雲端 managed：AWS ElastiCache for Valkey 自動遷移工具</li>
</ul>
<h3 id="授權合規評估">授權合規評估</h3>
<p>子議題：</p>
<ul>
<li>為何 Redis 改 RSALv2 / SSPL — OSI 認知（不算 OSI 認可開源）</li>
<li>Valkey BSD 3-clause — 商業使用無限制</li>
<li>對 SaaS 供應商：Redis 限制把 Redis 當成 service 對外提供、Valkey 無此限制</li>
<li>對企業 / 公部門：開源合規政策可能要求 OSI 認可、Valkey 通過、Redis 不過</li>
</ul>
<h3 id="module-生態相容性">Module 生態相容性</h3>
<p>子議題：</p>
<ul>
<li>Valkey 計畫自有 modules（valkey-search / valkey-bloom 等）</li>
<li>Redis Stack modules（RedisJSON / RedisSearch）部分有 fork</li>
<li>評估你用的 modules 是否有 Valkey 替代、否則考慮遷 module-free 設計</li>
</ul>
<h3 id="雲端-managed-valkey">雲端 managed Valkey</h3>
<p>子議題：</p>
<ul>
<li>AWS ElastiCache for Valkey（成本比 Redis 低 ~20%、AWS 推）</li>
<li>GCP Memorystore（規劃 Valkey 支援）</li>
<li>Azure Cache（規劃中）</li>
<li>managed 邊界跟 ElastiCache for Redis 一致</li>
</ul>
<h3 id="跟-redis-8-的功能差距">跟 Redis 8 的功能差距</h3>
<p>子議題：</p>
<ul>
<li>Redis 8 新功能對 Valkey 的影響（功能落後幾個月）</li>
<li>Valkey 自有 roadmap（valkey.io/blog 追蹤）</li>
<li>何時 Redis 新功能值得遷回（罕見、通常 Valkey 跟上）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="client-連不上api-相容問題">Client 連不上（API 相容問題）</h3>
<p>操作原則：先確認 Valkey 回報的相容版本、再對照 client library 支援到 Redis 哪個版本。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 用這個判斷相容性</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># valkey_version:8.1.8</span></span></span></code></pre></div><p>絕大多數情況直接相容、若失敗多是 client library 太舊（不支援 Redis 7.2 對應版本）。</p>
<h3 id="module-不可用">Module 不可用</h3>
<p>操作原則：Valkey 對 Redis Stack modules 不一定有 fork、看 Valkey modules 清單。</p>
<h3 id="監控工具相容性">監控工具相容性</h3>
<p>操作原則：RedisInsight 連 Valkey 可能 partial 工作（部分 vendor-specific 命令缺）、用通用工具（valkey-cli、Prometheus + redis_exporter）較穩。</p>
<h3 id="performance-regressionvs-redis">Performance regression（vs Redis）</h3>
<p>操作原則：Valkey 跟 Redis 7.2.4 為 baseline、效能應接近、差距 &lt; 5% 屬於正常。明顯回歸要看 Valkey roadmap 是否有 known issue。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>依賴 Redis Stack 商業 modules</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（Redis Inc 商業版）</td>
      </tr>
      <tr>
          <td>純 KV cache 不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>極高 throughput / 多核</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（已 default Valkey）</td>
      </tr>
      <tr>
          <td>Durable Redis-compatible</td>
          <td>AWS MemoryDB</td>
      </tr>
      <tr>
          <td>跨雲 fully-portable</td>
          <td>Valkey self-host（無 vendor lock-in）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>完整 Valkey command reference（valkey.io/commands）</li>
<li>Linux Foundation governance 細節</li>
<li>各語言 client compatibility matrix</li>
<li>Redis Stack module 對應替代清單</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例沿用-redis-同源案例--待補-valkey-specific-case">直接相關案例（沿用 Redis 同源案例 + 待補 Valkey-specific case）</h3>
<p>Valkey 從 Redis 7.2.4 fork、API 與行為 100% 相容、Redis-on-Valkey 同源案例可直接套用。截至本文時 Valkey-specific production case 仍累積中。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Valkey 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移策略 client-side 實作、Valkey 跟 Redis 行為一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 在 Valkey 上跟 Redis 同樣 API、無遷移風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>invalidation / shard move 一致性議題、Valkey Cluster 沿用 Redis Cluster 模型</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Valkey-specific 案例</strong>：Linux Foundation Valkey customer adoption stories、AWS ElastiCache for Valkey 客戶遷移個案、re:Invent 2025+ talks、企業 OSI 合規驅動的遷移路徑公開分享。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Valkey 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>Valkey 跟 Redis 規模化路徑一致（fork 同源）、小型 single / 中型 Sentinel / 大型 Cluster</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter / singleflight 通用、Valkey 行為跟 Redis 一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>Memcached routing 案例、Valkey 對應為 Cluster + client-side routing 或 Envoy Redis proxy</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 為 Memcached based、Valkey 對應為 Global Datastore（ElastiCache for Valkey）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（fork 源頭）、<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a></li>
<li>下游能力：跟 Redis 完全一致、見 Redis vendor 頁的下游連結</li>
</ul>
]]></content:encoded></item><item><title>2.2 cache aside 與失效策略</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/</guid><description>&lt;p>旁路快取（cache aside）的核心責任是把讀取加速與正式狀態分離。資料庫維持 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a>，快取維持可重建副本；兩者透過失效策略與新鮮度窗口對齊。&lt;/p>
&lt;h2 id="基本流程">基本流程&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside&lt;/a> 的讀路徑是「先讀 cache，miss 後回源，再回填 cache」；寫路徑是「先寫 source of truth，再做 cache invalidation 或版本更新」。這個流程讓正式狀態維持單一責任，同時讓熱門讀取獲得低延遲。&lt;/p>
&lt;p>實務上要先定義 freshness window。每個資料類型可容忍的不新鮮時間不同：商品介紹可接受秒級延遲，價格、庫存、權限與配額則需要更短窗口或即時失效。&lt;/p>
&lt;h2 id="失效策略">失效策略&lt;/h2>
&lt;p>失效策略的責任是控制 cache 和 source of truth 之間的偏差。常見做法有三類：&lt;/p>
&lt;ol>
&lt;li>事件驅動失效：寫入成功後推事件刪 key 或更新版本，適合正確性要求高的資料。&lt;/li>
&lt;li>TTL 失效：以時間上限控制資料壽命，適合可短暫不新鮮的資料。&lt;/li>
&lt;li>混合策略：事件失效為主、TTL 為保底，適合多來源寫入或跨區快取。&lt;/li>
&lt;/ol>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data&lt;/a> 是快取系統的常態成本，視為例外事件會導致設計盲區。設計時要先定義可接受的 stale 形式，再設計對應補償與回退路徑。&lt;/p>
&lt;h3 id="應用層--邊緣層-invalidation-pipeline">應用層 + 邊緣層 Invalidation Pipeline&lt;/h3>
&lt;p>當系統同時用應用層快取（Redis、本機 cache）跟邊緣層快取（&lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN&lt;/a>）時、失效策略要把兩層當「一條 pipeline」設計、不能各自獨立 purge。兩層失效的物理特性差異：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>層級&lt;/th>
 &lt;th>Purge 控制&lt;/th>
 &lt;th>Purge 延遲&lt;/th>
 &lt;th>失敗代價&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>應用層 cache&lt;/td>
 &lt;td>自家 cluster 內、application 控制&lt;/td>
 &lt;td>毫秒 - 秒級（cache cluster 內傳播）&lt;/td>
 &lt;td>Cluster 內 stale、用戶感受立即修正&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>CDN edge&lt;/td>
 &lt;td>Vendor API 控制、全球節點同步&lt;/td>
 &lt;td>秒 - 分鐘級（傳統 origin pull）或 150ms 級（push-based）&lt;/td>
 &lt;td>全球節點 stale、回填到應用層污染快取&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>正確順序是「先應用層、再 CDN」：&lt;/p>
&lt;ol>
&lt;li>業務寫入完成、source of truth 更新&lt;/li>
&lt;li>Purge 應用層 cache（毫秒級完成）&lt;/li>
&lt;li>Purge CDN（秒級到分鐘級）&lt;/li>
&lt;li>等 CDN purge 完成的 ack（或設等待窗口）&lt;/li>
&lt;/ol>
&lt;p>順序顛倒會出事 — 若先 purge CDN、CDN 全球節點 miss 後到 origin 拉資料、若 origin 應用層還是舊 cache、CDN 會把舊資料回填到全球節點、stale 被「重新永久化」一個 TTL 週期。&lt;/p>
&lt;p>實務上的權衡是「CDN purge ack 是否要等」。等了會讓 write API latency 升高到秒級、不等則必須接受短暫雙層不一致。價格 / 庫存類資料適合「短 TTL + 等 purge ack」、blog 文章類適合「長 TTL + 不等 ack」。詳見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a> 的 purge 操作模型。&lt;/p>
&lt;h2 id="cache-aside-vs-write-through-的選擇">Cache aside vs write-through 的選擇&lt;/h2>
&lt;p>選 cache 模式由 &lt;em>miss 成本&lt;/em> 跟 &lt;em>寫入頻率&lt;/em> 的取捨決定。Cache aside、write-through、write-behind 三種主流模式各自適合不同業務壓力。&lt;/p>
&lt;p>&lt;strong>Cache aside&lt;/strong>（read-through）：寫入只動 source-of-truth、讀取 miss 時才填 cache。適合寫入頻率低於讀取、cache 可以重建、寫入失敗時 cache 保持不污染的場景。常見於商品詳情、推薦列表、設定值這類 read-heavy 資料、業務代價是 cache miss 時用戶等待回源、可接受。&lt;/p></description><content:encoded><![CDATA[<p>旁路快取（cache aside）的核心責任是把讀取加速與正式狀態分離。資料庫維持 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>，快取維持可重建副本；兩者透過失效策略與新鮮度窗口對齊。</p>
<h2 id="基本流程">基本流程</h2>
<p><a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">cache aside</a> 的讀路徑是「先讀 cache，miss 後回源，再回填 cache」；寫路徑是「先寫 source of truth，再做 cache invalidation 或版本更新」。這個流程讓正式狀態維持單一責任，同時讓熱門讀取獲得低延遲。</p>
<p>實務上要先定義 freshness window。每個資料類型可容忍的不新鮮時間不同：商品介紹可接受秒級延遲，價格、庫存、權限與配額則需要更短窗口或即時失效。</p>
<h2 id="失效策略">失效策略</h2>
<p>失效策略的責任是控制 cache 和 source of truth 之間的偏差。常見做法有三類：</p>
<ol>
<li>事件驅動失效：寫入成功後推事件刪 key 或更新版本，適合正確性要求高的資料。</li>
<li>TTL 失效：以時間上限控制資料壽命，適合可短暫不新鮮的資料。</li>
<li>混合策略：事件失效為主、TTL 為保底，適合多來源寫入或跨區快取。</li>
</ol>
<p><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 是快取系統的常態成本，視為例外事件會導致設計盲區。設計時要先定義可接受的 stale 形式，再設計對應補償與回退路徑。</p>
<h3 id="應用層--邊緣層-invalidation-pipeline">應用層 + 邊緣層 Invalidation Pipeline</h3>
<p>當系統同時用應用層快取（Redis、本機 cache）跟邊緣層快取（<a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">CDN</a>）時、失效策略要把兩層當「一條 pipeline」設計、不能各自獨立 purge。兩層失效的物理特性差異：</p>
<table>
  <thead>
      <tr>
          <th>層級</th>
          <th>Purge 控制</th>
          <th>Purge 延遲</th>
          <th>失敗代價</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>應用層 cache</td>
          <td>自家 cluster 內、application 控制</td>
          <td>毫秒 - 秒級（cache cluster 內傳播）</td>
          <td>Cluster 內 stale、用戶感受立即修正</td>
      </tr>
      <tr>
          <td>CDN edge</td>
          <td>Vendor API 控制、全球節點同步</td>
          <td>秒 - 分鐘級（傳統 origin pull）或 150ms 級（push-based）</td>
          <td>全球節點 stale、回填到應用層污染快取</td>
      </tr>
  </tbody>
</table>
<p>正確順序是「先應用層、再 CDN」：</p>
<ol>
<li>業務寫入完成、source of truth 更新</li>
<li>Purge 應用層 cache（毫秒級完成）</li>
<li>Purge CDN（秒級到分鐘級）</li>
<li>等 CDN purge 完成的 ack（或設等待窗口）</li>
</ol>
<p>順序顛倒會出事 — 若先 purge CDN、CDN 全球節點 miss 後到 origin 拉資料、若 origin 應用層還是舊 cache、CDN 會把舊資料回填到全球節點、stale 被「重新永久化」一個 TTL 週期。</p>
<p>實務上的權衡是「CDN purge ack 是否要等」。等了會讓 write API latency 升高到秒級、不等則必須接受短暫雙層不一致。價格 / 庫存類資料適合「短 TTL + 等 purge ack」、blog 文章類適合「長 TTL + 不等 ack」。詳見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a> 的 purge 操作模型。</p>
<h2 id="cache-aside-vs-write-through-的選擇">Cache aside vs write-through 的選擇</h2>
<p>選 cache 模式由 <em>miss 成本</em> 跟 <em>寫入頻率</em> 的取捨決定。Cache aside、write-through、write-behind 三種主流模式各自適合不同業務壓力。</p>
<p><strong>Cache aside</strong>（read-through）：寫入只動 source-of-truth、讀取 miss 時才填 cache。適合寫入頻率低於讀取、cache 可以重建、寫入失敗時 cache 保持不污染的場景。常見於商品詳情、推薦列表、設定值這類 read-heavy 資料、業務代價是 cache miss 時用戶等待回源、可接受。</p>
<p><strong>Write-through</strong>：寫入同時動 source-of-truth + cache、保證 cache 永遠最新。對應 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify Write-through Cache</a> — Shopify 在 Shop App 後端的 read-heavy 路徑用 write-through 降低 cache miss 風險、改善熱門資料讀取穩定性。適合場景：cache miss 成本很高（回源慢或會壓垮 origin）、寫入流量可控、資料更新時間可預測。典型應用包括熱門商品的庫存 / 價格、用戶 session、需要避免讀路徑抖動的場景。</p>
<p><strong>Write-behind</strong>（async）：寫入只動 cache、async 同步到 source-of-truth。適合寫入頻率極高、source-of-truth 跟不上、可接受 cache crash 丟失少量資料的場景。常見於 counter、rate limit、metrics aggregation 這類 <em>吞吐優先、可接受短暫不持久</em> 的資料。代價是 cache crash 會丟最近 N 秒寫入、要確認業務代價可承受。</p>
<p>判讀順序：先看 read/write 比例（read-heavy 偏 cache aside / write-through、write-extreme 偏 write-behind）、再看 miss 成本（miss 貴選 write-through、miss 便宜選 cache aside）、最後看持久性需求（不可丟選 write-through、可丟選 write-behind）。</p>
<h2 id="cache-模式選擇的判讀順序">Cache 模式選擇的判讀順序</h2>
<p>當「重算成本」「資料一致性」「持久性」三個維度互相衝突、選擇優先序：</p>
<ol>
<li><strong>持久性必須</strong>（不可丟、無法重建）→ 必須選 write-through 或 persistent store + cache、不能選 write-behind 或純 cache aside</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性嚴格</strong>（餘額、權限類）→ write-through 同步更新、確保 cache 不 stale</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性可放寬</strong> + <strong>重算貴</strong> → cache aside + 較長 TTL、減少回源</li>
<li><strong>持久性可接受失損</strong> + <strong>一致性可放寬</strong> + <strong>重算便宜</strong> → cache aside + 短 TTL 或 write-behind</li>
</ol>
<p>例如 ML feature store 場景（<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a>）— 持久性可接受失損（feature 可重算）、一致性可放寬（推薦演算法）、重算便宜（feature engineering pipeline 跑得到）— 落在第 4 類、Tubi 把 feature store 從 ScyllaDB 遷到 ElastiCache 是合理取捨。p99 落在 ElastiCache 的 &lt; 10ms 範圍（先前 ScyllaDB-based 架構為 ML inference 路徑的延遲瓶頸、案例未公開 ScyllaDB 端具體延遲數字）。</p>
<p>判讀重點：cache 的本質是用 miss 風險換取 latency；資料若無法重建、需採 persistent store 並接受 latency 成本；資料若可重建但一致性嚴格、可用 cache 但要 write-through 確保即時收斂。詳見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a> 的「Cache vs Persistent Store 取捨」段。</p>
<h2 id="判讀訊號與回源保護">判讀訊號與回源保護</h2>
<p>cache 命中下降時，來源系統會承受瞬間回源壓力。回源保護需要和失效策略一起設計：</p>
<table>
  <thead>
      <tr>
          <th>風險訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>hit ratio 下降且 origin QPS 快速上升</td>
          <td>大量 key 同時過期或失效策略失準</td>
          <td>分散 TTL、分批失效、啟用 <a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a></td>
      </tr>
      <tr>
          <td>熱門 key miss 後延遲與錯誤率同步上升</td>
          <td>單 key 造成 stampede</td>
          <td>啟用 request coalescing、局部預熱、限流回源</td>
      </tr>
      <tr>
          <td>cache 層延遲穩定但業務錯誤增加</td>
          <td>值語意過期或序列化版本漂移</td>
          <td>補 key version 與 schema migration</td>
      </tr>
      <tr>
          <td>eviction rate 升高且 value size 變大</td>
          <td>容量策略與資料形狀不匹配</td>
          <td>重配記憶體策略、調整 value 拆分</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a> 與 <a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 都是回源保護議題；重點是把來源系統視為有限資源，讓 miss 風險可控。</p>
<h2 id="服務情境">服務情境</h2>
<p>商品詳情頁是典型 cache aside 場景。頁面讀取需要組合商品主檔、價格、庫存與行銷標籤。主檔可用較長 TTL 與背景更新，價格與庫存則用事件失效與較短 TTL，讓讀取延遲與正確性維持平衡。</p>
<p>當促銷開始時，大量熱門商品同時被讀取。這時 cache 策略的重點從命中率轉到來源保護與新鮮度控制：是否能限制回源尖峰、是否能快速修正錯誤資料、是否能在事故時降級。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把命中率當作唯一目標，會忽略資料語意與失敗代價。命中率高不代表結果正確，尤其在價格、權限、配額類資料。</p>
<p>把 cache 當成正式資料來源，會讓資料修復與稽核變複雜。快取系統適合承擔讀取加速，不適合承擔正式狀態的最終判定。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>cache aside 的失效風險可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> 做回寫。先看事件中的失效節奏：是大批 key 同時過期、失效順序錯置，還是熱點 key 回源放大，再對照本章的 freshness window、回源保護與容量策略。
這個案例主要支撐的是「失效節奏與回源壓力」判讀，不直接支撐分散式鎖租約或 queue replay；若是互斥控制或重播問題，應轉到 2.4 或 3.x。</p>
<p>命中率看似正常但業務錯誤上升時，先回到本章檢查值語意與 key 版本化，再把量測缺口接到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>cache aside 的設計會直接影響觀測、驗證與事故處理。</p>
<ol>
<li>與 01 的交接：source of truth 與查詢壓力回到 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發讀寫邊界</a>。</li>
<li>與 04 的交接：hit ratio、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 與 eviction 進入 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 06 的交接：回源保護與壓測邊界進入 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 08 的交接：失效策略誤配與 stampede 事故回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p><strong>規模成長路線下一站 → <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a></strong>：應用層快取上面還有 CDN 邊緣層、兩層失效時序要對齊（先 purge 應用層、再 purge 邊緣層、避免邊緣回填到應用層舊資料）。</p>
<p>其他延伸方向：</p>
<ul>
<li>進一步處理 TTL、容量與淘汰策略 → <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a></li>
<li>快取策略在真實事件中的失敗與修復 → <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a></li>
</ul>
]]></content:encoded></item><item><title>模組二：快取與 Redis</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/</link><pubDate>Wed, 22 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/</guid><description>&lt;p>快取模組的核心目標是說明暫存資料如何提升讀取效率，同時保護 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 的正式判斷責任。語言教材會處理 cache port、資料複製邊界與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 的程式邊界；本模組負責 Redis 與快取策略的具體實作。&lt;/p>
&lt;h2 id="vendor--platform-清單">Vendor / Platform 清單&lt;/h2>
&lt;p>實作時的常用選擇見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors&lt;/a> — T1 收錄 Redis / Valkey / Memcached / DragonflyDB / AWS ElastiCache，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。&lt;/p>
&lt;p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors/&lt;/a> 的「內容覆蓋進度」段。&lt;/p>
&lt;h2 id="暫定分類">暫定分類&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>分類&lt;/th>
 &lt;th>內容方向&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">Cache aside&lt;/a>&lt;/td>
 &lt;td>read-through 思路、cache &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss&lt;/a>、invalidation&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a>&lt;/td>
 &lt;td>過期策略、容量控制、熱點資料&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">Redis data types&lt;/a>&lt;/td>
 &lt;td>string、hash、set、sorted set、stream 的適用場景&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">Presence store&lt;/a>&lt;/td>
 &lt;td>即時連線狀態、過期清理、跨節點查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">Distributed lock&lt;/a>&lt;/td>
 &lt;td>lock 語意、租約、失效與風險&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a>&lt;/td>
 &lt;td>即時通知、跨節點 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out&lt;/a>、可靠性限制&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="快取分層與邊緣層">快取分層與邊緣層&lt;/h2>
&lt;p>本模組討論的是「應用層快取」（Redis、in-memory cache），跟 CDN / edge cache 是不同責任：CDN 解決「請求是否需要進到應用程式」（網路入口層），本模組討論的快取解決「應用程式如何降低資料層讀寫成本」（應用層）。完整三層快取分工（邊緣層 → 應用層 → DB buffer pool）跟 origin protection 設計見 &lt;a href="https://tarrragon.github.io/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源&lt;/a>。&lt;/p>
&lt;p>兩層快取的失效路徑要協調設計：應用層 purge 在自家 cluster 內可控、CDN purge 要等全球節點同步。寫入路徑變更時，要先 purge 應用層、再 purge 邊緣層，避免短時間內邊緣回填到應用層舊資料。&lt;/p>
&lt;h2 id="選型入口">選型入口&lt;/h2>
&lt;p>快取選型的核心判斷是資料是否可以重建，以及讀取壓力是否集中。當正式狀態已經存在於資料庫或下游服務，但熱門讀取造成延遲、成本或容量壓力時，快取與 Redis 值得優先評估。&lt;/p>
&lt;p>Cache aside 適合商品詳情、權限摘要、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag&lt;/a> 這類可重建讀取資料；&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 用來控制資料新鮮度與容量；Redis data types 用來表達 set、sorted set、hash、stream 等不同資料形狀；presence store 適合即時連線狀態；distributed lock 適合需要短時間互斥的協調流程；pub/sub 適合即時 fan-out。&lt;/p></description><content:encoded><![CDATA[<p>快取模組的核心目標是說明暫存資料如何提升讀取效率，同時保護 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 的正式判斷責任。語言教材會處理 cache port、資料複製邊界與 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 的程式邊界；本模組負責 Redis 與快取策略的具體實作。</p>
<h2 id="vendor--platform-清單">Vendor / Platform 清單</h2>
<p>實作時的常用選擇見 <a href="/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors</a> — T1 收錄 Redis / Valkey / Memcached / DragonflyDB / AWS ElastiCache，每個 vendor 有定位、適用場景、取捨與預計實作話題的骨架。</p>
<p>Deep article（vendor 自身的配置、故障、容量）跟 migration playbook（跨 vendor 遷移流程）的撰寫進度見 <a href="/blog/backend/02-cache-redis/vendors/" data-link-title="快取 Vendor 清單" data-link-desc="規劃快取、Redis 相容服務與 managed cache 的服務頁撰寫順序與判準">vendors/</a> 的「內容覆蓋進度」段。</p>
<h2 id="暫定分類">暫定分類</h2>
<table>
  <thead>
      <tr>
          <th>分類</th>
          <th>內容方向</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/cache-aside/" data-link-title="Cache Aside" data-link-desc="說明 application 如何在讀取時自行管理快取與正式資料來源">Cache aside</a></td>
          <td>read-through 思路、cache <a href="/blog/backend/knowledge-cards/cache-hit-miss/" data-link-title="Cache Hit / Miss" data-link-desc="說明快取命中與未命中如何影響讀取成本與下游壓力">miss</a>、invalidation</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a></td>
          <td>過期策略、容量控制、熱點資料</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">Redis data types</a></td>
          <td>string、hash、set、sorted set、stream 的適用場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">Presence store</a></td>
          <td>即時連線狀態、過期清理、跨節點查詢</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">Distributed lock</a></td>
          <td>lock 語意、租約、失效與風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a></td>
          <td>即時通知、跨節點 <a href="/blog/backend/knowledge-cards/fan-out/" data-link-title="Fan-out" data-link-desc="說明單一事件同時分發給多個下游的訊息拓撲">fan-out</a>、可靠性限制</td>
      </tr>
  </tbody>
</table>
<h2 id="快取分層與邊緣層">快取分層與邊緣層</h2>
<p>本模組討論的是「應用層快取」（Redis、in-memory cache），跟 CDN / edge cache 是不同責任：CDN 解決「請求是否需要進到應用程式」（網路入口層），本模組討論的快取解決「應用程式如何降低資料層讀寫成本」（應用層）。完整三層快取分工（邊緣層 → 應用層 → DB buffer pool）跟 origin protection 設計見 <a href="/blog/backend/05-deployment-platform/edge-cdn-static-distribution/" data-link-title="5.9 邊緣分發與靜態資源（CDN / Origin Protection）" data-link-desc="整理 CDN 與 edge cache 在部署平台中的責任邊界、origin protection、purge 與 invalidation 策略">5.9 邊緣分發與靜態資源</a>。</p>
<p>兩層快取的失效路徑要協調設計：應用層 purge 在自家 cluster 內可控、CDN purge 要等全球節點同步。寫入路徑變更時，要先 purge 應用層、再 purge 邊緣層，避免短時間內邊緣回填到應用層舊資料。</p>
<h2 id="選型入口">選型入口</h2>
<p>快取選型的核心判斷是資料是否可以重建，以及讀取壓力是否集中。當正式狀態已經存在於資料庫或下游服務，但熱門讀取造成延遲、成本或容量壓力時，快取與 Redis 值得優先評估。</p>
<p>Cache aside 適合商品詳情、權限摘要、<a href="/blog/backend/knowledge-cards/feature-flag/" data-link-title="Feature Flag" data-link-desc="說明如何用可動態開關控制功能曝光與風險">feature flag</a> 這類可重建讀取資料；<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 用來控制資料新鮮度與容量；Redis data types 用來表達 set、sorted set、hash、stream 等不同資料形狀；presence store 適合即時連線狀態；distributed lock 適合需要短時間互斥的協調流程；pub/sub 適合即時 fan-out。</p>
<p>接近真實網路服務的例子包括熱門商品頁、會員 session、<a href="/blog/backend/knowledge-cards/websocket/" data-link-title="WebSocket" data-link-desc="說明 WebSocket 如何提供長連線雙向即時通訊">WebSocket</a> presence、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> counter 與跨節點通知。這些場景的共同問題是讀取節奏、過期策略與資料一致性，因此本模組會先處理資料形狀、<a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a>、<a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 與失效邊界。</p>
<h2 id="與語言教材的分工">與語言教材的分工</h2>
<p>語言教材處理 interface / <a href="/blog/backend/knowledge-cards/protocol/" data-link-title="Communication Protocol" data-link-desc="說明不同系統如何對齊資料交換與錯誤語意">protocol</a>、並發或非同步保護、<a href="/blog/backend/knowledge-cards/timeout/" data-link-title="Timeout" data-link-desc="說明等待外部操作的時間上限如何保護資源與使用者體驗">timeout</a> 與 cache 呼叫邊界。Backend cache 模組處理 Redis command、資料結構、失效策略、跨節點一致性與操作風險。</p>
<h2 id="案例驅動讀法">案例驅動讀法</h2>
<p>快取案例的核心讀法是先看「一致性問題長什麼樣」，再決定要調策略還是調架構。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>先看章節</th>
          <th>回寫目標</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta：Cache Consistency 升級</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>把 invalidation 問題前移到訊號治理 + mutation tracing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>把快取路由層納入可用性邊界、跨區一致性窗口設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify：序列化遷移</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>把格式轉換做成雙軌相容與可回退流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta：CacheLib / Kangaroo 分層快取</a></td>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>分層 cache 容量跟成本曲線（DRAM / flash / 持久 KV）</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify：Write-through Cache</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>cache aside / write-through / write-behind 選擇條件</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix：EVCache 全域快取層</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>、<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a></td>
          <td>cache 成為跨區資料層、平台層基礎設施</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare：Cache Reserve 分層</a></td>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>edge + persistent reserve 的長尾命中率設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta：TAO 社交圖快取演進</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>cache 變資料層能力、資料模型治理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder：ElastiCache 47M MAU</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>cache 是主要服務面、sustained growth 成本曲線</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi：ML feature store</a></td>
          <td><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>ML feature store 三層 cache 設計、cache vs persistent store 取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap：KeyDB cross-cloud</a></td>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>KeyDB multi-threaded fork、跨 cloud 部署資料引力</td>
      </tr>
  </tbody>
</table>
<h2 id="章節列表">章節列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>關鍵收穫</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a></td>
          <td>高併發下的 Redis 讀寫邊界</td>
          <td>共用 client、控制 pipeline、避免 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 與 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a></td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
          <td>cache aside 與失效策略</td>
          <td>寫出讀取優先的 cache 流程與失效方式</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a></td>
          <td>TTL 與 eviction</td>
          <td>規劃過期、淘汰與容量控制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.4</a></td>
          <td>distributed lock 與租約</td>
          <td>分辨鎖語意、租約風險與適用場景</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5</a></td>
          <td>presence store 與即時狀態</td>
          <td>追蹤線上狀態、跨節點查詢與過期清理</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">2.6</a></td>
          <td>快取威脅建模（Threat Modeling）</td>
          <td>用一致性、污染、放大與 side-channel 風險盤點快取設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a></td>
          <td>Cache Copy Boundary 與 Freshness</td>
          <td>分辨快取副本、正式狀態、新鮮度與回源保護</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a></td>
          <td>Cache Data Shape 與 Access Pattern</td>
          <td>用 key space、value shape 與 access pattern 判讀資料形狀</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9</a></td>
          <td>Cache Migration 與 Stampede Rollback 實作示範</td>
          <td>以商品詳情或價格快取示範 evidence、gate 與 rollback trigger</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10</a></td>
          <td>Pub/Sub 與即時 fan-out</td>
          <td>用 at-most-once 邊界判讀即時廣播何時夠用、何時升級到 Streams 或 message queue</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11</a></td>
          <td>Redis data types 實作</td>
          <td>用 sorted set、bitmap、HLL、counter、hash 各自的原子性與記憶體曲線選型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/" data-link-title="模組二案例正文" data-link-desc="快取策略與快取平台演進案例入口。">2.C</a></td>
          <td>轉換案例正文</td>
          <td>把快取策略、路由層與序列化遷移轉成可回寫實作</td>
      </tr>
  </tbody>
</table>
<p>反例與規模對照入口： <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> / <a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 對照</a>。</p>
<p>回退判讀寫法見 <a href="/blog/backend/00-service-selection/cases/post-scale-migration-language-tool-architecture/#%e5%9b%9e%e9%80%80%e5%88%a4%e8%ae%80%e5%af%ab%e6%b3%95" data-link-title="營運後技術轉換：語言、工具與架構何時該換" data-link-desc="服務營運一段時間後，如何判讀何時該轉語言、工具或架構，並用案例說明轉換動機。">0.C4 回退判讀寫法</a>，快取案例要優先保留回源壓力、資料新鮮度與熱門 key 行為。</p>
<p>跨模組端到端串聯（DB → cache → event → observability）見 <a href="/blog/backend/00-service-selection/cross-module-checkout-episode/" data-link-title="0.15 跨模組 Checkout Episode：從資料寫入到觀測證據" data-link-desc="以 checkout 為切片，走完 DB write → cache invalidation → event publish → observability evidence 四層串聯，標示各模組的交接欄位與失敗判讀">0.15 跨模組 Checkout Episode</a>。</p>
<h2 id="觀念網路補完方向">觀念網路補完方向</h2>
<p>快取章節下一輪的核心責任是把「暫存副本」和「正式狀態」的界線寫清楚。現有章節已經有 cache aside、TTL、distributed lock、presence store，並補上了 Pub/Sub 即時 fan-out（2.10）與 data types 型別實作（2.11）兩個向度；仍可深化的是資料新鮮度、失效語意、回源保護與快取遷移之間的引用關係，讓讀者知道快取策略何時只是加速，何時已經變成服務正確性風險。</p>
<table>
  <thead>
      <tr>
          <th>補完方向</th>
          <th>需要回答的問題</th>
          <th>主要路由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Cache copy boundary</td>
          <td>cache value 是否只是可重建副本，還是被誤用成正式狀態</td>
          <td><a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a>、<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a></td>
      </tr>
      <tr>
          <td>Freshness window</td>
          <td>stale data 在產品上可接受多久，誰承擔錯誤後果</td>
          <td><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a></td>
      </tr>
      <tr>
          <td>Invalidation model</td>
          <td>更新、刪除、TTL、event invalidation 是否互相對齊</td>
          <td><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>、<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a></td>
      </tr>
      <tr>
          <td>Origin protection</td>
          <td>miss、hot key、stampede 是否會把壓力打回資料庫</td>
          <td><a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a></td>
      </tr>
      <tr>
          <td>Cache migration</td>
          <td>key format、value schema、TTL 策略是否能分批回退</td>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a>、<a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a></td>
      </tr>
  </tbody>
</table>
<p>這些方向要用快取自己的服務壓力展開。商品詳情、價格、權限摘要、presence 與 rate limit 的失敗代價不同，寫作時要分別處理它們的新鮮度與回源壓力。</p>
<h2 id="知識卡補強方向">知識卡補強方向</h2>
<p>快取模組的 knowledge card 缺口集中在「新鮮度」與「回源保護」。已有 <a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache hit rate</a>、<a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a>、<a href="/blog/backend/knowledge-cards/cache-prefetching/" data-link-title="Cache Prefetching" data-link-desc="說明系統如何在資料被需要前預先載入快取">cache prefetching</a> 與 <a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 可以先引用。</p>
<p>下一批候選卡片包括 freshness window、origin protection、request coalescing（single-flight）、negative cache、cache key versioning 與 cache serialization migration。這些卡片要讓讀者能分辨「可短暫不新鮮」和「錯誤會直接影響交易或權限」的差異。2.4 帶入的 fencing token 是跨模組的分散式術語、且是「鎖不是正確性保證」這個核心論點的依據，值得獨立建卡（候選）。</p>
<h2 id="實作探討入口">實作探討入口</h2>
<p>快取的第一條實作路徑是 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback（實作示範）</a>。這篇以商品詳情或價格快取為例，說明 cache evidence package、origin protection gate、warmup plan 與 rollback trigger 如何一起成立。型別實作層面的具體入口是 <a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11 Redis data types 實作</a>，聚焦 sorted set、bitmap、HLL、counter、hash 各自的操作語意、原子性與容量行為。</p>
<p>這條路徑的前置引用應該是 2.2 cache aside、2.3 TTL / eviction、<a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>、<a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a> 與 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>。完成後可依 <a href="/blog/backend/#%e5%ad%b8%e7%bf%92%e8%b7%af%e7%b7%9a" data-link-title="Backend 服務實務指南" data-link-desc="用跨語言教學路線整理資料庫、快取、訊息佇列、觀測、部署、可靠性、資安、事故與容量等後端服務能力">Backend 學習路線</a> 進入下一條服務路徑。</p>
<p>快取路徑的 artifact 對齊重點是「先證明回源壓力受控，再擴大快取覆蓋率」。對 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17</a> / <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20</a> 要交 <code>Source/Time range/Query link/Owner/Data quality</code>，並覆蓋 hit/miss、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 與 hot key 分布；對 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20</a> / <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8</a> 要交 <code>Gate decision/Checks/Stop condition/Rollback window/Owner</code>，呈現 warmup 演練與 stampede 停損門檻；對 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22</a> / <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19</a> 要交 <code>Timestamp/Decision/Context/Evidence/Owner/Expected effect/Rollback condition</code>，記錄 key pattern、影響範圍與修復後追蹤信號。</p>
<h2 id="跨語言適配評估">跨語言適配評估</h2>
<p>快取與 Redis 的使用方式會受語言的資料複製模型、client lifecycle、序列化成本與並發模型影響。同步 runtime 要避免每個 request 建立連線；async runtime 要避免 blocking Redis client 卡住 event loop；輕量並發 runtime 要用 timeout、<a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與 pipeline 邊界保護 Redis。動態語言要特別留意 cache value schema 演進；強型別語言則要避免把內部型別直接當成跨服務快取 <a href="/blog/backend/knowledge-cards/contract/" data-link-title="Boundary Contract" data-link-desc="說明跨邊界約定如何維持相容與可驗證">contract</a>。</p>
]]></content:encoded></item><item><title>2.C3 Shopify：快取序列化格式遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/</guid><description>&lt;p>這個案例的核心責任是說明快取轉換常見的格式遷移如何安全落地。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Shopify 在快取編碼轉換過程使用雙軌策略，先允許新舊格式共存，再逐步收斂。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>快取格式轉換本質上是相容性遷移。若一次切換，回退與資料可讀性風險會放大。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>新格式可編碼就先寫新格式。&lt;/li>
&lt;li>編碼失敗回落舊格式，保留服務可用性。&lt;/li>
&lt;li>維持一段雙軌期，觀測命中率與錯誤率再收斂。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/caching-without-marshal-part-two-messagepack">Caching Without Marshal Part 2&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取轉換常見的格式遷移如何安全落地。</p>
<h2 id="觀察">觀察</h2>
<p>Shopify 在快取編碼轉換過程使用雙軌策略，先允許新舊格式共存，再逐步收斂。</p>
<h2 id="判讀">判讀</h2>
<p>快取格式轉換本質上是相容性遷移。若一次切換，回退與資料可讀性風險會放大。</p>
<h2 id="策略">策略</h2>
<ol>
<li>新格式可編碼就先寫新格式。</li>
<li>編碼失敗回落舊格式，保留服務可用性。</li>
<li>維持一段雙軌期，觀測命中率與錯誤率再收斂。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 migration safety</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/caching-without-marshal-part-two-messagepack">Caching Without Marshal Part 2</a></li>
</ul>
]]></content:encoded></item><item><title>Memcached</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/</guid><description>&lt;p>Memcached 是純粹的 in-memory key-value cache、承擔三個責任：簡單 string KV cache、多執行緒高吞吐、嚴格的 cache 邊界（無持久化 / 無 data types / 無 lock）。設計取捨偏向「越簡單越好」— 沒有 Redis 的 data types / Streams / Pub/Sub、也沒有持久化 / 複製 / cluster mode。極輕量、運維成本低、適合 strict cache 場景。&lt;/p>
&lt;p>對「純 cache、避免誤用為 source-of-truth、需要多執行緒高 throughput、極簡運維」這條路徑、Memcached 是首選。從 LiveJournal 2003 年開源至今、是業界最久經考驗的 cache。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 Memcached、用 telnet 或 memcached-tool 驗證&lt;/li>
&lt;li>用 SET / GET / DELETE / INCR / DECR 操作、區分 Memcached 跟 Redis 的場景界限&lt;/li>
&lt;li>設計 client-side consistent hashing 做 sharding&lt;/li>
&lt;li>看懂 hit rate / slab fragmentation / eviction 訊號&lt;/li>
&lt;li>評估 Memcached vs Redis 的選用判讀（何時純粹勝過豐富）&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-memcached-跑起來">最短路徑：5 分鐘把 Memcached 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 Memcached（-t 4 開 4 條 worker thread、-m 64 給 64MB）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 memcached -t &lt;span class="m">4&lt;/span> -m &lt;span class="m">64&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 text protocol 驗證讀寫（沒有 redis-cli 這種專屬 CLI、直接走 TCP）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># set &amp;lt;key&amp;gt; &amp;lt;flags&amp;gt; &amp;lt;ttl&amp;gt; &amp;lt;bytes&amp;gt;，下一行是 value&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;set foo 0 60 3\r\nbar\r\nget foo\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="c1"># STORED&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="c1"># VALUE foo 0 3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="c1"># END&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認多執行緒與記憶體上限&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;stats settings\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;num_threads|maxbytes&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT maxbytes 67108864 ← 64MB&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT num_threads 4 ← -t 4 生效&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑驗證「Memcached 起來、能讀寫、多執行緒生效」。Memcached 沒有 redis-cli 這類專屬 CLI、實際 ops 走 client library（python-memcached / pylibmc / go memcache）+ &lt;code>stats&lt;/code> 系列命令。實機驗證於 memcached:1.6（VERSION 1.6.42）、最後檢查日 2026-06-16。&lt;/p></description><content:encoded><![CDATA[<p>Memcached 是純粹的 in-memory key-value cache、承擔三個責任：簡單 string KV cache、多執行緒高吞吐、嚴格的 cache 邊界（無持久化 / 無 data types / 無 lock）。設計取捨偏向「越簡單越好」— 沒有 Redis 的 data types / Streams / Pub/Sub、也沒有持久化 / 複製 / cluster mode。極輕量、運維成本低、適合 strict cache 場景。</p>
<p>對「純 cache、避免誤用為 source-of-truth、需要多執行緒高 throughput、極簡運維」這條路徑、Memcached 是首選。從 LiveJournal 2003 年開源至今、是業界最久經考驗的 cache。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 Memcached、用 telnet 或 memcached-tool 驗證</li>
<li>用 SET / GET / DELETE / INCR / DECR 操作、區分 Memcached 跟 Redis 的場景界限</li>
<li>設計 client-side consistent hashing 做 sharding</li>
<li>看懂 hit rate / slab fragmentation / eviction 訊號</li>
<li>評估 Memcached vs Redis 的選用判讀（何時純粹勝過豐富）</li>
</ol>
<h2 id="最短路徑5-分鐘把-memcached-跑起來">最短路徑：5 分鐘把 Memcached 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 Memcached（-t 4 開 4 條 worker thread、-m 64 給 64MB）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 memcached -t <span class="m">4</span> -m <span class="m">64</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1"># 2. 用 text protocol 驗證讀寫（沒有 redis-cli 這種專屬 CLI、直接走 TCP）</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#    set &lt;key&gt; &lt;flags&gt; &lt;ttl&gt; &lt;bytes&gt;，下一行是 value</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;set foo 0 60 3\r\nbar\r\nget foo\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># STORED</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># VALUE foo 0 3</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># bar</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># END</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 3. 確認多執行緒與記憶體上限</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;stats settings\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;num_threads|maxbytes&#34;</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># STAT maxbytes 67108864      ← 64MB</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># STAT num_threads 4          ← -t 4 生效</span></span></span></code></pre></div><p>最短路徑驗證「Memcached 起來、能讀寫、多執行緒生效」。Memcached 沒有 redis-cli 這類專屬 CLI、實際 ops 走 client library（python-memcached / pylibmc / go memcache）+ <code>stats</code> 系列命令。實機驗證於 memcached:1.6（VERSION 1.6.42）、最後檢查日 2026-06-16。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="協議與-client-library">協議與 client library</h3>
<p>子議題：</p>
<ul>
<li>ASCII protocol vs binary protocol（兩種都支援、binary 較有效率）</li>
<li>Client library：python-memcached、pylibmc（libmemcached 綁定）、go memcache、Java spymemcached</li>
<li>Connection management：connection pool / persistent connection</li>
</ul>
<h3 id="指令對照">指令對照</h3>
<p>子議題：</p>
<ul>
<li>基本：SET / GET / ADD / REPLACE / DELETE / FLUSH_ALL</li>
<li>Counter：INCR / DECR（不能 &lt; 0）</li>
<li>條件：CAS（compare-and-swap）做 optimistic lock</li>
<li>批次：GETS（批次 + CAS token）</li>
</ul>
<h3 id="client-side-sharding">Client-side sharding</h3>
<p>Memcached server 本身無 cluster mode、靠 client library 做 sharding。子議題：</p>
<ul>
<li>Consistent hashing（ketama）— 加減 node 時 minimum key 移動</li>
<li>Hash 演算法：md5 / SHA1 / ketama</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a></li>
</ul>
<h3 id="memory-modelslab-allocator">Memory model（slab allocator）</h3>
<p>子議題：</p>
<ul>
<li>Memcached 用 slab allocator 預分配記憶體 chunk</li>
<li>不同 size class（slab class）對應不同 chunk size</li>
<li>Fragmentation：當 value size 跟 slab 不對齊、memory 浪費</li>
<li>對應指令：<code>stats slabs</code> / <code>stats items</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="slab-allocator-與-memory-fragmentation">Slab allocator 與 memory fragmentation</h3>
<p>子議題：</p>
<ul>
<li>Slab class 自動分配機制</li>
<li>Slab reassignment（Memcached 1.4.25+）— 把記憶體在 slab class 間搬移</li>
<li>監控 <code>STAT total_malloced</code> vs <code>STAT bytes_read</code></li>
<li>對應指令：<code>stats slabs</code>、<code>slabs reassign &lt;src&gt; &lt;dst&gt;</code></li>
</ul>
<h3 id="multi-threaded-scaling">Multi-threaded scaling</h3>
<p>子議題：</p>
<ul>
<li>Memcached 從早期就 multi-threaded（vs Redis 早期 single-thread）</li>
<li><code>-t</code> 設 thread 數、預設 4、依 CPU core 調</li>
<li>Lock contention：高 thread 數可能 hit per-bucket lock</li>
<li>對比 Redis：Redis 6+ 加 I/O threads、但 main thread 仍單線</li>
</ul>
<h3 id="aws-elasticache-for-memcached">AWS ElastiCache for Memcached</h3>
<p>子議題：</p>
<ul>
<li>ElastiCache 提供 managed Memcached cluster</li>
<li>Auto Discovery：客戶端自動發現 cluster node 變化</li>
<li>ElastiCache config endpoint 取代 client-side sharding 配置</li>
<li>跟 Redis ElastiCache 的成本對照</li>
</ul>
<h3 id="cascompare-and-swap">CAS（compare-and-swap）</h3>
<p>子議題：</p>
<ul>
<li>GETS 拿 value + token、SET 帶 token 做 conditional update</li>
<li>適合做 optimistic lock（vs Redis SETNX + lua）</li>
<li>CAS 失敗時的 retry 策略</li>
</ul>
<h3 id="memcached-vs-redis-的場景區分">Memcached vs Redis 的場景區分</h3>
<p>子議題：</p>
<ul>
<li>純 cache 不需 data types → Memcached 更輕量</li>
<li>Session store / counter / hot key 兩者都行</li>
<li>Leaderboard / sorted set / Streams / Pub/Sub → 只 Redis</li>
<li>Distributed lock → Redis（Memcached CAS 不夠強）</li>
<li>持久化（cache warmup 後不想全失）→ Redis（RDB / AOF）</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="hit-rate-下降">Hit rate 下降</h3>
<p>操作原則：先看 eviction 是否提高、再看 key naming 是否變動。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;stats\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;get_hits|get_misses|evictions&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># get_hits / get_misses 算 hit rate、evictions 持續增加代表 memory 壓力</span></span></span></code></pre></div><h3 id="eviction-增加memory-pressure">Eviction 增加（memory pressure）</h3>
<p>操作原則：超過 <code>-m</code> 設定的 memory limit、Memcached 用 LRU evict 老 key。看 <code>stats slabs</code> 哪些 slab class 最常 evict、可能要 slab reassign。</p>
<h3 id="slab-fragmentation">Slab fragmentation</h3>
<p>操作原則：value size 跟 slab class 不對齊造成 wasted memory。判讀：<code>stats slabs</code> 看每個 slab class 的 used vs total chunks。</p>
<h3 id="client-side-sharding-不平衡">Client-side sharding 不平衡</h3>
<p>操作原則：node 加減後、ketama 應 minimum 移動、但實際分布可能因 key 集中而偏斜。判讀：每個 node 的 <code>stats</code> 看 key count + memory usage 是否均衡。</p>
<h3 id="connection-耗盡">Connection 耗盡</h3>
<p>操作原則：每個 client 開太多 connection、Memcached 預設 max 1024 connection。看 <code>stats curr_connections</code>。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 data types（hash / list / set）</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要持久化 / 半持久化</td>
          <td>Redis with AOF / RDB</td>
      </tr>
      <tr>
          <td>需要 distributed lock</td>
          <td>Redis（Redlock 或 SETNX）</td>
      </tr>
      <tr>
          <td>需要 Pub/Sub / Streams</td>
          <td>Redis / Kafka / NATS</td>
      </tr>
      <tr>
          <td>多核高 throughput</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache for Memcached</a></td>
      </tr>
      <tr>
          <td>Process-local cache</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> / Guava Cache（JVM 內、無網路）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>各語言 Memcached client 完整 API</li>
<li>Memcached internal data structure 細節</li>
<li>Custom binary protocol 實作</li>
<li>ASCII vs binary protocol 完整對照</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Memcached 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>mcrouter 是 Memcached 專屬 protocol-aware routing proxy、處理跨叢集 / 跨區流量收斂與失效隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 基於 Memcached、Netflix 加上跨 AZ replication + client-side smart routing</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>TAO 底層用 Memcached 作為 graph 資料的快取層、上層加一致性 / 關聯查詢能力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>Meta 大規模 Memcached 部署的 invalidation / shard move 一致性治理</td>
      </tr>
  </tbody>
</table>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Memcached 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>通用、Memcached 也需 TTL jitter / lease / probabilistic early expiration</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single instance / 中型 client-side ketama / 大型 mcrouter 路由 + 跨區 pool</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>CacheLib 是 Memcached 之後 Meta 的分層 cache library、處理 DRAM 經濟極限後的議題</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 編碼遷移在 Memcached 上一樣適用、雙軌策略不依賴 vendor</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 模式 Memcached 用 SET + CAS 實作、不像 Redis 有 Lua / transaction 可組合</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a></li>
</ul>
]]></content:encoded></item><item><title>2.3 TTL 與 eviction</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/</guid><description>&lt;p>存活時間與淘汰策略（TTL and eviction）的核心責任是把快取資源分配成可預期策略。TTL 決定資料可存活多久，eviction 決定容量壓力下誰先被移除；兩者共同定義快取的新鮮度、命中率與回源風險。&lt;/p>
&lt;h2 id="ttl-是新鮮度預算">TTL 是新鮮度預算&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 是資料類型的新鮮度預算，用單一時間常數理解它會漏掉關鍵差異。商品描述、推薦列表、活動文案可容忍較長 TTL；價格、庫存、配額、權限則需要更短 TTL 或事件失效。&lt;/p>
&lt;p>TTL 設計要連到業務代價。可容忍舊資料的欄位可用長 TTL 降回源壓力；不可容忍錯誤結果的欄位要搭配事件失效與版本控制，讓 TTL 只作為保底機制。&lt;/p>
&lt;h2 id="eviction-是容量分流策略">eviction 是容量分流策略&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 的責任是當記憶體不足時，優先保留最有價值資料。常見策略如 LRU、LFU、TTL-based eviction，各自偏好不同存取型態。&lt;/p>
&lt;p>策略選擇重點在流量形狀，演算法名稱是次要的：高重複讀取場景偏向保留高頻資料；大量一次性讀取場景需要避免短期噪音擠掉核心 key。快取層若同時承載多種資料，應分 key space 或分叢集管理，避免策略互相干擾。&lt;/p>
&lt;h2 id="hot--cold-data-的容量節奏">hot / cold data 的容量節奏&lt;/h2>
&lt;p>hot data 與 cold data 的差異不只在存取次數，也在回源成本與業務風險。熱資料 miss 會直接放大來源壓力，冷資料 miss 多半只影響單次延遲。容量規劃要先保護熱資料，再決定冷資料淘汰節奏。&lt;/p>
&lt;p>在促銷或重大活動期間，流量分布常快速改變。TTL 與 eviction 需要具備活動模式：預熱核心 key、分散過期時間、限制單批失效，讓來源系統不被同時回源壓垮。&lt;/p>
&lt;h2 id="分層快取的容量跟成本曲線">分層快取的容量跟成本曲線&lt;/h2>
&lt;p>當熱資料集合超過 DRAM 經濟範圍、單層快取會同時遇到成本跟命中率瓶頸、要把 cache 結構擴展到分層管理。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo&lt;/a> — Meta 把快取結構從 DRAM-only 擴展到 DRAM + flash 分層、改善容量跟成本平衡。當「全部熱資料塞 DRAM」變太貴、把次熱資料推到 flash、保留 DRAM 給最熱的子集。&lt;/p>
&lt;p>&lt;strong>分層快取的相對特性&lt;/strong>（具體 size / latency / cost 視硬體配置跟業務 workload）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>L1 (DRAM)&lt;/strong>：容量最小、延遲最低、單位成本最高、放最熱的子集 — Meta CacheLib 用這層保留熱度最高的 working set&lt;/li>
&lt;li>&lt;strong>L2 (flash / NVMe)&lt;/strong>：容量比 L1 大、延遲比 L1 高、單位成本比 L1 低 — Meta Kangaroo 在這層處理次熱資料&lt;/li>
&lt;li>&lt;strong>L3 (持久 KV)&lt;/strong>：容量最大、延遲最高、單位成本最低、放冷資料跟 fallback&lt;/li>
&lt;/ul>
&lt;p>落層策略要看 &lt;em>資料熱度分布&lt;/em>。Zipfian 分布（80/20 法則）下、L1 放最熱 20% 就能命中大部分；如果分布更平、要把 L1 擴大或接受更低命中率。具體 L1 / L2 大小比例要實測 workload 才能定。&lt;/p>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve&lt;/a> — edge cache 跟 persistent reserve 的分層、長尾資料用 reserve 接住、降低 origin 回源。這是 &lt;em>同類設計思維&lt;/em> 在 CDN 場景的應用、但分層語意不同（edge cache 是地理分散的、Meta 分層是垂直記憶體 / flash 層）— 兩者都用「冷熱分離降低總成本」、實作機制差異需依場景區分。&lt;/p>
&lt;p>&lt;strong>Eviction 跟回補延遲要納入共同指標&lt;/strong>：分層 cache 的訊號不只看 L1 命中率、要看 L1 evict 到 L2 的速率、L2 回補到 L1 的延遲、L3 回源到 L2 的尾巴延遲。混合 metric 才能判斷分層策略是否健康。&lt;/p>
&lt;p>判讀重點：分層 cache 屬規模觸發的設計、要從 working set 大小判斷。Working set 在 DRAM 經濟範圍內、單層即可；working set 顯著超過 DRAM 容量、需分層讓 DRAM 集中放最熱子集、其餘走 flash 或更下層。&lt;/p></description><content:encoded><![CDATA[<p>存活時間與淘汰策略（TTL and eviction）的核心責任是把快取資源分配成可預期策略。TTL 決定資料可存活多久，eviction 決定容量壓力下誰先被移除；兩者共同定義快取的新鮮度、命中率與回源風險。</p>
<h2 id="ttl-是新鮮度預算">TTL 是新鮮度預算</h2>
<p><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 是資料類型的新鮮度預算，用單一時間常數理解它會漏掉關鍵差異。商品描述、推薦列表、活動文案可容忍較長 TTL；價格、庫存、配額、權限則需要更短 TTL 或事件失效。</p>
<p>TTL 設計要連到業務代價。可容忍舊資料的欄位可用長 TTL 降回源壓力；不可容忍錯誤結果的欄位要搭配事件失效與版本控制，讓 TTL 只作為保底機制。</p>
<h2 id="eviction-是容量分流策略">eviction 是容量分流策略</h2>
<p><a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 的責任是當記憶體不足時，優先保留最有價值資料。常見策略如 LRU、LFU、TTL-based eviction，各自偏好不同存取型態。</p>
<p>策略選擇重點在流量形狀，演算法名稱是次要的：高重複讀取場景偏向保留高頻資料；大量一次性讀取場景需要避免短期噪音擠掉核心 key。快取層若同時承載多種資料，應分 key space 或分叢集管理，避免策略互相干擾。</p>
<h2 id="hot--cold-data-的容量節奏">hot / cold data 的容量節奏</h2>
<p>hot data 與 cold data 的差異不只在存取次數，也在回源成本與業務風險。熱資料 miss 會直接放大來源壓力，冷資料 miss 多半只影響單次延遲。容量規劃要先保護熱資料，再決定冷資料淘汰節奏。</p>
<p>在促銷或重大活動期間，流量分布常快速改變。TTL 與 eviction 需要具備活動模式：預熱核心 key、分散過期時間、限制單批失效，讓來源系統不被同時回源壓垮。</p>
<h2 id="分層快取的容量跟成本曲線">分層快取的容量跟成本曲線</h2>
<p>當熱資料集合超過 DRAM 經濟範圍、單層快取會同時遇到成本跟命中率瓶頸、要把 cache 結構擴展到分層管理。</p>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib / Kangaroo</a> — Meta 把快取結構從 DRAM-only 擴展到 DRAM + flash 分層、改善容量跟成本平衡。當「全部熱資料塞 DRAM」變太貴、把次熱資料推到 flash、保留 DRAM 給最熱的子集。</p>
<p><strong>分層快取的相對特性</strong>（具體 size / latency / cost 視硬體配置跟業務 workload）：</p>
<ul>
<li><strong>L1 (DRAM)</strong>：容量最小、延遲最低、單位成本最高、放最熱的子集 — Meta CacheLib 用這層保留熱度最高的 working set</li>
<li><strong>L2 (flash / NVMe)</strong>：容量比 L1 大、延遲比 L1 高、單位成本比 L1 低 — Meta Kangaroo 在這層處理次熱資料</li>
<li><strong>L3 (持久 KV)</strong>：容量最大、延遲最高、單位成本最低、放冷資料跟 fallback</li>
</ul>
<p>落層策略要看 <em>資料熱度分布</em>。Zipfian 分布（80/20 法則）下、L1 放最熱 20% 就能命中大部分；如果分布更平、要把 L1 擴大或接受更低命中率。具體 L1 / L2 大小比例要實測 workload 才能定。</p>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a> — edge cache 跟 persistent reserve 的分層、長尾資料用 reserve 接住、降低 origin 回源。這是 <em>同類設計思維</em> 在 CDN 場景的應用、但分層語意不同（edge cache 是地理分散的、Meta 分層是垂直記憶體 / flash 層）— 兩者都用「冷熱分離降低總成本」、實作機制差異需依場景區分。</p>
<p><strong>Eviction 跟回補延遲要納入共同指標</strong>：分層 cache 的訊號不只看 L1 命中率、要看 L1 evict 到 L2 的速率、L2 回補到 L1 的延遲、L3 回源到 L2 的尾巴延遲。混合 metric 才能判斷分層策略是否健康。</p>
<p>判讀重點：分層 cache 屬規模觸發的設計、要從 working set 大小判斷。Working set 在 DRAM 經濟範圍內、單層即可；working set 顯著超過 DRAM 容量、需分層讓 DRAM 集中放最熱子集、其餘走 flash 或更下層。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>eviction rate 持續上升</td>
          <td>容量不足或 key/value 體積失控</td>
          <td>調整策略、拆分 key space、補容量</td>
      </tr>
      <tr>
          <td>hit rate 下降且 origin QPS 同步上升</td>
          <td>TTL 設定過短或過期同步爆發</td>
          <td>拉長部分 TTL、加入 jitter、分批更新</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a> 事件上升</td>
          <td>TTL 過長或失效機制不足</td>
          <td>縮短關鍵欄位 TTL、補事件失效</td>
      </tr>
      <tr>
          <td>熱門 key 在尖峰時段頻繁 miss</td>
          <td>熱資料未被優先保留</td>
          <td>預熱 hot set、調整 eviction 權重</td>
      </tr>
      <tr>
          <td>記憶體穩定但業務錯誤增加</td>
          <td>值語意失真，非容量問題</td>
          <td>檢查序列化版本、補新鮮度監控與驗證</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把 TTL 統一設定成同一數值，會掩蓋資料語意差異。快取策略應反映資料的重要性與可容忍延遲，而不是單一預設。</p>
<p>把 eviction 視為平台預設值即可，也常導致壓力失真。策略與流量形狀不對齊時，命中率看似可接受，來源系統仍可能在尖峰被回源壓垮。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>TTL/eviction 的容量節奏可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a> 回寫。先看事件中的過期同步與回源尖峰，再回到本章檢查 TTL 分布、淘汰策略與熱資料保護是否同時成立。
這個案例主要支撐的是「容量淘汰與過期波形」判讀，不直接支撐資料庫交易切分或部署切流策略；若事件核心在交易提交或 rollout 批次，應轉到 1.3 或 5.2。</p>
<p>當 eviction 上升但命中率未明顯下降時，先補 value size 與 key 分布監控，再把量測定義回寫到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 Telemetry Data Quality</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>TTL 與 eviction 設計會直接影響觀測、驗證與事故處理。</p>
<ol>
<li>與 2.2 的交接：讀寫失效流程落在 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside</a>。</li>
<li>與 4.17 的交接：新鮮度與容量訊號進入 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.20 的交接：尖峰演練與停損邊界進入 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 8.22 的交接：容量失配與快取事故教訓回寫 <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要把 TTL/eviction 放進失效流程，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要看容量與策略失配案例，接著讀 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>。</p>
]]></content:encoded></item><item><title>2.C4 Meta：CacheLib / Kangaroo 分層快取</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/</guid><description>&lt;p>這個案例的核心責任是說明快取容量壓力升高後，策略會從單層記憶體轉向分層管理。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta 透過 CacheLib 與 Kangaroo 把快取結構擴展到記憶體與快閃分層，改善容量與成本平衡。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當熱門資料集合超過 DRAM 經濟範圍時，單層快取會同時遇到成本與命中率瓶頸。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>定義不同資料熱度的落層策略。&lt;/li>
&lt;li>把 eviction 與回補延遲納入共同指標。&lt;/li>
&lt;li>驗證分層後 tail latency 與成本曲線。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity/cost&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2021/04/09/core-data/cachelib/">CacheLib and Kangaroo&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取容量壓力升高後，策略會從單層記憶體轉向分層管理。</p>
<h2 id="觀察">觀察</h2>
<p>Meta 透過 CacheLib 與 Kangaroo 把快取結構擴展到記憶體與快閃分層，改善容量與成本平衡。</p>
<h2 id="判讀">判讀</h2>
<p>當熱門資料集合超過 DRAM 經濟範圍時，單層快取會同時遇到成本與命中率瓶頸。</p>
<h2 id="策略">策略</h2>
<ol>
<li>定義不同資料熱度的落層策略。</li>
<li>把 eviction 與回補延遲納入共同指標。</li>
<li>驗證分層後 tail latency 與成本曲線。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL/eviction</a> 與 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9 capacity/cost</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2021/04/09/core-data/cachelib/">CacheLib and Kangaroo</a></li>
</ul>
]]></content:encoded></item><item><title>DragonflyDB</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/</guid><description>&lt;p>DragonflyDB 是 C++ 重寫的 in-memory store、承擔三個責任：Redis / Memcached protocol 相容（drop-in 替換）、shared-nothing 多核架構（充分利用 CPU）、高 memory efficiency。設計取捨偏向「協議相容但效能大幅提升」、宣稱比 Redis 高 25 倍 throughput。授權從 Apache 2.0 改 BSL（Business Source License）、商業使用有限制。&lt;/p>
&lt;p>對「需要極高 single-instance throughput、多核機器希望充分利用 CPU、Redis drop-in 但要 scale up 而非 out」這條路徑、DragonflyDB 是值得評估的替代。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 DragonflyDB、用 redis-cli 驗證 protocol 相容&lt;/li>
&lt;li>評估從 Redis 遷移的相容性風險（unsupported commands）&lt;/li>
&lt;li>看懂 shared-nothing 多核架構跟 Redis I/O thread 的差異&lt;/li>
&lt;li>評估 BSL 授權對你的商業使用影響&lt;/li>
&lt;li>區分 DragonflyDB 跟 Redis Cluster / Garnet / KeyDB 的選用判讀&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-dragonflydb-跑起來">最短路徑：5 分鐘把 DragonflyDB 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 DragonflyDB（thread 數預設 = CPU 核數、自動多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name dragonfly -p 6379:6379 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> docker.dragonflydb.io/dragonflydb/dragonfly
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 redis-cli 驗證（wire-protocol 相容、直接用 redis-cli）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">redis-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">redis-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本與多核：DragonflyDB 回報相容的 redis_version + 自身版本 + thread 數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|dragonfly_version|thread_count|multiplexing_api&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.4.0 ← client library 以此判斷相容性&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># dragonfly_version:df-v1.39.0 ← DragonflyDB 自身版本&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># thread_count:8 ← 自動對齊 CPU 核數（shared-nothing 多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="c1"># multiplexing_api:epoll&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>第三步是 DragonflyDB 跟 Redis 的核心差異證據：&lt;code>thread_count&lt;/code> 自動對齊 CPU 核數、每個 thread 管自己的 partition（shared-nothing），這是它高吞吐的來源；&lt;code>redis_version:7.4.0&lt;/code> 讓既有 Redis client 直接相容、無需改 code。實機驗證於 dragonfly df-v1.39.0、最後檢查日 2026-06-16。實際遷移評估見 &lt;a href="#redis-%e7%9b%b8%e5%ae%b9%e9%82%8a%e7%95%8c">Redis 相容邊界&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>直接用 redis-cli（DragonflyDB 100% wire-protocol 相容）&lt;/li>
&lt;li>所有 Redis client library 自動相容&lt;/li>
&lt;li>沒有 dragonfly-cli、用 INFO 命令確認 server type&lt;/li>
&lt;/ul>
&lt;h3 id="redis-相容邊界">Redis 相容邊界&lt;/h3>
&lt;p>DragonflyDB 相容大多數 Redis commands、但部分行為差異。子議題：&lt;/p></description><content:encoded><![CDATA[<p>DragonflyDB 是 C++ 重寫的 in-memory store、承擔三個責任：Redis / Memcached protocol 相容（drop-in 替換）、shared-nothing 多核架構（充分利用 CPU）、高 memory efficiency。設計取捨偏向「協議相容但效能大幅提升」、宣稱比 Redis 高 25 倍 throughput。授權從 Apache 2.0 改 BSL（Business Source License）、商業使用有限制。</p>
<p>對「需要極高 single-instance throughput、多核機器希望充分利用 CPU、Redis drop-in 但要 scale up 而非 out」這條路徑、DragonflyDB 是值得評估的替代。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 DragonflyDB、用 redis-cli 驗證 protocol 相容</li>
<li>評估從 Redis 遷移的相容性風險（unsupported commands）</li>
<li>看懂 shared-nothing 多核架構跟 Redis I/O thread 的差異</li>
<li>評估 BSL 授權對你的商業使用影響</li>
<li>區分 DragonflyDB 跟 Redis Cluster / Garnet / KeyDB 的選用判讀</li>
</ol>
<h2 id="最短路徑5-分鐘把-dragonflydb-跑起來">最短路徑：5 分鐘把 DragonflyDB 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 DragonflyDB（thread 數預設 = CPU 核數、自動多核）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name dragonfly -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. 用 redis-cli 驗證（wire-protocol 相容、直接用 redis-cli）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli SET foo bar    <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli GET foo        <span class="c1"># → bar</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. 確認版本與多核：DragonflyDB 回報相容的 redis_version + 自身版本 + thread 數</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|dragonfly_version|thread_count|multiplexing_api&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># redis_version:7.4.0          ← client library 以此判斷相容性</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0 ← DragonflyDB 自身版本</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># thread_count:8               ← 自動對齊 CPU 核數（shared-nothing 多核）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># multiplexing_api:epoll</span></span></span></code></pre></div><p>第三步是 DragonflyDB 跟 Redis 的核心差異證據：<code>thread_count</code> 自動對齊 CPU 核數、每個 thread 管自己的 partition（shared-nothing），這是它高吞吐的來源；<code>redis_version:7.4.0</code> 讓既有 Redis client 直接相容、無需改 code。實機驗證於 dragonfly df-v1.39.0、最後檢查日 2026-06-16。實際遷移評估見 <a href="#redis-%e7%9b%b8%e5%ae%b9%e9%82%8a%e7%95%8c">Redis 相容邊界</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>直接用 redis-cli（DragonflyDB 100% wire-protocol 相容）</li>
<li>所有 Redis client library 自動相容</li>
<li>沒有 dragonfly-cli、用 INFO 命令確認 server type</li>
</ul>
<h3 id="redis-相容邊界">Redis 相容邊界</h3>
<p>DragonflyDB 相容大多數 Redis commands、但部分行為差異。子議題：</p>
<ul>
<li>支援：Core data types / commands / persistence / pub-sub / transactions</li>
<li>注意：部分 Module 不支援（RedisJSON 有自家版、RedisSearch 沒有）</li>
<li>注意：Lua scripting 支援但效能取捨不同</li>
<li>限制：Cluster mode 採 single-instance scale-up、沒有 Redis Cluster mode（單 instance 已能處理 Redis Cluster 規模）</li>
</ul>
<p>對應指令：<code>INFO server</code> 確認 dragonfly version + 配置。</p>
<h3 id="配置與調優">配置與調優</h3>
<p>子議題：</p>
<ul>
<li><code>--threads</code>：thread 數量、預設 CPU core 數</li>
<li><code>--maxmemory</code>：memory limit、行為跟 Redis 類似</li>
<li><code>--cache_mode</code>：傳統 cache 模式 vs DragonflyDB 預設模式</li>
<li><code>--snapshot_cron</code>：snapshot 策略</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="shared-nothing-多核架構">Shared-nothing 多核架構</h3>
<p>子議題：</p>
<ul>
<li>每個 thread 管自己的 partition、no shared state</li>
<li>VLL（Very Lightweight Lock）取代 Redis 的 single-thread model</li>
<li>Hash 分到不同 thread、靠 epoll 跟 io_uring 做 I/O</li>
<li>跟 Redis I/O threads 的對比：Redis 仍 single main thread、只 I/O 多線；DragonflyDB 完全多線</li>
</ul>
<h3 id="memory-efficiency">Memory efficiency</h3>
<p>子議題：</p>
<ul>
<li>用 dashtable（DragonflyDB 自製 hash table）取代 Redis dict</li>
<li>Snapshot 用 fork-less 機制、避免大記憶體 fork 開銷</li>
<li>同樣 dataset 通常比 Redis 省 20-40% memory（依資料形狀）</li>
</ul>
<h3 id="bsl-授權影響">BSL 授權影響</h3>
<p>子議題：</p>
<ul>
<li>BSL（Business Source License）：商業使用受限、4 年後轉 Apache 2.0</li>
<li>限制：不可作為 managed DragonflyDB service 對外提供</li>
<li>內部使用無限制（多數企業場景）</li>
<li>對 SaaS 供應商：要審慎評估</li>
</ul>
<h3 id="跟-keydb--garnet-的對比">跟 KeyDB / Garnet 的對比</h3>
<p>子議題：</p>
<ul>
<li><strong>KeyDB</strong>：Redis fork、multi-threaded、Snap 收購後相對停滯</li>
<li><strong>Garnet</strong>（Microsoft）：研究用、極高 throughput、生態淺</li>
<li><strong>DragonflyDB</strong>：商業化最積極、生態最活躍</li>
</ul>
<h3 id="scale-up-vs-scale-out">Scale-up vs Scale-out</h3>
<p>子議題：</p>
<ul>
<li>DragonflyDB 哲學：single instance 撐到很大規模（廠商宣稱 1TB+ memory / 6.4M QPS）</li>
<li>Redis 哲學：single instance 有上限、靠 Cluster sharding</li>
<li>何時 scale-up 不夠：跨 region / 跨 AZ HA 需求 → 仍需 replica / sentinel</li>
</ul>
<h3 id="從-redis-遷移">從 Redis 遷移</h3>
<p>子議題：</p>
<ul>
<li>評估 module 使用：列出當前 modules、確認 DragonflyDB 對應</li>
<li>評估 Cluster mode 使用：DragonflyDB 不支援 Cluster mode、要評估能否回到 single instance</li>
<li>遷移路徑：replica 模式雙寫 / 直接 cutover</li>
<li>對應 BSL 授權影響評估</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="performance-不如預期">Performance 不如預期</h3>
<p>操作原則：先確認 thread 數對齊 CPU core、再看 memory pressure。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;dragonfly_version|thread_count&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># thread_count:8                ← 對齊 CPU 核數才能發揮多核</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli INFO memory <span class="p">|</span> grep -E <span class="s2">&#34;used_memory:|maxmemory:&#34;</span></span></span></code></pre></div><p>判讀：thread &lt; core → 沒充分利用 CPU；memory &gt; 50% maxmemory → 影響 throughput。</p>
<h3 id="command-不支援">Command 不支援</h3>
<p>操作原則：DragonflyDB 不支援全部 Redis commands、看 dragonflydb.io/docs/api/redis 確認。</p>
<p>判讀路徑：client error「unknown command」→ 確認 DragonflyDB 對應實作狀態。</p>
<h3 id="cluster-mode-client-連不上">Cluster mode client 連不上</h3>
<p>操作原則：DragonflyDB 不支援 Redis Cluster mode、若 client 配置 cluster mode 會連不上。判讀：改回 standalone client config。</p>
<h3 id="module-不可用">Module 不可用</h3>
<p>對應 KeyDB / Garnet 的對照思路：DragonflyDB 自家 modules 偏少、Redis Stack modules 大多沒有 fork。</p>
<h3 id="bsl-授權商業使用問題">BSL 授權商業使用問題</h3>
<p>操作原則：商業使用前審 license terms、若是 managed service 對外提供、需聯絡 DragonflyDB 取得商業 license。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 Redis Cluster mode</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要 OSI 認可開源授權</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>需要 Redis Stack 完整 modules</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></td>
      </tr>
      <tr>
          <td>純 KV 不需 data types</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（無 Dragonfly managed）</td>
      </tr>
      <tr>
          <td>Multi-threaded Redis fork</td>
          <td>KeyDB（停滯中）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>DragonflyDB internal 架構細節（dashtable、VLL 等）</li>
<li>BSL 授權法律解讀（請諮詢律師）</li>
<li>各語言 client 完整對應表</li>
<li>詳細 benchmark methodology</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例沿用-redis-compatible-同源案例--待補-dragonflydb-specific-case">直接相關案例（沿用 Redis-compatible 同源案例 + 待補 DragonflyDB-specific case）</h3>
<p>DragonflyDB 2022 年開源、wire-protocol 與 Redis 相容、Redis 上的 cache pattern 案例可作為框架參考。Production case 仍累積中。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 DragonflyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 模式在 DragonflyDB 上行為一致、單 instance 多核可承接更大 throughput</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移 client-side 實作、DragonflyDB 跟 Redis 共用 API、遷移路徑相同</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 DragonflyDB-specific 案例</strong>：早期採用者 benchmark 報告、從 Redis Cluster 收回 single-instance 的遷移案例、BSL 授權實際商業使用評估、multi-core 加速效果的 production 實測。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 DragonflyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>DragonflyDB 擅長 scale-up、中大型 single instance 取代 Redis Cluster 是核心賣點</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter 通用、DragonflyDB 行為跟 Redis 一致、多核擴展不會消除 stampede 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>分層 cache 議題對照、DragonflyDB 強調 memory efficiency 取代 flash tier 的部分需求</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>一致性治理框架通用、但 DragonflyDB 無 Cluster mode、shard move 議題不同（單 instance scope）</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
<li>回退路徑：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/" data-link-title="DragonflyDB → Redis / Valkey：回退到標準生態的遷移路徑" data-link-desc="從 DragonflyDB 遷回 Redis 或 Valkey，處理 snapshotting → RDB/AOF 差異、HA 架構切換與 Cluster mode 重建的階段化流程">DragonflyDB → Redis/Valkey</a></li>
</ul>
]]></content:encoded></item><item><title>2.4 distributed lock 與租約</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">分散式鎖（distributed lock）&lt;/a>的核心責任是協調跨節點互斥，避免同一資源被重複處理。它解的是協調一致性問題；正式狀態一致性仍由交易邊界或版本控制承擔。&lt;/p>
&lt;h2 id="鎖與租約">鎖與租約&lt;/h2>
&lt;p>分散式鎖通常採租約語意：持鎖者在租約有效期內擁有操作權，租約到期後鎖自動釋放、需重新競爭。租約的存在是為了處理「持鎖者掛掉但沒釋放鎖」這個分散式系統無法避免的情況——沒有租約，一個 crash 的節點會讓鎖永遠卡住。代價是引入時鐘漂移、網路延遲與續租失敗這幾個新風險。&lt;/p>
&lt;p>在 Redis 上，取鎖是一個原子命令：&lt;code>SET lock:order:42 &amp;lt;token&amp;gt; NX PX 30000&lt;/code>。&lt;code>NX&lt;/code> 保證只有 key 不存在時才寫入，這讓「檢查鎖是否被持有」與「取得鎖」變成單一原子操作，避免兩個節點同時判斷「沒人持鎖」後都寫入。&lt;code>PX 30000&lt;/code> 設定 30 秒租約，持鎖者 crash 時鎖會在租約到期後自動消失。&lt;code>&amp;lt;token&amp;gt;&lt;/code> 是每個持鎖者產生的唯一隨機值，它的作用在釋放階段才顯現。&lt;/p>
&lt;p>釋放鎖不能用單純的 &lt;code>DEL lock:order:42&lt;/code>，因為這會誤刪別人的鎖。考慮這個時序：節點 A 取得鎖、處理超過 30 秒、租約到期自動釋放、節點 B 取得同一把鎖；此時 A 終於處理完、執行 &lt;code>DEL&lt;/code>，刪掉的是 B 的鎖。正確的釋放是「比對 token 相同才刪」，而這個 check-and-delete 必須原子，用 Lua script 達成：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-lua" data-lang="lua">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="kr">if&lt;/span> &lt;span class="n">redis.call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;GET&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">KEYS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span> &lt;span class="o">==&lt;/span> &lt;span class="n">ARGV&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">]&lt;/span> &lt;span class="kr">then&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> &lt;span class="kr">return&lt;/span> &lt;span class="n">redis.call&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s2">&amp;#34;DEL&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span> &lt;span class="n">KEYS&lt;/span>&lt;span class="p">[&lt;/span>&lt;span class="mi">1&lt;/span>&lt;span class="p">])&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="kr">else&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="kr">return&lt;/span> &lt;span class="mi">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="kr">end&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>ARGV[1]&lt;/code> 帶入持鎖者自己的 token，只有 token 吻合才刪。這把「釋放鎖」從一個盲目的刪除，變成「確認我仍是持鎖者後才釋放」的條件操作。&lt;/p>
&lt;p>租約長度要對著任務耗時分布校準，而非拍一個固定值：租約要明顯長於正常任務的 P99 耗時，避免工作還沒做完租約就過期、引發雙持鎖；但也不能長到讓 crash 的持鎖者把鎖卡住太久。兩個方向夾出一個區間，長尾工作再用 watchdog 補足。&lt;/p>
&lt;p>續租策略要明確：何時續租、續租失敗如何降級。長時間工作會用 watchdog 在租約過半（約 T/2）時用 &lt;code>PEXPIRE&lt;/code> 延長租約，讓鎖跟著工作存活；但 watchdog 也意味著鎖可能被無限延長，需要設一個絕對上限（例如業務超時的數倍）避免一個卡住的工作永久佔用鎖。若只依賴「拿到鎖就安全」的假設、不處理續租失敗，異常時容易產生重複副作用。&lt;/p>
&lt;h2 id="split-brain-與-fencing">split brain 與 fencing&lt;/h2>
&lt;p>split brain 常見於網路分割或 process 暫停（GC stop-the-world、容器被搶占）後恢復。核心問題是租約：節點 A 取得鎖後發生一次長 GC 暫停，暫停期間租約到期、鎖被節點 B 取得，A 從暫停中醒來時仍「以為」自己持有鎖，於是 A 與 B 同時對下游寫入，互斥語意失效。這是基於租約的鎖在自身層無法消除的時序窗口，解法要往下游推——讓擁有正式狀態的那一層成為最終仲裁者，而非期望鎖本身堵住這個窗口。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/fencing-token/" data-link-title="Fencing Token" data-link-desc="說明用單調遞增的 token 讓下游拒絕過期持鎖者的寫入，把互斥正確性下沉到資料層">fencing token&lt;/a> 的責任是把這個問題推到下游解決：每次取鎖時發一個單調遞增的 token，持鎖者對下游的每個寫入都帶上這個 token，下游記住「見過的最大 token」並拒絕比它小的寫入。回到上面的時序，A 帶 token 33、B 帶 token 34，當 A 醒來用 token 33 寫入時，下游已經接受過 34，於是拒絕 33。token 的單調遞增可以用 Redis 原子計數器（&lt;code>INCR fence:order:42&lt;/code>）或鎖服務自己維護的自增序號實作，關鍵是取鎖動作本身要保證拿到的序號嚴格遞增。fencing token 讓下游成為仲裁者，鎖只負責減少競爭、不再是唯一的正確性保證。&lt;/p>
&lt;p>若下游無法驗證 fencing token（例如下游是不支援條件寫入的第三方 API），distributed lock 的保護能力會明顯下降——它只能降低衝突機率，無法消除雙寫。這時更穩定的做法是改成資料版本控制或條件更新（&lt;code>WATCH&lt;/code>/&lt;code>MULTI&lt;/code> 的樂觀鎖、資料庫的 &lt;code>UPDATE ... WHERE version = ?&lt;/code>），把互斥下沉到擁有正式狀態的那一層。&lt;/p>
&lt;h2 id="redlock-與單節點的取捨">Redlock 與單節點的取捨&lt;/h2>
&lt;p>單節點 Redis 鎖有一個可用性缺口：持鎖期間 Redis 主節點故障、failover 到還沒同步該鎖的副本時，新主節點上這把鎖不存在，另一個節點能立刻取得，造成雙持鎖。Redis 作者提出的 Redlock 演算法用多個獨立 master（通常 5 個）解這個問題：向所有節點取鎖，取得多數（3/5）且總耗時在租約內才算成功，藉冗餘避免單點 failover 造成的鎖遺失。&lt;/p>
&lt;p>Redlock 是否真的更安全有公開爭論。Martin Kleppmann 的批評指出，Redlock 依賴各節點時鐘不發生大幅跳動，而 GC 暫停與時鐘校正這類事件仍會讓持鎖者醒來時鎖已失效；更進一步，若 NTP 時鐘跳躍發生在取鎖過程中，各節點對租約是否有效的判斷本身就可能出錯，Redlock 賴以成立的多數決計數因此無法可靠排除雙持鎖。也就是說，Redlock 提升了「鎖不會因單節點故障而遺失」的可用性，但沒有解決「持鎖者暫停導致的 split brain」，後者仍需 fencing token。判讀因此落在：鎖只是效率優化（偶爾雙跑代價可接受）時，單節點 Redis 鎖足夠且運維簡單；鎖牽涉正確性（雙跑會造成金錢或資料損壞）時，無論單節點還是 Redlock 都不足以單獨成立，必須有 fencing token 或下游條件寫入兜底。&lt;/p>
&lt;h2 id="何時使用何時轉向">何時使用、何時轉向&lt;/h2>
&lt;p>distributed lock 在「偶爾失效的代價可控」的場景是一個效率優化工具，降低重複工作的機率而非保證互斥。符合這個特徵的場景包括排程任務避免重複執行、單資源批次工作協調、短期臨界區互斥。以 cron job 為例，偶爾被兩個節點同時觸發時，若任務本身 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotent&lt;/a>，重複執行只是浪費資源而非產生錯誤結果，鎖把這類浪費的機率壓低就足夠。&lt;/p>
&lt;p>讀取路徑上避免 cache miss 風暴的 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight&lt;/a> 互斥也屬這一類，但租約特性不同：熱門 key 失效時用一把短鎖讓單一請求回源重建快取、其餘請求等結果，偶爾多跑一次回源的代價可控。它的鎖租約通常很短（一次回源的時間），競爭集中在少數熱門 key，與批次任務的長租約、低競爭剖面相反，校準時要分開看。&lt;/p>
&lt;p>高價值交易資料更新則相反，優先使用資料庫交易與唯一約束，將鎖作為輔助而非核心一致性機制。扣款、出貨、配額扣減這類操作，正確性不能依賴「鎖沒失效」這個無法保證的前提，而要靠資料層的唯一約束或版本檢查讓重複操作在最後一刻被擋下。&lt;/p></description><content:encoded><![CDATA[<p><a href="/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">分散式鎖（distributed lock）</a>的核心責任是協調跨節點互斥，避免同一資源被重複處理。它解的是協調一致性問題；正式狀態一致性仍由交易邊界或版本控制承擔。</p>
<h2 id="鎖與租約">鎖與租約</h2>
<p>分散式鎖通常採租約語意：持鎖者在租約有效期內擁有操作權，租約到期後鎖自動釋放、需重新競爭。租約的存在是為了處理「持鎖者掛掉但沒釋放鎖」這個分散式系統無法避免的情況——沒有租約，一個 crash 的節點會讓鎖永遠卡住。代價是引入時鐘漂移、網路延遲與續租失敗這幾個新風險。</p>
<p>在 Redis 上，取鎖是一個原子命令：<code>SET lock:order:42 &lt;token&gt; NX PX 30000</code>。<code>NX</code> 保證只有 key 不存在時才寫入，這讓「檢查鎖是否被持有」與「取得鎖」變成單一原子操作，避免兩個節點同時判斷「沒人持鎖」後都寫入。<code>PX 30000</code> 設定 30 秒租約，持鎖者 crash 時鎖會在租約到期後自動消失。<code>&lt;token&gt;</code> 是每個持鎖者產生的唯一隨機值，它的作用在釋放階段才顯現。</p>
<p>釋放鎖不能用單純的 <code>DEL lock:order:42</code>，因為這會誤刪別人的鎖。考慮這個時序：節點 A 取得鎖、處理超過 30 秒、租約到期自動釋放、節點 B 取得同一把鎖；此時 A 終於處理完、執行 <code>DEL</code>，刪掉的是 B 的鎖。正確的釋放是「比對 token 相同才刪」，而這個 check-and-delete 必須原子，用 Lua script 達成：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-lua" data-lang="lua"><span class="line"><span class="ln">1</span><span class="cl"><span class="kr">if</span> <span class="n">redis.call</span><span class="p">(</span><span class="s2">&#34;GET&#34;</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span> <span class="o">==</span> <span class="n">ARGV</span><span class="p">[</span><span class="mi">1</span><span class="p">]</span> <span class="kr">then</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="kr">return</span> <span class="n">redis.call</span><span class="p">(</span><span class="s2">&#34;DEL&#34;</span><span class="p">,</span> <span class="n">KEYS</span><span class="p">[</span><span class="mi">1</span><span class="p">])</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="kr">else</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="kr">return</span> <span class="mi">0</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="kr">end</span></span></span></code></pre></div><p><code>ARGV[1]</code> 帶入持鎖者自己的 token，只有 token 吻合才刪。這把「釋放鎖」從一個盲目的刪除，變成「確認我仍是持鎖者後才釋放」的條件操作。</p>
<p>租約長度要對著任務耗時分布校準，而非拍一個固定值：租約要明顯長於正常任務的 P99 耗時，避免工作還沒做完租約就過期、引發雙持鎖；但也不能長到讓 crash 的持鎖者把鎖卡住太久。兩個方向夾出一個區間，長尾工作再用 watchdog 補足。</p>
<p>續租策略要明確：何時續租、續租失敗如何降級。長時間工作會用 watchdog 在租約過半（約 T/2）時用 <code>PEXPIRE</code> 延長租約，讓鎖跟著工作存活；但 watchdog 也意味著鎖可能被無限延長，需要設一個絕對上限（例如業務超時的數倍）避免一個卡住的工作永久佔用鎖。若只依賴「拿到鎖就安全」的假設、不處理續租失敗，異常時容易產生重複副作用。</p>
<h2 id="split-brain-與-fencing">split brain 與 fencing</h2>
<p>split brain 常見於網路分割或 process 暫停（GC stop-the-world、容器被搶占）後恢復。核心問題是租約：節點 A 取得鎖後發生一次長 GC 暫停，暫停期間租約到期、鎖被節點 B 取得，A 從暫停中醒來時仍「以為」自己持有鎖，於是 A 與 B 同時對下游寫入，互斥語意失效。這是基於租約的鎖在自身層無法消除的時序窗口，解法要往下游推——讓擁有正式狀態的那一層成為最終仲裁者，而非期望鎖本身堵住這個窗口。</p>
<p><a href="/blog/backend/knowledge-cards/fencing-token/" data-link-title="Fencing Token" data-link-desc="說明用單調遞增的 token 讓下游拒絕過期持鎖者的寫入，把互斥正確性下沉到資料層">fencing token</a> 的責任是把這個問題推到下游解決：每次取鎖時發一個單調遞增的 token，持鎖者對下游的每個寫入都帶上這個 token，下游記住「見過的最大 token」並拒絕比它小的寫入。回到上面的時序，A 帶 token 33、B 帶 token 34，當 A 醒來用 token 33 寫入時，下游已經接受過 34，於是拒絕 33。token 的單調遞增可以用 Redis 原子計數器（<code>INCR fence:order:42</code>）或鎖服務自己維護的自增序號實作，關鍵是取鎖動作本身要保證拿到的序號嚴格遞增。fencing token 讓下游成為仲裁者，鎖只負責減少競爭、不再是唯一的正確性保證。</p>
<p>若下游無法驗證 fencing token（例如下游是不支援條件寫入的第三方 API），distributed lock 的保護能力會明顯下降——它只能降低衝突機率，無法消除雙寫。這時更穩定的做法是改成資料版本控制或條件更新（<code>WATCH</code>/<code>MULTI</code> 的樂觀鎖、資料庫的 <code>UPDATE ... WHERE version = ?</code>），把互斥下沉到擁有正式狀態的那一層。</p>
<h2 id="redlock-與單節點的取捨">Redlock 與單節點的取捨</h2>
<p>單節點 Redis 鎖有一個可用性缺口：持鎖期間 Redis 主節點故障、failover 到還沒同步該鎖的副本時，新主節點上這把鎖不存在，另一個節點能立刻取得，造成雙持鎖。Redis 作者提出的 Redlock 演算法用多個獨立 master（通常 5 個）解這個問題：向所有節點取鎖，取得多數（3/5）且總耗時在租約內才算成功，藉冗餘避免單點 failover 造成的鎖遺失。</p>
<p>Redlock 是否真的更安全有公開爭論。Martin Kleppmann 的批評指出，Redlock 依賴各節點時鐘不發生大幅跳動，而 GC 暫停與時鐘校正這類事件仍會讓持鎖者醒來時鎖已失效；更進一步，若 NTP 時鐘跳躍發生在取鎖過程中，各節點對租約是否有效的判斷本身就可能出錯，Redlock 賴以成立的多數決計數因此無法可靠排除雙持鎖。也就是說，Redlock 提升了「鎖不會因單節點故障而遺失」的可用性，但沒有解決「持鎖者暫停導致的 split brain」，後者仍需 fencing token。判讀因此落在：鎖只是效率優化（偶爾雙跑代價可接受）時，單節點 Redis 鎖足夠且運維簡單；鎖牽涉正確性（雙跑會造成金錢或資料損壞）時，無論單節點還是 Redlock 都不足以單獨成立，必須有 fencing token 或下游條件寫入兜底。</p>
<h2 id="何時使用何時轉向">何時使用、何時轉向</h2>
<p>distributed lock 在「偶爾失效的代價可控」的場景是一個效率優化工具，降低重複工作的機率而非保證互斥。符合這個特徵的場景包括排程任務避免重複執行、單資源批次工作協調、短期臨界區互斥。以 cron job 為例，偶爾被兩個節點同時觸發時，若任務本身 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotent</a>，重複執行只是浪費資源而非產生錯誤結果，鎖把這類浪費的機率壓低就足夠。</p>
<p>讀取路徑上避免 cache miss 風暴的 <a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight</a> 互斥也屬這一類，但租約特性不同：熱門 key 失效時用一把短鎖讓單一請求回源重建快取、其餘請求等結果，偶爾多跑一次回源的代價可控。它的鎖租約通常很短（一次回源的時間），競爭集中在少數熱門 key，與批次任務的長租約、低競爭剖面相反，校準時要分開看。</p>
<p>高價值交易資料更新則相反，優先使用資料庫交易與唯一約束，將鎖作為輔助而非核心一致性機制。扣款、出貨、配額扣減這類操作，正確性不能依賴「鎖沒失效」這個無法保證的前提，而要靠資料層的唯一約束或版本檢查讓重複操作在最後一刻被擋下。</p>
<p>當鎖競爭成為常態、租約續租頻繁失敗、鎖持有時間與業務耗時高度耦合時，代表模型需要轉向分片、隊列化或版本檢查。鎖競爭高通常是粒度設計問題：把單一全域鎖換成依資源分片的細粒度鎖（<code>lock:order:42</code> 而非 <code>lock:orders</code>），讓不相關的資源互不阻塞。若工作本身就是序列化處理一批項目，改用 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a> 的單一 consumer 語意，比用鎖模擬序列更穩定。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>鎖等待時間持續拉長</td>
          <td>臨界區過大或熱點資源集中</td>
          <td>縮小臨界區、拆分資源粒度</td>
      </tr>
      <tr>
          <td>續租失敗與重入衝突同時上升</td>
          <td>租約時間與工作耗時不匹配</td>
          <td>重設租約、加入 fencing token</td>
      </tr>
      <tr>
          <td>相同任務重複執行率上升</td>
          <td>鎖語意失效或持鎖者判定漂移</td>
          <td>檢查時鐘與網路、補下游去重</td>
      </tr>
      <tr>
          <td>網路抖動時 split brain 事件增加</td>
          <td>鎖系統與下游防護未對位</td>
          <td>補下游版本檢查、限制高風險操作</td>
      </tr>
      <tr>
          <td>鎖系統穩定但業務仍不一致</td>
          <td>問題層級在資料一致性而非協調層</td>
          <td>回到 transaction/constraint 設計</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把分散式鎖當作通用一致性解法，會讓錯誤責任落在錯誤層級。最常見的具體形狀是「用鎖保護寫入、但讀取路徑不過鎖」：寫入互斥成立了，讀取卻仍可能讀到未提交或 stale 的值，不一致沒有被鎖擋住。鎖負責互斥協調，資料正確性要由資料模型與交易邊界保護，讀寫兩端要納入同一套一致性設計、而非只鎖寫端。</p>
<p>用單純的 <code>DEL</code> 釋放鎖，是最容易在程式碼裡漏掉的一個錯誤。租約到期後鎖可能已被別人取得，盲目 <code>DEL</code> 會誤刪他人的鎖、讓互斥瓦解。釋放一律要走 token 比對的條件刪除。</p>
<p>把 Redlock 或多節點鎖當成正確性保證，是第二個誤區。多節點冗餘提升的是「鎖不會因單點故障遺失」的可用性，不是「持鎖者暫停不會造成雙寫」的正確性。需要正確性時，fencing token 或下游條件寫入才是真正的防線，鎖只是減少競爭。</p>
<p>把租約時間固定為常數，也會在流量波動下放大風險。租約太短會在正常工作未完成時就過期、引發雙持鎖；太長則讓 crash 的持鎖者長時間卡住鎖。租約策略需要和任務耗時分布與錯誤模型一起校準，長尾工作要靠 watchdog 續租而非把租約一律設大。</p>
<h2 id="情境回寫">情境回寫</h2>
<p>分散式鎖失效回寫到真實服務時，最常見的形狀是排程任務的重複執行。一個跨多節點部署的對帳 job 用 distributed lock 確保同一批次只有一個節點處理；當持鎖節點發生長暫停、租約到期被另一節點接手，而暫停節點醒來後仍繼續寫入時，同一批對帳被執行兩次。回寫時先判讀鎖失效來自時序漂移、網路分割還是續租策略，再決定防線往哪裡補。</p>
<p>這個形狀支撐的是「互斥語意在異常下失效」的判讀。若任務本身 idempotent，重複執行只是浪費資源；若會產生重複副作用（重複出帳、重複通知），正確性不能靠鎖，要靠下游的 fencing token 或唯一約束。高風險路徑可接到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a> 做故障演練。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.2 的交接：鎖搭配失效策略回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 1.3 的交接：高價值資料一致性回到 <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">transaction 與一致性邊界</a>。</li>
<li>與 6.20 的交接：鎖失效演練與停損條件回到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
<li>與 8.19 的交接：鎖衝突與回退判斷回到 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">Incident Decision Log</a>。</li>
<li>與 2.6 的交接：split brain 與鎖失效的弱點可從威脅建模角度重新盤點，回到 <a href="/blog/backend/02-cache-redis/attacker-view-cache-risks/" data-link-title="2.6 快取威脅建模（Threat Modeling）" data-link-desc="從快取污染、一致性偏移與流量放大風險，盤點 cache/redis 的主要弱點">快取威脅建模</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取層一致性與容量壓力，接著讀 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>。要看鎖語意在事故裡的擴散方式，接著讀 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>。</p>
]]></content:encoded></item><item><title>2.C5 Shopify：Write-through Cache 在高讀流量的實作</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/</guid><description>&lt;p>這個案例的核心責任是把快取從被動補貨模式，轉成資料寫入時即同步更新的模式。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Shopify 在高讀取路徑以 write-through 策略降低 miss 風險，改善熱門資料讀取穩定性。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當 cache miss 成本過高且資料更新可控時，write-through 能降低讀路徑抖動。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把寫入流程與快取更新綁定。&lt;/li>
&lt;li>對失敗寫入設計補償與重試。&lt;/li>
&lt;li>用 hit rate 與 stale rate 檢驗策略收益。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://shopify.engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">How Shop App uses write-through caching&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把快取從被動補貨模式，轉成資料寫入時即同步更新的模式。</p>
<h2 id="觀察">觀察</h2>
<p>Shopify 在高讀取路徑以 write-through 策略降低 miss 風險，改善熱門資料讀取穩定性。</p>
<h2 id="判讀">判讀</h2>
<p>當 cache miss 成本過高且資料更新可控時，write-through 能降低讀路徑抖動。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把寫入流程與快取更新綁定。</li>
<li>對失敗寫入設計補償與重試。</li>
<li>用 hit rate 與 stale rate 檢驗策略收益。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 與 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 release gate</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://shopify.engineering/horizontally-scaling-the-rails-backend-of-shop-app-with-vitess">How Shop App uses write-through caching</a></li>
</ul>
]]></content:encoded></item><item><title>AWS ElastiCache</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/</guid><description>&lt;p>AWS ElastiCache 是 AWS managed cache 服務、承擔三個責任：託管 Redis / Valkey / Memcached engine（無需自管 broker）、自動 failover + 跨 AZ 複製、AWS 生態原生整合（IAM / VPC / CloudWatch / KMS）。設計取捨偏向「把運維責任轉給 AWS、付 managed premium 換可預測 SLA」、AWS 生態下的 cache 預設選擇。2024 起 default engine 從 Redis 改為 Valkey（成本約低 20%）。&lt;/p>
&lt;p>對「AWS 生態服務需要 cache、不想自管 Redis cluster、跨 AZ 高可用」這條路徑、ElastiCache 是首選。本頁先給最短路徑、再展開日常 cluster 管理跟 engine 選擇、最後進階治理（Serverless、MemoryDB 對照）跟排錯。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 AWS CLI 建立 ElastiCache cluster、選擇 engine（Redis / Valkey / Memcached）&lt;/li>
&lt;li>區分 Cluster mode enabled vs disabled 的選用條件&lt;/li>
&lt;li>配置 auto failover、cross-AZ replication、snapshot backup&lt;/li>
&lt;li>評估 ElastiCache Serverless vs node-based 的成本取捨&lt;/li>
&lt;li>區分 ElastiCache 跟 MemoryDB（durable）跟自管 Redis 的定位&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-elasticache-跑起來">最短路徑：5 分鐘把 ElastiCache 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 建立 Valkey replication group（cluster mode disabled、單 primary + 1 replica、Multi-AZ）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">aws elasticache create-replication-group &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --replication-group-id demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --replication-group-description &lt;span class="s2">&amp;#34;demo cache&amp;#34;&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --engine valkey &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --cache-node-type cache.t4g.micro &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --num-cache-clusters &lt;span class="m">2&lt;/span> &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --automatic-failover-enabled &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --multi-az-enabled
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 取得 primary endpoint（建立需數分鐘、status 變 available 才有 endpoint）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">aws elasticache describe-replication-groups &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --replication-group-id demo &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> --query &lt;span class="s2">&amp;#34;ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address&amp;#34;&lt;/span> --output text
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 從 VPC 內（EC2 / Lambda）用 redis-cli 連線（ElastiCache 只在 VPC 內可達）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">redis-cli -h &amp;lt;primary-endpoint&amp;gt; -p &lt;span class="m">6379&lt;/span> PING &lt;span class="c1"># → PONG&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>指令依 &lt;a href="https://docs.aws.amazon.com/cli/latest/reference/elasticache/">AWS ElastiCache CLI 官方文件&lt;/a>、最後檢查日 2026-06-16（managed 服務需 AWS 帳號與 VPC、本機無法 docker 驗證、引數以官方為準）。ElastiCache 端點只在 VPC 內可達、不對公網開放。實際 production 需要評估 cluster mode、節點大小、replica 數、AZ 分布。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="aws-cli-與-console">AWS CLI 與 console&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>AWS ElastiCache 是 AWS managed cache 服務、承擔三個責任：託管 Redis / Valkey / Memcached engine（無需自管 broker）、自動 failover + 跨 AZ 複製、AWS 生態原生整合（IAM / VPC / CloudWatch / KMS）。設計取捨偏向「把運維責任轉給 AWS、付 managed premium 換可預測 SLA」、AWS 生態下的 cache 預設選擇。2024 起 default engine 從 Redis 改為 Valkey（成本約低 20%）。</p>
<p>對「AWS 生態服務需要 cache、不想自管 Redis cluster、跨 AZ 高可用」這條路徑、ElastiCache 是首選。本頁先給最短路徑、再展開日常 cluster 管理跟 engine 選擇、最後進階治理（Serverless、MemoryDB 對照）跟排錯。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 AWS CLI 建立 ElastiCache cluster、選擇 engine（Redis / Valkey / Memcached）</li>
<li>區分 Cluster mode enabled vs disabled 的選用條件</li>
<li>配置 auto failover、cross-AZ replication、snapshot backup</li>
<li>評估 ElastiCache Serverless vs node-based 的成本取捨</li>
<li>區分 ElastiCache 跟 MemoryDB（durable）跟自管 Redis 的定位</li>
</ol>
<h2 id="最短路徑5-分鐘把-elasticache-跑起來">最短路徑：5 分鐘把 ElastiCache 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 建立 Valkey replication group（cluster mode disabled、單 primary + 1 replica、Multi-AZ）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elasticache create-replication-group <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --replication-group-id demo <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --replication-group-description <span class="s2">&#34;demo cache&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine valkey <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --cache-node-type cache.t4g.micro <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --num-cache-clusters <span class="m">2</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --automatic-failover-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --multi-az-enabled
</span></span><span class="line"><span class="ln">10</span><span class="cl">
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># 2. 取得 primary endpoint（建立需數分鐘、status 變 available 才有 endpoint）</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">aws elasticache describe-replication-groups <span class="se">\
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="se"></span>  --replication-group-id demo <span class="se">\
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;ReplicationGroups[0].NodeGroups[0].PrimaryEndpoint.Address&#34;</span> --output text
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 3. 從 VPC 內（EC2 / Lambda）用 redis-cli 連線（ElastiCache 只在 VPC 內可達）</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">redis-cli -h &lt;primary-endpoint&gt; -p <span class="m">6379</span> PING   <span class="c1"># → PONG</span></span></span></code></pre></div><p>指令依 <a href="https://docs.aws.amazon.com/cli/latest/reference/elasticache/">AWS ElastiCache CLI 官方文件</a>、最後檢查日 2026-06-16（managed 服務需 AWS 帳號與 VPC、本機無法 docker 驗證、引數以官方為準）。ElastiCache 端點只在 VPC 內可達、不對公網開放。實際 production 需要評估 cluster mode、節點大小、replica 數、AZ 分布。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="aws-cli-與-console">AWS CLI 與 console</h3>
<p>子議題：</p>
<ul>
<li>CLI 指令對照表（create-cache-cluster / create-replication-group / describe-* / modify-* / delete-*）</li>
<li>Console 操作流程（VPC subnet group / security group / parameter group）</li>
<li>Terraform / CloudFormation 範例</li>
<li>對應指令範例：<code>aws elasticache describe-replication-groups --replication-group-id &lt;id&gt;</code></li>
</ul>
<h3 id="engine-選擇">Engine 選擇</h3>
<p>子議題：</p>
<ul>
<li><strong>Valkey</strong>（2024+ default）：成本低 20%、OSI 開源、Redis 7.2.4 fork</li>
<li><strong>Redis OSS</strong>（legacy support）：仍可選、但 AWS 不推</li>
<li><strong>Memcached</strong>：純 cache 場景、無 cluster mode 概念（client-side sharding）</li>
<li>選擇判讀：新部署 → Valkey；既有 Redis 遷移 → Valkey（API 相容）；純 cache → Memcached</li>
</ul>
<h3 id="cluster-mode-enabled-vs-disabled">Cluster mode enabled vs disabled</h3>
<p>子議題：</p>
<ul>
<li><strong>Disabled</strong>：1 primary + N replica（最多 5）、單 shard、上限 ~340GB</li>
<li><strong>Enabled</strong>：多 shard（最多 500）、自動 sharding、橫向擴展</li>
<li>客戶端要求：Cluster mode enabled 需要 cluster-aware client</li>
<li>選擇判讀：&lt; 300GB + 簡單 → disabled；&gt; 300GB 或要 sharding → enabled</li>
</ul>
<h3 id="snapshot-與-backup">Snapshot 與 backup</h3>
<p>子議題：</p>
<ul>
<li>Automatic snapshot（保留 1-35 天）</li>
<li>Manual snapshot（保留永久、可跨 region 複製）</li>
<li>Restore：從 snapshot 建新 cluster</li>
<li>對應指令：<code>aws elasticache create-snapshot</code></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="auto-failover-機制">Auto failover 機制</h3>
<p>子議題：</p>
<ul>
<li>Multi-AZ 部署：primary 失敗、replica 自動晉升</li>
<li>Failover 時間：~30 秒到幾分鐘（依 client 重連)</li>
<li>Client 影響：DNS 切到新 primary、client 要 reconnect</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a> 跨 AZ 對照</li>
</ul>
<h3 id="elasticache-serverless">ElastiCache Serverless</h3>
<p>子議題：</p>
<ul>
<li>On-demand 模式：不選 node type、按 ECPU + storage 計費</li>
<li>自動 scale：流量增加自動擴</li>
<li>適合：流量不可預測、不想規劃容量</li>
<li>不適合：成本敏感（serverless premium）、極大 dataset</li>
</ul>
<h3 id="跨-region-replicationglobal-datastore">跨 region replication（Global Datastore）</h3>
<p>子議題：</p>
<ul>
<li>Global Datastore：1 primary region + 多個 secondary region read replica</li>
<li>跨 region replication lag &lt; 1 second（業界宣稱）</li>
<li>適合 active-passive DR</li>
<li>不支援 active-active multi-master</li>
</ul>
<h3 id="memorydb-對照">MemoryDB 對照</h3>
<p>子議題：</p>
<ul>
<li>ElastiCache：cache、Multi-AZ replica 但仍是 cache 語意（資料可重建）</li>
<li>MemoryDB：Redis-compatible durable database、multi-AZ transaction log</li>
<li>MemoryDB cost 2-3x ElastiCache、但提供 source-of-truth 語意</li>
<li>選擇判讀：要 source-of-truth Redis API → MemoryDB；cache 用途 → ElastiCache</li>
</ul>
<h3 id="parameter-group-與配置">Parameter group 與配置</h3>
<p>子議題：</p>
<ul>
<li>Parameter group：custom maxmemory-policy、timeout、client-output-buffer-limit</li>
<li>Cluster vs parameter group 的應用範圍</li>
<li>對應指令：<code>aws elasticache modify-cache-parameter-group</code></li>
</ul>
<h3 id="iam-authenticationredis-7">IAM authentication（Redis 7+）</h3>
<p>子議題：</p>
<ul>
<li>從 Redis AUTH password 升級到 IAM-based authentication</li>
<li>IAM role / user 連 ElastiCache、無需傳 password</li>
<li>對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a></li>
</ul>
<h3 id="cost-模型">Cost 模型</h3>
<p>子議題：</p>
<ul>
<li>Node type 成本（t4g.micro 到 r7g.16xlarge）</li>
<li>Reserved Instance（1/3 年承諾、折扣 30-60%）</li>
<li>Data transfer cost（同 AZ 免費、跨 AZ 收費）</li>
<li>Snapshot storage cost</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="endpoint-連不上">Endpoint 連不上</h3>
<p>操作原則：先確認 VPC + security group + subnet group 配置正確。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws elasticache describe-replication-groups --replication-group-id &lt;id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --query <span class="s2">&#34;ReplicationGroups[0].Status&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 從 VPC 內 EC2 測試連通性</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli -h &lt;primary-endpoint&gt; -p <span class="m">6379</span> PING</span></span></code></pre></div><p>判讀路徑：security group 沒開 6379 → VPC peering 不通 → DNS 解析失敗。</p>
<h3 id="failover-過程中-client-持續-error">Failover 過程中 client 持續 error</h3>
<p>操作原則：failover 期間 client 重連需要時間、確認 client 有 reconnect 邏輯。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws elasticache describe-events --source-identifier &lt;id&gt; --source-type replication-group
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 看 failover 開始 / 完成事件、對照 client 重連時間軸</span></span></span></code></pre></div><h3 id="replication-lag-高">Replication lag 高</h3>
<p>操作原則：cross-AZ replication 通常 ms 級、若 &gt; 1 sec 看 CloudWatch ReplicationLag metric。原因可能是 write throughput 過高、replica node 規格不足。</p>
<h3 id="memory-pressure--eviction">Memory pressure / eviction</h3>
<p>操作原則：看 CloudWatch DatabaseMemoryUsagePercentage、超 80% 考慮 scale up node type 或調 maxmemory-policy。</p>
<h3 id="snapshot-失敗">Snapshot 失敗</h3>
<p>操作原則：snapshot 過程暫時 fork（Redis）會佔用記憶體、若 memory 已緊張可能失敗。看 CloudWatch BytesUsedForCache。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要 source-of-truth Redis API</td>
          <td>AWS MemoryDB（durable Redis-compatible）</td>
      </tr>
      <tr>
          <td>跨雲</td>
          <td>自管 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
      </tr>
      <tr>
          <td>極端 throughput single instance</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> self-host</td>
      </tr>
      <tr>
          <td>Edge / HTTP cache</td>
          <td>CloudFront / Cloudflare Cache（T4 候選）</td>
      </tr>
      <tr>
          <td>不在 AWS 生態</td>
          <td>GCP Memorystore / Azure Cache for Redis</td>
      </tr>
      <tr>
          <td>完全 serverless 計費</td>
          <td>ElastiCache Serverless（同模組內）/ <a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a></td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>AWS IAM / VPC / Security Group 完整配置（見 security 模組）</li>
<li>CloudFormation / Terraform 完整模板</li>
<li>AWS pricing 詳細計算</li>
<li>ElastiCache vs Memorystore vs Azure Cache 完整對照</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 ElastiCache 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a></td>
          <td>EVCache 為 Netflix 自管 Memcached based 全域 cache、對應 ElastiCache for Memcached + Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5 Shopify write-through</a></td>
          <td>Write-through 在 managed cache 的實作、ElastiCache 提供同樣 Redis/Valkey API、無 self-host 維運負擔</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify serialization</a></td>
          <td>Payload 雙軌遷移 client-side 實作、ElastiCache 對應為 engine version upgrade + parameter group 滾動</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 ElastiCache-specific 案例</strong>：Airbnb / Lyft / Pinterest 等公開的 ElastiCache 規模化案例、re:Invent talks（如 ElastiCache for Valkey 遷移、Serverless 採用、Global Datastore active-passive DR 實作）。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 ElastiCache 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>Managed 也會 stampede、AWS 不會幫你做 client-side jitter / singleflight、需自行設計</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>小型 single primary / 中型 Multi-AZ replica / 大型 Cluster mode enabled + Global Datastore</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a></td>
          <td>ElastiCache 對應為 Cluster mode + Configuration Endpoint（client-side discovery）、無原生 protocol proxy</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta cache consistency</a></td>
          <td>Failover / replica promotion 期間 ElastiCache 也會出現一致性議題、CloudWatch ReplicationLag 是主要訊號</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7 Cloudflare Cache Reserve</a></td>
          <td>分層儲存對照、AWS 對應為 ElastiCache（hot）+ S3 / DynamoDB（cold）的應用層分層設計</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
</ul>
]]></content:encoded></item><item><title>2.5 presence store 與即時狀態</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/</guid><description>&lt;p>在線狀態儲存（presence store）的核心責任是維持短生命週期狀態的可查詢性，例如線上狀態、連線節點、最後活動時間。它屬於即時協調層，與正式帳務資料分層治理。&lt;/p>
&lt;h2 id="狀態模型">狀態模型&lt;/h2>
&lt;p>presence 模型通常包含 &lt;code>subject&lt;/code>、&lt;code>node&lt;/code>、&lt;code>last_seen_at&lt;/code>、&lt;code>ttl&lt;/code>。主體可能是使用者、裝置、連線或工作者。模型設計重點是查詢責任：需要查單一主體是否在線、查群組在線清單，還是查節點負載分布。&lt;/p>
&lt;p>presence 資料具備高變動、短保留特性。設計時應避免把正式業務欄位混入 presence store，讓它保持可快速更新與快速過期。&lt;/p>
&lt;h2 id="heartbeat-與-expiration">heartbeat 與 expiration&lt;/h2>
&lt;p>heartbeat 的責任是維持活性訊號，expiration 的責任是清理失效狀態。heartbeat 間隔太長會放大誤判離線，太短會增加寫入壓力。expiration 視窗要和網路抖動容忍度一起設計。&lt;/p>
&lt;p>穩定做法是定義「可接受延遲在線」窗口，而不是追求絕對即時。presence 判讀通常是近即時近似，不是強一致保證。&lt;/p>
&lt;h2 id="cross-node-query">cross-node query&lt;/h2>
&lt;p>跨節點查詢要先明確一致性需求。聊天室在線名單可容忍短暫不一致；調度系統節點可用性則需要更保守窗口與校驗策略。查詢層應同時提供快取讀取與回源校正路徑，避免單一路徑失真。&lt;/p>
&lt;p>在多區部署中，presence 常採區域內優先、跨區聚合延遲同步。這樣能降低廣域寫入成本，同時保留可接受的全域可見性。&lt;/p>
&lt;h2 id="cleanup-策略">cleanup 策略&lt;/h2>
&lt;p>cleanup 的責任是避免殭屍狀態堆積。定期掃描、lazy cleanup、事件驅動清理可混合使用。清理策略要與業務容忍度對齊：社交場景可容忍秒級延遲清除，調度場景則需更快收斂。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀重點&lt;/th>
 &lt;th>對應動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>在線數異常下降但流量未下降&lt;/td>
 &lt;td>heartbeat 發送或寫入路徑中斷&lt;/td>
 &lt;td>檢查 producer 路徑、降級為回源校驗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>離線判斷延遲明顯增加&lt;/td>
 &lt;td>expiration 視窗過長或清理積壓&lt;/td>
 &lt;td>調整 TTL、提高 cleanup 頻率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨節點查詢結果波動大&lt;/td>
 &lt;td>多節點寫入競態與聚合窗口不一致&lt;/td>
 &lt;td>收斂聚合邏輯、加入版本時間戳&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>節點重啟後出現大量殭屍在線&lt;/td>
 &lt;td>清理與重建流程未對齊&lt;/td>
 &lt;td>啟動全量重整、補啟動時同步清理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>高峰時段 presence 查詢延遲拉高&lt;/td>
 &lt;td>熱 key 集中與查詢模式不匹配&lt;/td>
 &lt;td>分散 key、按群組分片、加查詢快取&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="常見誤區">常見誤區&lt;/h2>
&lt;p>把在線狀態儲存當正式狀態來源，會讓一致性與修復成本快速上升。presence 模型適合即時協調，最終業務判定仍由正式資料層承擔。&lt;/p>
&lt;p>把 heartbeat 當固定頻率任務，也會造成高峰寫入抖動。頻率應該與線上人數與連線型態一起調整。&lt;/p>
&lt;h2 id="案例回寫">案例回寫&lt;/h2>
&lt;p>presence 模型可用 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由&lt;/a> 回寫。先看跨區路由如何影響在線可見性，再回到本章檢查 heartbeat 視窗、跨節點聚合與清理節奏是否一致。
這個案例主要支撐的是「跨區可見性與狀態新鮮度」判讀，不直接支撐 lock 租約或 queue 語意；若問題是互斥衝突或重播邊界，應轉到 2.4 或 3.x。&lt;/p>
&lt;p>若區域內在線正常、跨區可見性延遲偏大，先調整跨區同步策略與 fallback 壽命，再把影響評估接到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment&lt;/a>。&lt;/p>
&lt;h2 id="跨模組路由">跨模組路由&lt;/h2>
&lt;ol>
&lt;li>與 2.3 的交接：保留與清理策略回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction&lt;/a>。&lt;/li>
&lt;li>與 4.17 的交接：presence 資料品質與延遲偏差回到 &lt;a href="https://tarrragon.github.io/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality&lt;/a>。&lt;/li>
&lt;li>與 6.22 的交接：穩態定義與高峰演練回到 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">Steady State Definition&lt;/a>。&lt;/li>
&lt;li>與 8.20 的交接：即時狀態誤判造成客戶影響回到 &lt;a href="https://tarrragon.github.io/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">Customer Impact Assessment&lt;/a>。&lt;/li>
&lt;li>與 2.10 的交接：presence 狀態變更如何即時廣播給其他節點回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">Pub/Sub 與即時 fan-out&lt;/a>。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>要看快取層一致性與失效策略，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略&lt;/a>。要看 presence 狀態變更如何即時扇出給其他節點，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out&lt;/a>。要看跨規模 presence 路由案例，接著讀 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>在線狀態儲存（presence store）的核心責任是維持短生命週期狀態的可查詢性，例如線上狀態、連線節點、最後活動時間。它屬於即時協調層，與正式帳務資料分層治理。</p>
<h2 id="狀態模型">狀態模型</h2>
<p>presence 模型通常包含 <code>subject</code>、<code>node</code>、<code>last_seen_at</code>、<code>ttl</code>。主體可能是使用者、裝置、連線或工作者。模型設計重點是查詢責任：需要查單一主體是否在線、查群組在線清單，還是查節點負載分布。</p>
<p>presence 資料具備高變動、短保留特性。設計時應避免把正式業務欄位混入 presence store，讓它保持可快速更新與快速過期。</p>
<h2 id="heartbeat-與-expiration">heartbeat 與 expiration</h2>
<p>heartbeat 的責任是維持活性訊號，expiration 的責任是清理失效狀態。heartbeat 間隔太長會放大誤判離線，太短會增加寫入壓力。expiration 視窗要和網路抖動容忍度一起設計。</p>
<p>穩定做法是定義「可接受延遲在線」窗口，而不是追求絕對即時。presence 判讀通常是近即時近似，不是強一致保證。</p>
<h2 id="cross-node-query">cross-node query</h2>
<p>跨節點查詢要先明確一致性需求。聊天室在線名單可容忍短暫不一致；調度系統節點可用性則需要更保守窗口與校驗策略。查詢層應同時提供快取讀取與回源校正路徑，避免單一路徑失真。</p>
<p>在多區部署中，presence 常採區域內優先、跨區聚合延遲同步。這樣能降低廣域寫入成本，同時保留可接受的全域可見性。</p>
<h2 id="cleanup-策略">cleanup 策略</h2>
<p>cleanup 的責任是避免殭屍狀態堆積。定期掃描、lazy cleanup、事件驅動清理可混合使用。清理策略要與業務容忍度對齊：社交場景可容忍秒級延遲清除，調度場景則需更快收斂。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>在線數異常下降但流量未下降</td>
          <td>heartbeat 發送或寫入路徑中斷</td>
          <td>檢查 producer 路徑、降級為回源校驗</td>
      </tr>
      <tr>
          <td>離線判斷延遲明顯增加</td>
          <td>expiration 視窗過長或清理積壓</td>
          <td>調整 TTL、提高 cleanup 頻率</td>
      </tr>
      <tr>
          <td>跨節點查詢結果波動大</td>
          <td>多節點寫入競態與聚合窗口不一致</td>
          <td>收斂聚合邏輯、加入版本時間戳</td>
      </tr>
      <tr>
          <td>節點重啟後出現大量殭屍在線</td>
          <td>清理與重建流程未對齊</td>
          <td>啟動全量重整、補啟動時同步清理</td>
      </tr>
      <tr>
          <td>高峰時段 presence 查詢延遲拉高</td>
          <td>熱 key 集中與查詢模式不匹配</td>
          <td>分散 key、按群組分片、加查詢快取</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把在線狀態儲存當正式狀態來源，會讓一致性與修復成本快速上升。presence 模型適合即時協調，最終業務判定仍由正式資料層承擔。</p>
<p>把 heartbeat 當固定頻率任務，也會造成高峰寫入抖動。頻率應該與線上人數與連線型態一起調整。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>presence 模型可用 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a> 回寫。先看跨區路由如何影響在線可見性，再回到本章檢查 heartbeat 視窗、跨節點聚合與清理節奏是否一致。
這個案例主要支撐的是「跨區可見性與狀態新鮮度」判讀，不直接支撐 lock 租約或 queue 語意；若問題是互斥衝突或重播邊界，應轉到 2.4 或 3.x。</p>
<p>若區域內在線正常、跨區可見性延遲偏大，先調整跨區同步策略與 fallback 壽命，再把影響評估接到 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">8.20 Customer Impact Assessment</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.3 的交接：保留與清理策略回到 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction</a>。</li>
<li>與 4.17 的交接：presence 資料品質與延遲偏差回到 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">Telemetry Data Quality</a>。</li>
<li>與 6.22 的交接：穩態定義與高峰演練回到 <a href="/blog/backend/06-reliability/steady-state-definition/" data-link-title="6.22 Steady State Definition" data-link-desc="在 chaos 與 failover 前先定義系統應維持的穩定狀態與可接受退化">Steady State Definition</a>。</li>
<li>與 8.20 的交接：即時狀態誤判造成客戶影響回到 <a href="/blog/backend/08-incident-response/customer-impact-assessment/" data-link-title="8.20 Customer Impact Assessment" data-link-desc="把受影響用戶、功能、區域、金額、SLO 與補償判斷串成影響評估模型">Customer Impact Assessment</a>。</li>
<li>與 2.10 的交接：presence 狀態變更如何即時廣播給其他節點回到 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">Pub/Sub 與即時 fan-out</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取層一致性與失效策略，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要看 presence 狀態變更如何即時扇出給其他節點，接著讀 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out</a>。要看跨規模 presence 路由案例，接著讀 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta：mcrouter 跨區路由</a>。</p>
]]></content:encoded></item><item><title>KeyDB</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/</guid><description>&lt;p>KeyDB 是 Redis 的 multi-threaded fork、承擔三個責任：把 Redis 的命令執行從單執行緒改成多執行緒（不只 I/O、連命令處理都多核）、提供 active-active 多主複製（兩個 master 互相同步、都可寫）、維持 Redis protocol 相容（drop-in 替換）。設計取捨偏向「沿用 Redis 生態 + 單實例榨多核 + 多主寫入」、是 Redis 單執行緒撞牆但又不想重寫 client 的中間選項。&lt;/p>
&lt;p>對「單 key 極熱、Redis Cluster 切不開、需要單實例多執行緒撐單 partition」這條路徑、KeyDB 是值得評估的 fork。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 GCP 上用 KeyDB&lt;/a> 是這條路線最大的公開採用者——但要注意該案例的主因是 multi-cloud 架構下的 cross-cloud latency 治理（把 cache 跟 application 放同一個 cloud），KeyDB 的 multi-threaded 單實例吞吐是附帶優勢、不是 Snap 採用的主要驅動。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>跑起 KeyDB、用 redis-cli 驗證 protocol 相容&lt;/li>
&lt;li>評估 multi-threaded 命令執行跟 Redis I/O threads 的差異&lt;/li>
&lt;li>判斷 active-active 多主複製適用與衝突風險&lt;/li>
&lt;li>評估 KeyDB on FLASH 對大 dataset 的成本意義&lt;/li>
&lt;li>區分 KeyDB 跟 DragonflyDB / Redis Cluster 的選用判讀，並評估 Snap 收購後的治理風險&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑5-分鐘把-keydb-跑起來">最短路徑：5 分鐘把 KeyDB 跑起來&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 1. 啟動 KeyDB（--server-threads 開多執行緒、命令執行也多核）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">docker run -d --name keydb -p 6379:6379 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> eqalpha/keydb keydb-server --server-threads &lt;span class="m">4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 2. 用 redis-cli 驗證（KeyDB 100% Redis protocol 相容）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">redis-cli SET foo bar &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">redis-cli GET foo &lt;span class="c1"># → bar&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 3. 確認版本（KeyDB 回報 redis_version、client 以此判斷相容性）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|redis_mode&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:6.3.4 ← KeyDB 的版本方案、client library 以此協商相容&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_mode:standalone&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；&lt;code>--server-threads&lt;/code> 是啟動參數（不在 &lt;code>CONFIG GET&lt;/code> 內、改值要重啟）。多主複製見&lt;a href="#active-active-%e5%a4%9a%e4%b8%bb%e8%a4%87%e8%a3%bd">進階主題&lt;/a>。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="cli-與-client-api">CLI 與 client API&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>直接用 redis-cli / 所有 Redis client library（KeyDB 維持 Redis protocol）&lt;/li>
&lt;li>&lt;code>--server-threads N&lt;/code> 設命令執行的執行緒數、對齊 CPU 核數&lt;/li>
&lt;li>&lt;code>INFO server&lt;/code> 確認 redis_version（KeyDB 的版本對應 Redis 哪個 base）&lt;/li>
&lt;/ul>
&lt;h3 id="multi-threaded-命令執行">Multi-threaded 命令執行&lt;/h3>
&lt;p>KeyDB 跟 Redis I/O threads 的差異是核心賣點。子議題：&lt;/p></description><content:encoded><![CDATA[<p>KeyDB 是 Redis 的 multi-threaded fork、承擔三個責任：把 Redis 的命令執行從單執行緒改成多執行緒（不只 I/O、連命令處理都多核）、提供 active-active 多主複製（兩個 master 互相同步、都可寫）、維持 Redis protocol 相容（drop-in 替換）。設計取捨偏向「沿用 Redis 生態 + 單實例榨多核 + 多主寫入」、是 Redis 單執行緒撞牆但又不想重寫 client 的中間選項。</p>
<p>對「單 key 極熱、Redis Cluster 切不開、需要單實例多執行緒撐單 partition」這條路徑、KeyDB 是值得評估的 fork。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 GCP 上用 KeyDB</a> 是這條路線最大的公開採用者——但要注意該案例的主因是 multi-cloud 架構下的 cross-cloud latency 治理（把 cache 跟 application 放同一個 cloud），KeyDB 的 multi-threaded 單實例吞吐是附帶優勢、不是 Snap 採用的主要驅動。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>跑起 KeyDB、用 redis-cli 驗證 protocol 相容</li>
<li>評估 multi-threaded 命令執行跟 Redis I/O threads 的差異</li>
<li>判斷 active-active 多主複製適用與衝突風險</li>
<li>評估 KeyDB on FLASH 對大 dataset 的成本意義</li>
<li>區分 KeyDB 跟 DragonflyDB / Redis Cluster 的選用判讀，並評估 Snap 收購後的治理風險</li>
</ol>
<h2 id="最短路徑5-分鐘把-keydb-跑起來">最短路徑：5 分鐘把 KeyDB 跑起來</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 啟動 KeyDB（--server-threads 開多執行緒、命令執行也多核）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name keydb -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --server-threads <span class="m">4</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. 用 redis-cli 驗證（KeyDB 100% Redis protocol 相容）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli SET foo bar    <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli GET foo        <span class="c1"># → bar</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 3. 確認版本（KeyDB 回報 redis_version、client 以此判斷相容性）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|redis_mode&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># redis_version:6.3.4    ← KeyDB 的版本方案、client library 以此協商相容</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># redis_mode:standalone</span></span></span></code></pre></div><p>實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；<code>--server-threads</code> 是啟動參數（不在 <code>CONFIG GET</code> 內、改值要重啟）。多主複製見<a href="#active-active-%e5%a4%9a%e4%b8%bb%e8%a4%87%e8%a3%bd">進階主題</a>。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="cli-與-client-api">CLI 與 client API</h3>
<p>子議題：</p>
<ul>
<li>直接用 redis-cli / 所有 Redis client library（KeyDB 維持 Redis protocol）</li>
<li><code>--server-threads N</code> 設命令執行的執行緒數、對齊 CPU 核數</li>
<li><code>INFO server</code> 確認 redis_version（KeyDB 的版本對應 Redis 哪個 base）</li>
</ul>
<h3 id="multi-threaded-命令執行">Multi-threaded 命令執行</h3>
<p>KeyDB 跟 Redis I/O threads 的差異是核心賣點。子議題：</p>
<ul>
<li>Redis 6+ 的 I/O threads 只分擔 socket 讀寫、命令仍在 main thread；KeyDB 連命令執行都多執行緒</li>
<li><code>--server-threads</code> 對齊核數、單實例吞吐隨核數擴展</li>
<li>多執行緒下單 key 的並發保護由 KeyDB 內部處理、application 端語意不變</li>
</ul>
<h3 id="active-active-多主複製">Active-active 多主複製</h3>
<p>子議題：</p>
<ul>
<li>兩個（含以上）KeyDB master 互相複製、都可接受寫入</li>
<li>衝突解決用 last-write-wins（依時間戳）、不是強一致</li>
<li>適合跨 AZ / 跨 region 的讀寫就近、但要接受最終一致與衝突覆蓋風險</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="active-active-多主複製-1">Active-active 多主複製</h3>
<p>子議題：</p>
<ul>
<li><code>replicaof</code> + <code>active-replica yes</code> 開雙向複製</li>
<li>衝突語意：同 key 並發寫入、last-write-wins、可能丟其中一側的寫入</li>
<li>適用：跨區讀寫就近、可容忍最終一致的 cache；不適用：需要強一致的 counter / lock</li>
</ul>
<h3 id="keydb-on-flash">KeyDB on FLASH</h3>
<p>子議題：</p>
<ul>
<li>把冷資料放 SSD、熱資料留記憶體、降低大 dataset 的記憶體成本</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">Meta CacheLib + Kangaroo</a> 的 DRAM + flash 分層思路</li>
<li>代價：FLASH 路徑延遲高於純記憶體、適合冷熱分明的 workload</li>
</ul>
<h3 id="跟-dragonflydb--garnet-的對比">跟 DragonflyDB / Garnet 的對比</h3>
<p>子議題：</p>
<ul>
<li>KeyDB：Redis fork（沿用 Redis code base、相容度高、base 版本較舊）</li>
<li>DragonflyDB：C++ 從零重寫（架構更激進、shared-nothing、相容核心但非 fork）</li>
<li>Garnet（Microsoft）：研究型高吞吐 store、生態淺</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/" data-link-title="DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster" data-link-desc="Redis 要靠 Cluster 分片才能用滿一台多核機器，DragonflyDB 賭的是相反方向——單一進程 thread-per-core、shared-nothing、把單機推到 Redis 要好幾個 shard 才達到的規模。本文展開 thread-per-core 與 dashtable 的架構、fork-less snapshot、5 個把架構假設寫成 production 事故的踩坑，以及 scale-up 撞牆該回 Cluster 的邊界">DragonflyDB 多核架構 deep article</a> 的 fork vs 重寫光譜</li>
</ul>
<h3 id="治理風險snap-收購後">治理風險（Snap 收購後）</h3>
<p>子議題：</p>
<ul>
<li>KeyDB 公司 2022 年被 Snap 收購、開源版本的後續投入與 roadmap 不確定</li>
<li>評估採用前確認專案活躍度（commit 頻率、release cadence）</li>
<li>對長期依賴敏感的場景、Redis fork 光譜上的 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Linux Foundation 治理）治理更穩</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="多執行緒下吞吐沒提升">多執行緒下吞吐沒提升</h3>
<p>操作原則：先確認 <code>--server-threads</code> 對齊 CPU 核數、再看是否 CPU 密集 workload。判讀：thread &lt; core → 沒用滿多核；單 key 極熱 → 仍受單 partition 限制。</p>
<h3 id="active-active-衝突丟資料">Active-active 衝突丟資料</h3>
<p>操作原則：last-write-wins 下並發寫同 key 會覆蓋。判讀：跨區同 key 高頻寫入要改設計（key 分區到不同 master）、或改用強一致儲存。</p>
<h3 id="protocol-相容問題">Protocol 相容問題</h3>
<p>操作原則：KeyDB base 版本較舊（redis_version 6.x），用到 Redis 7+ 新命令會不支援。判讀：<code>INFO server</code> 確認 base 版本、對照 application 用到的命令。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>要最新 Redis 功能 / 治理穩定</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Linux Foundation、跟上 Redis）</td>
      </tr>
      <tr>
          <td>更激進的多核 / 記憶體效率</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（重寫、shared-nothing）</td>
      </tr>
      <tr>
          <td>需要 Redis Cluster sharding</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / Valkey Cluster</td>
      </tr>
      <tr>
          <td>純 KV、極簡運維</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
      </tr>
      <tr>
          <td>AWS managed</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（無 managed KeyDB）</td>
      </tr>
      <tr>
          <td>需要強一致 + durability</td>
          <td>AWS MemoryDB</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>KeyDB 完整 command reference（沿用 Redis、查 redis.io/commands）</li>
<li>各語言 client API（用 Redis client 即可）</li>
<li>KeyDB on FLASH 詳細調參</li>
<li>Active-replication 內部複製協定細節</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="直接相關案例">直接相關案例</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 KeyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB cross-cloud</a></td>
          <td>Snap 在 GCP 部署 KeyDB cluster、主因是 multi-cloud 的 cross-cloud latency 治理（cache 與 application 共置同 cloud）；9.C35 另記 KeyDB multi-threaded「單實例 throughput 提升 5-10x」（通則、依 workload）</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 KeyDB-specific 案例</strong>：Snap 收購後的公開技術分享、KeyDB on FLASH 的 production 成本案例、active-active 多主複製的跨區衝突治理實例。</p>
<h3 id="跨-vendor-對照">跨 vendor 對照</h3>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 KeyDB 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>KeyDB on FLASH 對應 DRAM + flash 分層的成本決策</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>TTL jitter / singleflight 通用、KeyDB 多執行緒不消除 stampede 風險</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>KeyDB 是「單實例多核撐大」的選項、介於 Redis Cluster 與 DragonflyDB 之間</td>
      </tr>
  </tbody>
</table>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>deep article：<a href="/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/" data-link-title="KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入" data-link-desc="KeyDB 的 active-active 讓兩個 master 都能寫、互相同步，聽起來解決了跨區寫入的所有問題——直到兩邊同時寫同一個 key，last-write-wins 默默丟掉其中一筆。本文展開 active-active 的複製機制與衝突語意、實機驗證雙向同步、5 個把多主複製寫成資料遺失與迴圈的 production 踩坑，以及哪些資料能放 active-active、哪些不能的邊界">KeyDB active-active 多主複製</a>（last-write-wins 衝突與跨區寫入）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>（單執行緒邊界的四個選項）</li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>（跨區資料引力）</li>
<li>回退路徑：<a href="/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/" data-link-title="KeyDB → Redis / Valkey：從多線程 fork 回歸主線的遷移路徑" data-link-desc="從 KeyDB 遷回 Redis 或 Valkey，處理 active-active replication 拆除、多線程 → 單線程效能差異、FLASH storage 移除與 Sentinel/Cluster 對齊">KeyDB → Redis/Valkey</a></li>
</ul>
]]></content:encoded></item><item><title>9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/</guid><description>&lt;p>這個案例的核心責任是說明「cache layer 在持續成長服務」的角色 — 不是峰值問題、是延遲 SLA 與成本曲線同時拉緊的長期工程議題。Tinder 的配對引擎需要在每次滑動都查多個快取（用戶 profile、距離、偏好過濾、推薦池），單次互動的延遲就是 UX 本身。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tinder 在 ElastiCache for Valkey 的關鍵數字（引自 &lt;a href="https://aws.amazon.com/elasticache/customers/">ElastiCache customers&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>數字&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>月活用戶&lt;/td>
 &lt;td>約 4700 萬 MAU (2025)&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配對累計&lt;/td>
 &lt;td>超過 10 億次配對&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>地理覆蓋&lt;/td>
 &lt;td>190 個國家&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>服務年數&lt;/td>
 &lt;td>自 2012 年起&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲特性&lt;/td>
 &lt;td>sub-millisecond latency&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>ElastiCache for Redis 7.1 在 r7g.4xlarge 上可達單節點 100 萬 RPS、單 cluster 5 億 RPS（引自 &lt;a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog&lt;/a>）。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Tinder 案例值得讀的是「快取在 long-running 服務的角色變化」。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>快取不是 DB 的補救、是主要服務面&lt;/strong>：配對引擎每次互動讀 cache 不讀 DB、cache miss 是 &lt;em>邊緣案例&lt;/em>。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache-as-source-of-truth 與 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary&lt;/a> 設計。&lt;/li>
&lt;li>&lt;strong>次毫秒延遲是業務 KPI、不只是技術指標&lt;/strong>：手指滑動之後 250ms 內必須給結果、否則「卡頓」。中間整個 chain（網路、cache、序列化）的 latency budget 必須緊。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 反推。&lt;/li>
&lt;li>&lt;strong>長期 sustained growth 的容量曲線是成本曲線&lt;/strong>：47M MAU 沒有明顯峰谷、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency&lt;/a> 的長期成本工程。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：Tinder 的「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁。一個 schema 變更可能讓既有 cache 全部 invalid、引發 cache stampede。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback&lt;/a>。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>cache layer 容量規劃跟 DB 容量規劃要分開&lt;/strong>：cache 容量受 working set size 影響、DB 容量受 total dataset 影響、兩者擴容邏輯不一樣。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache sizing。&lt;/li>
&lt;li>&lt;strong>cache 命中率變化是業務變化的訊號&lt;/strong>：突然命中率掉、可能是新功能影響 access pattern、不一定是 cache 容量問題。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性&lt;/a> 的訊號治理。&lt;/li>
&lt;li>&lt;strong>Valkey vs Redis OSS vs MemoryDB 是不同 trade-off&lt;/strong>：Valkey（社群分支、AWS 主推）、Redis OSS（受授權變化影響）、MemoryDB（持久化）三者選擇影響長期 vendor lock-in。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：GCP Memorystore for Redis / Valkey、Azure Cache for Redis、自建 Redis Cluster + Sentinel 都可以實作對等架構。差異是 vendor 的 patch cadence 與容量擴張流程。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「cache layer 在持續成長服務」的角色 — 不是峰值問題、是延遲 SLA 與成本曲線同時拉緊的長期工程議題。Tinder 的配對引擎需要在每次滑動都查多個快取（用戶 profile、距離、偏好過濾、推薦池），單次互動的延遲就是 UX 本身。</p>
<h2 id="觀察">觀察</h2>
<p>Tinder 在 ElastiCache for Valkey 的關鍵數字（引自 <a href="https://aws.amazon.com/elasticache/customers/">ElastiCache customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>月活用戶</td>
          <td>約 4700 萬 MAU (2025)</td>
      </tr>
      <tr>
          <td>配對累計</td>
          <td>超過 10 億次配對</td>
      </tr>
      <tr>
          <td>地理覆蓋</td>
          <td>190 個國家</td>
      </tr>
      <tr>
          <td>服務年數</td>
          <td>自 2012 年起</td>
      </tr>
      <tr>
          <td>延遲特性</td>
          <td>sub-millisecond latency</td>
      </tr>
  </tbody>
</table>
<p>ElastiCache for Redis 7.1 在 r7g.4xlarge 上可達單節點 100 萬 RPS、單 cluster 5 億 RPS（引自 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog</a>）。</p>
<h2 id="判讀">判讀</h2>
<p>Tinder 案例值得讀的是「快取在 long-running 服務的角色變化」。</p>
<ol>
<li><strong>快取不是 DB 的補救、是主要服務面</strong>：配對引擎每次互動讀 cache 不讀 DB、cache miss 是 <em>邊緣案例</em>。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache-as-source-of-truth 與 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a> 設計。</li>
<li><strong>次毫秒延遲是業務 KPI、不只是技術指標</strong>：手指滑動之後 250ms 內必須給結果、否則「卡頓」。中間整個 chain（網路、cache、序列化）的 latency budget 必須緊。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的 latency budget 反推。</li>
<li><strong>長期 sustained growth 的容量曲線是成本曲線</strong>：47M MAU 沒有明顯峰谷、容量規劃變成「每月線性擴容 X%」的長期決策、不是峰值規劃。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.7 成本邊界與 efficiency</a> 的長期成本工程。</li>
</ol>
<p>需要警惕：Tinder 的「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁。一個 schema 變更可能讓既有 cache 全部 invalid、引發 cache stampede。對應 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback</a>。</p>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>cache layer 容量規劃跟 DB 容量規劃要分開</strong>：cache 容量受 working set size 影響、DB 容量受 total dataset 影響、兩者擴容邏輯不一樣。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache sizing。</li>
<li><strong>cache 命中率變化是業務變化的訊號</strong>：突然命中率掉、可能是新功能影響 access pattern、不一定是 cache 容量問題。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.8 效能可觀測性</a> 的訊號治理。</li>
<li><strong>Valkey vs Redis OSS vs MemoryDB 是不同 trade-off</strong>：Valkey（社群分支、AWS 主推）、Redis OSS（受授權變化影響）、MemoryDB（持久化）三者選擇影響長期 vendor lock-in。</li>
</ol>
<p>跨平台等效：GCP Memorystore for Redis / Valkey、Azure Cache for Redis、自建 Redis Cluster + Sentinel 都可以實作對等架構。差異是 vendor 的 patch cadence 與容量擴張流程。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想設計 cache layer 容量 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a></li>
<li>想做 latency budget 反推 → <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.1 壓測理論與系統行為</a></li>
<li>想理解 cache stampede 風險 → <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">02.6 cache migration stampede rollback</a></li>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/amazon-ads-dynamodb-extreme-kv/" data-link-title="9.C5 Amazon Ads：DynamoDB 9000 萬 reads/sec 的廣告事件量測" data-link-desc="Amazon Ads 在 DynamoDB 上跑 9000 萬 reads/sec &#43; 500 萬 writes/sec、99.999% 可用性的廣告事件量測">9.C5 Amazon Ads DynamoDB</a>（KV 高吞吐）</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/elasticache/customers/">Amazon ElastiCache Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">Achieve over 500 million requests per second per cluster with ElastiCache for Redis 7.1</a></li>
<li><a href="https://aws.amazon.com/blogs/database/optimize-redis-client-performance-for-amazon-elasticache/">Optimize Redis Client Performance for ElastiCache and MemoryDB</a></li>
</ul>
]]></content:encoded></item><item><title>2.C6 Netflix：EVCache 全域快取層</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/</guid><description>&lt;p>這個案例的核心責任是說明快取在全球服務下會變成平台能力。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Netflix 用 EVCache 支撐大規模低延遲讀取，把快取從單服務實作提升為共用基礎設施。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當讀取延遲目標很嚴格且區域分布廣，快取需要跨區一致性與故障容忍設計。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>平台化快取客戶端與治理規則。&lt;/li>
&lt;li>把失效策略與區域容錯納入同一模型。&lt;/li>
&lt;li>以可觀測指標評估命中率與恢復能力。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://netflixtechblog.com/caching-for-a-global-netflix-7bcc457012f1">EVCache&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取在全球服務下會變成平台能力。</p>
<h2 id="觀察">觀察</h2>
<p>Netflix 用 EVCache 支撐大規模低延遲讀取，把快取從單服務實作提升為共用基礎設施。</p>
<h2 id="判讀">判讀</h2>
<p>當讀取延遲目標很嚴格且區域分布廣，快取需要跨區一致性與故障容忍設計。</p>
<h2 id="策略">策略</h2>
<ol>
<li>平台化快取客戶端與治理規則。</li>
<li>把失效策略與區域容錯納入同一模型。</li>
<li>以可觀測指標評估命中率與恢復能力。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1</a> 與 <a href="/blog/backend/00-service-selection/failure-observability-design/" data-link-title="0.7 錯誤定位、觀測訊號與備援切換設計" data-link-desc="從錯誤分類、定位線索、降級策略與 failover 設計服務可維護性">0.7</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://netflixtechblog.com/caching-for-a-global-netflix-7bcc457012f1">EVCache</a></li>
</ul>
]]></content:encoded></item><item><title>2.6 快取威脅建模（Threat Modeling）</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/attacker-view-cache-risks/</link><pubDate>Fri, 24 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/attacker-view-cache-risks/</guid><description>&lt;p>快取層威脅建模的判讀目標是確認「資料是否可被污染、可被放大、可被錯用」。快取的存在是為了用副本換取讀取效率，盤點要問的就是這個副本能不能被攻擊者操弄：寫進錯誤內容、被放大成回源洪水、或被當成正式狀態誤信。只看效能設計快取，常把一致性與安全邊界留到事故後才補。&lt;/p>
&lt;h2 id="哪些快取場景要先做弱點盤點">哪些快取場景要先做弱點盤點&lt;/h2>
&lt;p>快取弱點會在特定條件下快速放大，這些條件出現時值得在設計階段就做一次盤點，而不是等流量打上來才發現。&lt;/p>
&lt;p>熱門資料高度集中是第一個訊號。當少數 key 承載大部分讀取，形成 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 時，這些 key 的失效或污染影響面被流量放大：一個被污染的熱門商品價格，會在 TTL 週期內被數百萬次請求讀到。攻擊者只要能影響一個熱門 key，就能用快取的扇出能力把單點錯誤放大成大規模事故。&lt;/p>
&lt;p>同一份資料被多個系統共用是第二個訊號。當一份快取資料同時被 API、搜尋與報表讀取，污染這份資料的後果會跨系統擴散，而各系統對資料正確性的假設不同——API 可能容忍短暫陳舊，計費報表不能。共用快取讓「誰負責驗證這份資料」變得模糊。&lt;/p>
&lt;p>失效策略依賴多服務協作是第三個訊號。當快取失效需要多個服務按順序執行才成立時，任何一個環節被延遲或繞過，都會留下一個 stale 窗口。攻擊者可以針對這個協作鏈的最弱環節，製造可預測的不一致窗口。&lt;/p>
&lt;p>匯出、權限摘要或價格資料大量走快取是第四個訊號。這幾類資料的錯誤直接對應到金錢、越權或合規後果，一旦快取成為它們的讀取路徑，快取的一致性強度就決定了這些高風險判斷的正確性。&lt;/p>
&lt;h2 id="快取弱點的檢查順序">快取弱點的檢查順序&lt;/h2>
&lt;p>弱點盤點依「資料責任 → 失效面 → 放大面 → 污染面」的順序展開，每一層對應一個攻擊者會利用的弱點。&lt;/p>
&lt;p>第一層看資料責任，先區分 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 與快取副本。攻擊者最想找的是「被當成正式狀態的快取副本」——如果某個權限判斷直接信任快取值而不回源驗證，污染這個快取值就等於提權。檢查的方法是追每個快取讀取點，確認它讀到的是可重建的副本，還是被誤用成不該被快取的正式判斷依據。&lt;/p>
&lt;p>第二層看失效面，檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction&lt;/a> 規則是否一致。失效面的弱點是「stale 窗口可被預測或延長」：若失效只靠廣播通知而沒有 TTL 兜底，攻擊者讓廣播漏送就能讓某節點長期持有舊值；若 TTL 設得過長，污染或過期資料的影響期就被拉長。&lt;/p>
&lt;p>第三層看放大面，檢查 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd&lt;/a> 與回源壓力保護。放大面的攻擊是 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-penetration/" data-link-title="Cache Penetration" data-link-desc="說明查詢必定不存在的 key 繞過快取直接打向 origin 的弱點與防護">cache penetration&lt;/a>：攻擊者枚舉大量必定不存在的 key（不連續的 id、構造的非法 slug），這些查詢全部 miss 並穿透到資料庫，把快取的保護作用繞過、直接打垮 origin。防線是對不存在的 key 也做短期 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache&lt;/a>（把「查無此 key」這個結果也快取一小段時間，擋掉重複穿透），以及對回源路徑加 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 與單飛（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight&lt;/a>，讓同一 key 的並發回源只有一個真正打到資料庫、其餘等結果）保護。negative cache 自身有代價：真實資料建立後要等 negative 項過期才會被命中，TTL 要夠短，避免新上架資料被「查無」結果短暫遮擋。&lt;/p>
&lt;p>第四層看污染面，檢查 key 設計、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">租戶隔離&lt;/a>與欄位遮罩是否防止快取污染與越權讀取。污染面最常見的弱點是 key 命名沒有把租戶或權限維度編進去：若兩個租戶的資料共用同一個快取 key，一個租戶就能讀到另一個租戶的快取值。key 設計要讓隔離維度成為 key 的一部分，而非依賴應用層在讀取後才過濾。&lt;/p>
&lt;p>第五層看推斷面，檢查 cache 命中與否是否會透過回應時間洩漏資訊。cache hit 與 miss 的延遲差異本身是一個 side-channel：攻擊者用一批查詢的回應時間分布，可以推斷某個帳號、商品或 slug 是否存在，即使回應內容本身有做存在性遮罩。對帳號存在性、未上架商品這類敏感判斷，要讓 hit 與 miss 的可觀察延遲一致（例如 miss 也走一段固定延遲），或把存在性判斷的快取與對外查詢路徑分離。這層在敏感資料才需要盤點，一般可重建副本不受此威脅。&lt;/p>
&lt;h2 id="快取弱點先表現為資料錯誤而非停機">快取弱點先表現為資料錯誤而非停機&lt;/h2>
&lt;p>快取事故的判讀重點是它的早期症狀先表現為資料錯誤，而停機往往是後續才浮現的次級症狀。價格、庫存、權限摘要若短時間錯誤，系統照常回應請求、監控的可用性指標一切正常，但回應的內容是錯的，直接造成客訴與營運損失。這種「服務還活著但說錯話」的故障比停機更難被即時發現，因為它不觸發可用性告警。&lt;/p>
&lt;p>回源壓力缺少保護時，快取問題還會反向擴散。原本快取是用來保護資料庫的，但當 stampede 或 penetration 讓大量請求同時穿透，快取從保護層變成放大層，把一個快取層的問題擴散成資料庫與下游服務的連鎖過載。弱點盤點因此要把「快取失效時，壓力會打到哪裡」當成必答問題。&lt;/p>
&lt;h2 id="低延遲與一致性強度的取捨">低延遲與一致性強度的取捨&lt;/h2>
&lt;p>快取命中率越高，延遲與成本越好，同時一致性風險也越高。按資料重要性分層是平衡這個張力最穩定的做法，比對所有資料套同一套快取策略更能讓高風險資料拿到該有的一致性強度。&lt;/p>
&lt;p>高風險資料採較短生命週期與強驗證。價格、權限、餘額這類錯誤代價高的資料，用較短 TTL（典型在數秒到數分鐘量級）縮小污染與陳舊的影響窗口，並在關鍵判斷點保留回源驗證，讓快取只承擔加速、不承擔最終正確性。低風險資料採較寬鬆策略以保留效能收益。商品描述、頭像、靜態文案這類偶爾陳舊無實質後果的資料，用較長 TTL（數十分鐘到數小時）換取更高命中率。具體秒數沒有通用值，依該資料的陳舊容忍度決定；分層的判準是「這份資料錯誤幾分鐘的代價是什麼」，代價高的往一致性傾斜，代價低的往效能傾斜。&lt;/p>
&lt;h2 id="進入實作前要先定義的最低控制面">進入實作前要先定義的最低控制面&lt;/h2>
&lt;p>弱點盤點的產出是一組進入實作前必須先定義清楚的控制面，缺少其中任何一項，後續的快取設計都是在未定義的安全假設上往前蓋。每一項控制面同時是一個診斷工具：可以用「若沒有它會看到什麼現象」反過來判斷現有系統是否缺這道防線。&lt;/p>
&lt;p>快取資料分級與可接受陳舊窗口要先定義，這決定每類資料的 TTL 與是否需要回源驗證。沒有分級，所有資料會被同一套策略對待，高風險資料的陳舊窗口被低風險策略放寬。未定義的早期訊號是「所有 key 用同一個預設 TTL」「說不清哪些資料錯了會出事」。&lt;/p>
&lt;p>失效策略與回源保護規則要先定義，這決定 stale 窗口的上界與回源洪水的防線。失效要明確是廣播、TTL 還是事件驅動，並確認廣播類失效一定有 TTL 兜底；回源要明確 negative cache、single-flight 與 rate limit 的配置。未定義的早期訊號是「失效靠廣播但沒有 TTL 兜底」「miss 尖峰會直接打到資料庫」。&lt;/p>
&lt;p>key 命名、租戶隔離與敏感欄位限制要先定義，這決定污染與越權讀取的防線。隔離維度要編進 key、敏感欄位要在寫入快取前就遮罩，而非依賴讀取後過濾。未定義的早期訊號是「快取 key 不含租戶 id」「敏感欄位整包序列化進 cache」。&lt;/p>
&lt;p>快取異常時的降級與回復流程要先定義，這決定事故發生時系統往哪個方向退。降級要明確是「快取失效時回源並接受延遲上升」還是「直接拒絕並保護資料庫」，並預先演練回復路徑，避免事故當下才設計。未定義的早期訊號是「沒人能回答快取掛掉時系統會怎樣」。&lt;/p>
&lt;h2 id="判讀訊號">判讀訊號&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>訊號&lt;/th>
 &lt;th>判讀重點&lt;/th>
 &lt;th>對應動作&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>大量查詢不存在的 key、回源 QPS 飆&lt;/td>
 &lt;td>cache penetration 繞過快取保護&lt;/td>
 &lt;td>對不存在的 key 加 negative cache、回源加 rate limit&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>兩租戶讀到彼此的快取資料&lt;/td>
 &lt;td>key 未把租戶維度編入、隔離失效&lt;/td>
 &lt;td>把隔離維度納入 key、敏感欄位寫入前遮罩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>可用性指標正常但客訴資料錯誤&lt;/td>
 &lt;td>快取污染或陳舊，故障不觸發可用性告警&lt;/td>
 &lt;td>補資料正確性監控、關鍵判斷點回源驗證&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>單一熱門 key 失效造成回源尖峰&lt;/td>
 &lt;td>hot key 失效面被流量放大&lt;/td>
 &lt;td>對熱門 key 加 single-flight、錯開 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>權限或價格判斷直接信任快取值&lt;/td>
 &lt;td>快取副本被誤用成正式狀態&lt;/td>
 &lt;td>關鍵判斷回源驗證，快取只承擔加速&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>可用性正常但資料錯誤之所以難察覺，是因為可用性監控設計上只看系統有沒有正常回應，不掃回應內容對不對，污染或陳舊因此可以在告警全程靜默的情況下持續數小時。這類問題要靠資料正確性監控而非可用性監控才能發現，弱點盤點要確認高風險資料有對應的正確性檢查，而不是只看 latency 與 error rate。&lt;/p></description><content:encoded><![CDATA[<p>快取層威脅建模的判讀目標是確認「資料是否可被污染、可被放大、可被錯用」。快取的存在是為了用副本換取讀取效率，盤點要問的就是這個副本能不能被攻擊者操弄：寫進錯誤內容、被放大成回源洪水、或被當成正式狀態誤信。只看效能設計快取，常把一致性與安全邊界留到事故後才補。</p>
<h2 id="哪些快取場景要先做弱點盤點">哪些快取場景要先做弱點盤點</h2>
<p>快取弱點會在特定條件下快速放大，這些條件出現時值得在設計階段就做一次盤點，而不是等流量打上來才發現。</p>
<p>熱門資料高度集中是第一個訊號。當少數 key 承載大部分讀取，形成 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 時，這些 key 的失效或污染影響面被流量放大：一個被污染的熱門商品價格，會在 TTL 週期內被數百萬次請求讀到。攻擊者只要能影響一個熱門 key，就能用快取的扇出能力把單點錯誤放大成大規模事故。</p>
<p>同一份資料被多個系統共用是第二個訊號。當一份快取資料同時被 API、搜尋與報表讀取，污染這份資料的後果會跨系統擴散，而各系統對資料正確性的假設不同——API 可能容忍短暫陳舊，計費報表不能。共用快取讓「誰負責驗證這份資料」變得模糊。</p>
<p>失效策略依賴多服務協作是第三個訊號。當快取失效需要多個服務按順序執行才成立時，任何一個環節被延遲或繞過，都會留下一個 stale 窗口。攻擊者可以針對這個協作鏈的最弱環節，製造可預測的不一致窗口。</p>
<p>匯出、權限摘要或價格資料大量走快取是第四個訊號。這幾類資料的錯誤直接對應到金錢、越權或合規後果，一旦快取成為它們的讀取路徑，快取的一致性強度就決定了這些高風險判斷的正確性。</p>
<h2 id="快取弱點的檢查順序">快取弱點的檢查順序</h2>
<p>弱點盤點依「資料責任 → 失效面 → 放大面 → 污染面」的順序展開，每一層對應一個攻擊者會利用的弱點。</p>
<p>第一層看資料責任，先區分 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 與快取副本。攻擊者最想找的是「被當成正式狀態的快取副本」——如果某個權限判斷直接信任快取值而不回源驗證，污染這個快取值就等於提權。檢查的方法是追每個快取讀取點，確認它讀到的是可重建的副本，還是被誤用成不該被快取的正式判斷依據。</p>
<p>第二層看失效面，檢查 <a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a>、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a> 與 <a href="/blog/backend/knowledge-cards/eviction/" data-link-title="Eviction" data-link-desc="說明快取容量不足時哪些資料會被淘汰，以及淘汰如何影響服務">eviction</a> 規則是否一致。失效面的弱點是「stale 窗口可被預測或延長」：若失效只靠廣播通知而沒有 TTL 兜底，攻擊者讓廣播漏送就能讓某節點長期持有舊值；若 TTL 設得過長，污染或過期資料的影響期就被拉長。</p>
<p>第三層看放大面，檢查 <a href="/blog/backend/knowledge-cards/cache-stampede/" data-link-title="Cache Stampede" data-link-desc="說明快取同時失效時大量 request 如何壓垮正式來源">cache stampede</a>、<a href="/blog/backend/knowledge-cards/thundering-herd/" data-link-title="Thundering Herd" data-link-desc="說明大量工作同時被喚醒或同時競爭資源時的尖峰風險">thundering herd</a> 與回源壓力保護。放大面的攻擊是 <a href="/blog/backend/knowledge-cards/cache-penetration/" data-link-title="Cache Penetration" data-link-desc="說明查詢必定不存在的 key 繞過快取直接打向 origin 的弱點與防護">cache penetration</a>：攻擊者枚舉大量必定不存在的 key（不連續的 id、構造的非法 slug），這些查詢全部 miss 並穿透到資料庫，把快取的保護作用繞過、直接打垮 origin。防線是對不存在的 key 也做短期 <a href="/blog/backend/knowledge-cards/negative-cache/" data-link-title="Negative Cache" data-link-desc="說明把「查無此 key」的結果也快取一小段時間，擋掉重複穿透的防護與代價">negative cache</a>（把「查無此 key」這個結果也快取一小段時間，擋掉重複穿透），以及對回源路徑加 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與單飛（<a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">single-flight</a>，讓同一 key 的並發回源只有一個真正打到資料庫、其餘等結果）保護。negative cache 自身有代價：真實資料建立後要等 negative 項過期才會被命中，TTL 要夠短，避免新上架資料被「查無」結果短暫遮擋。</p>
<p>第四層看污染面，檢查 key 設計、<a href="/blog/backend/knowledge-cards/tenant-boundary/" data-link-title="Tenant Boundary" data-link-desc="說明多租戶系統如何隔離不同客戶或組織的資料與資源">租戶隔離</a>與欄位遮罩是否防止快取污染與越權讀取。污染面最常見的弱點是 key 命名沒有把租戶或權限維度編進去：若兩個租戶的資料共用同一個快取 key，一個租戶就能讀到另一個租戶的快取值。key 設計要讓隔離維度成為 key 的一部分，而非依賴應用層在讀取後才過濾。</p>
<p>第五層看推斷面，檢查 cache 命中與否是否會透過回應時間洩漏資訊。cache hit 與 miss 的延遲差異本身是一個 side-channel：攻擊者用一批查詢的回應時間分布，可以推斷某個帳號、商品或 slug 是否存在，即使回應內容本身有做存在性遮罩。對帳號存在性、未上架商品這類敏感判斷，要讓 hit 與 miss 的可觀察延遲一致（例如 miss 也走一段固定延遲），或把存在性判斷的快取與對外查詢路徑分離。這層在敏感資料才需要盤點，一般可重建副本不受此威脅。</p>
<h2 id="快取弱點先表現為資料錯誤而非停機">快取弱點先表現為資料錯誤而非停機</h2>
<p>快取事故的判讀重點是它的早期症狀先表現為資料錯誤，而停機往往是後續才浮現的次級症狀。價格、庫存、權限摘要若短時間錯誤，系統照常回應請求、監控的可用性指標一切正常，但回應的內容是錯的，直接造成客訴與營運損失。這種「服務還活著但說錯話」的故障比停機更難被即時發現，因為它不觸發可用性告警。</p>
<p>回源壓力缺少保護時，快取問題還會反向擴散。原本快取是用來保護資料庫的，但當 stampede 或 penetration 讓大量請求同時穿透，快取從保護層變成放大層，把一個快取層的問題擴散成資料庫與下游服務的連鎖過載。弱點盤點因此要把「快取失效時，壓力會打到哪裡」當成必答問題。</p>
<h2 id="低延遲與一致性強度的取捨">低延遲與一致性強度的取捨</h2>
<p>快取命中率越高，延遲與成本越好，同時一致性風險也越高。按資料重要性分層是平衡這個張力最穩定的做法，比對所有資料套同一套快取策略更能讓高風險資料拿到該有的一致性強度。</p>
<p>高風險資料採較短生命週期與強驗證。價格、權限、餘額這類錯誤代價高的資料，用較短 TTL（典型在數秒到數分鐘量級）縮小污染與陳舊的影響窗口，並在關鍵判斷點保留回源驗證，讓快取只承擔加速、不承擔最終正確性。低風險資料採較寬鬆策略以保留效能收益。商品描述、頭像、靜態文案這類偶爾陳舊無實質後果的資料，用較長 TTL（數十分鐘到數小時）換取更高命中率。具體秒數沒有通用值，依該資料的陳舊容忍度決定；分層的判準是「這份資料錯誤幾分鐘的代價是什麼」，代價高的往一致性傾斜，代價低的往效能傾斜。</p>
<h2 id="進入實作前要先定義的最低控制面">進入實作前要先定義的最低控制面</h2>
<p>弱點盤點的產出是一組進入實作前必須先定義清楚的控制面，缺少其中任何一項，後續的快取設計都是在未定義的安全假設上往前蓋。每一項控制面同時是一個診斷工具：可以用「若沒有它會看到什麼現象」反過來判斷現有系統是否缺這道防線。</p>
<p>快取資料分級與可接受陳舊窗口要先定義，這決定每類資料的 TTL 與是否需要回源驗證。沒有分級，所有資料會被同一套策略對待，高風險資料的陳舊窗口被低風險策略放寬。未定義的早期訊號是「所有 key 用同一個預設 TTL」「說不清哪些資料錯了會出事」。</p>
<p>失效策略與回源保護規則要先定義，這決定 stale 窗口的上界與回源洪水的防線。失效要明確是廣播、TTL 還是事件驅動，並確認廣播類失效一定有 TTL 兜底；回源要明確 negative cache、single-flight 與 rate limit 的配置。未定義的早期訊號是「失效靠廣播但沒有 TTL 兜底」「miss 尖峰會直接打到資料庫」。</p>
<p>key 命名、租戶隔離與敏感欄位限制要先定義，這決定污染與越權讀取的防線。隔離維度要編進 key、敏感欄位要在寫入快取前就遮罩，而非依賴讀取後過濾。未定義的早期訊號是「快取 key 不含租戶 id」「敏感欄位整包序列化進 cache」。</p>
<p>快取異常時的降級與回復流程要先定義，這決定事故發生時系統往哪個方向退。降級要明確是「快取失效時回源並接受延遲上升」還是「直接拒絕並保護資料庫」，並預先演練回復路徑，避免事故當下才設計。未定義的早期訊號是「沒人能回答快取掛掉時系統會怎樣」。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>大量查詢不存在的 key、回源 QPS 飆</td>
          <td>cache penetration 繞過快取保護</td>
          <td>對不存在的 key 加 negative cache、回源加 rate limit</td>
      </tr>
      <tr>
          <td>兩租戶讀到彼此的快取資料</td>
          <td>key 未把租戶維度編入、隔離失效</td>
          <td>把隔離維度納入 key、敏感欄位寫入前遮罩</td>
      </tr>
      <tr>
          <td>可用性指標正常但客訴資料錯誤</td>
          <td>快取污染或陳舊，故障不觸發可用性告警</td>
          <td>補資料正確性監控、關鍵判斷點回源驗證</td>
      </tr>
      <tr>
          <td>單一熱門 key 失效造成回源尖峰</td>
          <td>hot key 失效面被流量放大</td>
          <td>對熱門 key 加 single-flight、錯開 TTL</td>
      </tr>
      <tr>
          <td>權限或價格判斷直接信任快取值</td>
          <td>快取副本被誤用成正式狀態</td>
          <td>關鍵判斷回源驗證，快取只承擔加速</td>
      </tr>
  </tbody>
</table>
<p>可用性正常但資料錯誤之所以難察覺，是因為可用性監控設計上只看系統有沒有正常回應，不掃回應內容對不對，污染或陳舊因此可以在告警全程靜默的情況下持續數小時。這類問題要靠資料正確性監控而非可用性監控才能發現，弱點盤點要確認高風險資料有對應的正確性檢查，而不是只看 latency 與 error rate。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把快取副本當成正式狀態信任，是最危險的誤區。權限、餘額、配額這類判斷若直接信任快取值而不回源驗證，污染快取就等於繞過業務規則。快取承擔加速，正式判斷的正確性要由 source of truth 保證。</p>
<p>只用廣播做失效而不設 TTL 兜底，是第二個誤區。廣播是 at-most-once，總有漏送可能，缺 TTL 時一次漏送就讓某節點長期持有污染或陳舊資料。TTL 是讓失效失敗的影響有上界的保險。</p>
<p>把租戶隔離放在讀取後過濾，是第三個誤區。若快取 key 不含租戶維度、靠應用層讀出後再過濾，任何一個漏掉過濾的讀取路徑都會洩漏跨租戶資料。隔離要編進 key，讓不同租戶在儲存層就不共用快取項。</p>
<h2 id="案例回寫">案例回寫</h2>
<p>快取放大面的弱點盤點可用 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：stampede rollout regression</a> 回寫。該案例的回源洪水來自部署回歸而非惡意攻擊，放大後果相似——大量請求同時 miss、穿透到 origin、把快取從保護層變成放大層——但觸發源不同：2.C9 是意外的部署回歸，攻擊場景則是刻意查詢大量不存在的 key。後果相似讓防護有共通部分（single-flight、回源 rate limit），觸發源不同則讓防線各有重點：部署回歸重在發布保護與 warmup，惡意穿透重在 negative cache 與請求來源限制。回寫時要保留「失效時壓力打到哪裡、單飛與 rate limit 是否就位」的判讀，把它從事後復盤前移到設計階段的弱點盤點。</p>
<p>這個案例主要支撐放大面與回源保護的判讀，不直接支撐污染面或租戶隔離；若根因是跨租戶資料洩漏或快取被當正式狀態，應回到 key 設計與 source of truth 邊界，而非回源保護。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.2 的交接：失效面的策略與 TTL 兜底回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 2.7 的交接：快取副本與正式狀態的邊界回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">Cache Copy Boundary 與 Freshness</a>。</li>
<li>與 2.1 的交接：hot key 與 stampede 的放大面保護回到 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">高併發下的 Redis 讀寫邊界</a>。</li>
<li>與 6.20 的交接：弱點盤點後的故障演練與停損條件回到 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">Experiment Safety Boundary</a>。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看快取副本與正式狀態的界線如何劃分，接著讀 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary 與 Freshness</a>。要看放大面的回源保護如何在實作中成立，接著讀 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback 實作示範</a>。</p>
]]></content:encoded></item><item><title>Momento</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/momento/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/momento/</guid><description>&lt;p>Momento 是 serverless cache 服務、承擔三個責任：把 cache 變成一個按用量計費的 API（沒有 node、沒有 cluster、不規劃容量）、自動隨流量 scale（尖峰自動擴、閒置不付固定費）、提供原生 SDK 與 Redis / Memcached 相容介面（既有 client 可遷）。設計取捨偏向「把 cache 的容量規劃與維運完全消除、用計費換掉 sizing」、是不想養 cache 叢集又要彈性的選項。&lt;/p>
&lt;p>對「流量不可預測、不想規劃容量與 sizing、團隊沒有 cache 運維資源」這條路徑、Momento 是 serverless 方向的代表。它跟自管 Redis、managed cache 的上層取捨（自管 vs managed vs serverless vs BaaS bundle）見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建&lt;/a>。&lt;/p>
&lt;blockquote>
&lt;p>本頁的計費、limit 與功能宣稱以 &lt;a href="https://docs.momentohq.com/">Momento 官方文件&lt;/a> 與 &lt;a href="https://www.gomomento.com/pricing/">Momento 定價&lt;/a> 為準、最後檢查日 2026-06-16。Momento 是 SaaS、需帳號與 API key、無法本機 docker 驗證、指令為依官方文件的範例。&lt;/p>&lt;/blockquote>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>理解 serverless cache 跟 node-based / managed cache 的計費與維運差異&lt;/li>
&lt;li>評估按用量計費（per request + data transfer）對你的流量形狀划不划算&lt;/li>
&lt;li>判斷 Momento 原生 SDK vs Redis 相容介面的遷移路徑&lt;/li>
&lt;li>區分 Momento 跟 ElastiCache Serverless 的定位差異&lt;/li>
&lt;li>判斷哪些 cache 場景適合 serverless、哪些該回 node-based&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑用-sdk-連-momento">最短路徑：用 SDK 連 Momento&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl"># 1. 在 Momento Console 建 cache + 取得 API key（無 node / cluster 配置）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"># 2. 用語言 SDK（以 pseudo-code 示意、實際 API 以官方 SDK 文件為準）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">client = CacheClient(api_key, default_ttl=60s)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">client.set(&amp;#34;my-cache&amp;#34;, &amp;#34;foo&amp;#34;, &amp;#34;bar&amp;#34;) # 寫入、TTL 內有效
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">client.get(&amp;#34;my-cache&amp;#34;, &amp;#34;foo&amp;#34;) # → &amp;#34;bar&amp;#34;&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>最短路徑的重點是「沒有 endpoint / node / sizing 要配」——建 cache 是一個 API 動作、不是 provision 一台機器。實際 SDK 介面以 &lt;a href="https://docs.momentohq.com/">Momento SDK 文件&lt;/a> 為準。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="sdk-與相容介面">SDK 與相容介面&lt;/h3>
&lt;p>子議題：&lt;/p>
&lt;ul>
&lt;li>原生 SDK（多語言）：gRPC-based、Momento 自有 API&lt;/li>
&lt;li>Redis / Memcached 相容介面：既有 Redis / Memcached client 可遷（相容範圍以官方為準、要驗證）&lt;/li>
&lt;li>沒有 redis-cli 等價的 server 操作（serverless 無 server 可登入）&lt;/li>
&lt;/ul>
&lt;h3 id="計費模型核心決策">計費模型（核心決策）&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Momento 是 serverless cache 服務、承擔三個責任：把 cache 變成一個按用量計費的 API（沒有 node、沒有 cluster、不規劃容量）、自動隨流量 scale（尖峰自動擴、閒置不付固定費）、提供原生 SDK 與 Redis / Memcached 相容介面（既有 client 可遷）。設計取捨偏向「把 cache 的容量規劃與維運完全消除、用計費換掉 sizing」、是不想養 cache 叢集又要彈性的選項。</p>
<p>對「流量不可預測、不想規劃容量與 sizing、團隊沒有 cache 運維資源」這條路徑、Momento 是 serverless 方向的代表。它跟自管 Redis、managed cache 的上層取捨（自管 vs managed vs serverless vs BaaS bundle）見 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a>。</p>
<blockquote>
<p>本頁的計費、limit 與功能宣稱以 <a href="https://docs.momentohq.com/">Momento 官方文件</a> 與 <a href="https://www.gomomento.com/pricing/">Momento 定價</a> 為準、最後檢查日 2026-06-16。Momento 是 SaaS、需帳號與 API key、無法本機 docker 驗證、指令為依官方文件的範例。</p></blockquote>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>理解 serverless cache 跟 node-based / managed cache 的計費與維運差異</li>
<li>評估按用量計費（per request + data transfer）對你的流量形狀划不划算</li>
<li>判斷 Momento 原生 SDK vs Redis 相容介面的遷移路徑</li>
<li>區分 Momento 跟 ElastiCache Serverless 的定位差異</li>
<li>判斷哪些 cache 場景適合 serverless、哪些該回 node-based</li>
</ol>
<h2 id="最短路徑用-sdk-連-momento">最短路徑：用 SDK 連 Momento</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl"># 1. 在 Momento Console 建 cache + 取得 API key（無 node / cluster 配置）
</span></span><span class="line"><span class="ln">2</span><span class="cl"># 2. 用語言 SDK（以 pseudo-code 示意、實際 API 以官方 SDK 文件為準）
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl">client = CacheClient(api_key, default_ttl=60s)
</span></span><span class="line"><span class="ln">5</span><span class="cl">client.set(&#34;my-cache&#34;, &#34;foo&#34;, &#34;bar&#34;)     # 寫入、TTL 內有效
</span></span><span class="line"><span class="ln">6</span><span class="cl">client.get(&#34;my-cache&#34;, &#34;foo&#34;)            # → &#34;bar&#34;</span></span></code></pre></div><p>最短路徑的重點是「沒有 endpoint / node / sizing 要配」——建 cache 是一個 API 動作、不是 provision 一台機器。實際 SDK 介面以 <a href="https://docs.momentohq.com/">Momento SDK 文件</a> 為準。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="sdk-與相容介面">SDK 與相容介面</h3>
<p>子議題：</p>
<ul>
<li>原生 SDK（多語言）：gRPC-based、Momento 自有 API</li>
<li>Redis / Memcached 相容介面：既有 Redis / Memcached client 可遷（相容範圍以官方為準、要驗證）</li>
<li>沒有 redis-cli 等價的 server 操作（serverless 無 server 可登入）</li>
</ul>
<h3 id="計費模型核心決策">計費模型（核心決策）</h3>
<p>子議題：</p>
<ul>
<li>按用量計費：data transfer（傳輸量）+ 可能的 request / storage 維度（以官方定價為準）</li>
<li>無固定 node 費用：閒置時段不付 idle node 的錢</li>
<li>流量尖峰自動 scale：不需預留容量、但尖峰量直接反映在帳單</li>
</ul>
<h3 id="沒有容量規劃">沒有容量規劃</h3>
<p>子議題：</p>
<ul>
<li>不選 node type、不設 maxmemory、不規劃 shard</li>
<li>scaling 由 Momento 處理、application 端不感知</li>
<li>代價：失去對底層的控制（無法調 eviction policy 等 server 參數）</li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="serverless-計費的甜蜜點與陷阱">Serverless 計費的甜蜜點與陷阱</h3>
<p>子議題：</p>
<ul>
<li>甜蜜點：流量不可預測、有大量閒置時段、不想為峰值預留容量</li>
<li>陷阱：穩態高流量下、按用量可能比 node-based + Reserved Instance 貴</li>
<li>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache Serverless 的計費踩坑</a> 同類議題、access pattern 低效會推高帳單</li>
</ul>
<h3 id="momento-vs-elasticache-serverless">Momento vs ElastiCache Serverless</h3>
<p>子議題：</p>
<ul>
<li>Momento：cache-as-API、完全 serverless、跨雲（不綁單一 cloud）</li>
<li>ElastiCache Serverless：AWS 生態內的 node 抽象、仍是 ElastiCache engine、綁 AWS</li>
<li>選擇：要完全擺脫容量規劃 + 跨雲 → Momento；已在 AWS 生態 + 要 engine 控制 → ElastiCache</li>
</ul>
<h3 id="遷移與相容性驗證">遷移與相容性驗證</h3>
<p>子議題：</p>
<ul>
<li>從 Redis / Memcached 遷 Momento：用相容介面或改用原生 SDK</li>
<li>相容範圍要逐項驗證（serverless 不支援 server-side 操作如 SCAN 全庫、Lua 等、以官方為準）</li>
<li>失去的能力：server 參數調校、自管 persistence、module</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="帳單超出預期">帳單超出預期</h3>
<p>操作原則：serverless 帳單反映實際用量、先看 data transfer 與 request 量。判讀：access pattern 低效（大量小請求、大 value）會推高、用批次 / 合併降量；穩態高流量重新評估 node-based。</p>
<h3 id="延遲比自管高">延遲比自管高</h3>
<p>操作原則：serverless cache 多一層 API gateway / 跨網路、延遲可能高於同 VPC 的自管 Redis。判讀：latency-sensitive 且穩態高流量的場景、評估自管或 managed node-based。</p>
<h3 id="相容介面行為差異">相容介面行為差異</h3>
<p>操作原則：Redis 相容介面不等於 100% Redis、server-side 操作可能不支援。判讀：對照官方相容清單、用到的命令逐一驗證。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>穩態高流量、成本敏感</td>
          <td>node-based <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> + Reserved Instance</td>
      </tr>
      <tr>
          <td>需要 server 參數 / eviction 控制</td>
          <td>自管 Redis / <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a></td>
      </tr>
      <tr>
          <td>已在 AWS 生態</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache Serverless</a>（同生態）</td>
      </tr>
      <tr>
          <td>需要 Redis data types / module</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（完整 data types）</td>
      </tr>
      <tr>
          <td>process-local 極低延遲</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a>（JVM 內、無網路）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Momento 完整 SDK API（各語言、以官方文件為準）</li>
<li>詳細計費計算（以官方定價為準）</li>
<li>Redis / Memcached 相容介面的完整相容矩陣</li>
<li>Momento Topics（pub/sub）等 cache 以外的產品線</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照本模組-case-庫暫無-momento-specific-case">跨 vendor 對照（本模組 case 庫暫無 Momento-specific case）</h3>
<p>Momento 是較新的 serverless cache、本 blog 的 cache case 庫（Meta / Shopify / Netflix / Cloudflare / Tinder / Tubi / Snap）暫無 Momento production case。以下用 serverless 的角度對照既有 case 提供判讀。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Momento 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 Cache Stampede</a></td>
          <td>serverless 也會 stampede、client-side jitter / singleflight 仍要自己做</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a></td>
          <td>「feature 可重算才選 cache」的判斷對 serverless 一樣適用、不可重建走 durable</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 規模對照</a></td>
          <td>serverless 適合早期 / 不可預測流量、規模穩定後評估 node-based 成本</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Momento-specific 案例</strong>：serverless cache 的成本與彈性 production 個案、從 ElastiCache 遷 Momento 的成本對照、不可預測流量場景的採用分享。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游能力：<a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a>（自管 vs managed vs serverless）、<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（Serverless 選項）、<a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a>（另一端：process-local）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
</ul>
]]></content:encoded></item><item><title>2.7 Cache Copy Boundary 與 Freshness</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/</guid><description>&lt;p>Cache copy boundary 與 freshness 的核心責任是定義快取副本能承擔什麼判斷，以及它可以不新鮮多久。進入 Redis、Valkey、Memcached 或其他快取服務前，讀者需要先理解快取同時是加速層，也是 source of truth 與讀取壓力之間的風險邊界。&lt;/p>
&lt;h2 id="cache-copy-boundary">Cache Copy Boundary&lt;/h2>
&lt;p>Cache copy boundary 的責任是把可重建副本和正式狀態分開。&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 承擔最終判斷，cache 承擔低延遲讀取與來源保護。&lt;/p>
&lt;p>商品描述、公開設定、推薦摘要通常是可重建副本。價格、庫存、權限、配額與付款狀態雖然可以被快取，但它們的錯誤會直接影響交易或安全判斷，因此 freshness 與 invalidation 要更嚴格。&lt;/p>
&lt;h2 id="freshness">Freshness&lt;/h2>
&lt;p>Freshness 的責任是定義資料可接受的 stale window。不同欄位需要不同 window，TTL 策略要跟欄位風險分層對齊。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料類型&lt;/th>
 &lt;th>可接受 stale&lt;/th>
 &lt;th>判斷重點&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>商品描述&lt;/td>
 &lt;td>秒到分鐘級&lt;/td>
 &lt;td>主要影響體驗&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>推薦清單&lt;/td>
 &lt;td>秒到分鐘級&lt;/td>
 &lt;td>主要影響排序與轉換率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>價格&lt;/td>
 &lt;td>秒級或事件失效&lt;/td>
 &lt;td>影響交易正確性&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>庫存&lt;/td>
 &lt;td>秒級或即時查詢&lt;/td>
 &lt;td>影響超賣與履約&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>權限&lt;/td>
 &lt;td>極短或強制失效&lt;/td>
 &lt;td>影響資料外洩與越權&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>配額&lt;/td>
 &lt;td>極短或原子更新&lt;/td>
 &lt;td>影響濫用與計費&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data&lt;/a> 本身是快取常態成本，定義 stale 代價能讓團隊選擇對應保護。可接受 stale 的資料可用 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 管理，高代價 stale 的資料需要事件失效、版本化 key 或回源確認。&lt;/p>
&lt;p>商品描述與推薦清單偏向體驗資料，短暫 stale 的主要代價是使用者看到較舊內容。價格與庫存偏向交易資料，stale 會改變付款、履約或客服判斷。權限與配額偏向控制資料，stale 會放大越權、濫用或計費風險。這些差異決定快取策略要分欄位設計，並以服務層邊界統一交接。&lt;/p>
&lt;h2 id="invalidation">Invalidation&lt;/h2>
&lt;p>Invalidation 的責任是讓快取副本在正式狀態變更後收斂。常見模型包含刪除 key、更新 key、版本化 key、事件驅動失效與 TTL 保底。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation&lt;/a> 要和資料責任對齊。價格類資料適合事件驅動失效加短 TTL；商品描述可以長 TTL 加背景刷新；權限類資料要能在撤權後快速失效。&lt;/p>
&lt;h3 id="cache-不一致的主要來源點">Cache 不一致的主要來源點&lt;/h3>
&lt;p>規模化 cache 的不一致主要由 &lt;em>topology 變動事件&lt;/em> 觸發、不是 TTL 設定。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency Upgrade&lt;/a> — 案例指出 promotion、shard move、故障恢復是三類主要事件來源、傳統 invalidation 在大規模系統難以維持穩定。&lt;/p>
&lt;p>&lt;strong>三類事件的典型機制&lt;/strong>（具體實作依 cluster 設計而異）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Promotion / failover&lt;/strong>：primary 切到 replica 過程中、寫入順序可能跨節點不一致、replica 變 primary 後可能讀到舊資料&lt;/li>
&lt;li>&lt;strong>Shard move / rebalance&lt;/strong>：cluster topology 變更時、部分 key 在搬遷窗口內可能讀到舊 shard 的副本&lt;/li>
&lt;li>&lt;strong>故障恢復&lt;/strong>：節點重啟後、cache 從 backing store 重建、跟 application 寫入的新值可能交錯&lt;/li>
&lt;/ul>
&lt;p>在這些事件中、cache 拓樸隨著事件改變、需要追蹤 mutation 收斂、不只清 key。Meta 的解法是把 &lt;em>mutation tracing&lt;/em> 制度化、追蹤每次資料變動是否在所有 cache 副本都收斂。&lt;/p>
&lt;h3 id="mutation-tracing-跟一致性指標">Mutation tracing 跟一致性指標&lt;/h3>
&lt;p>Mutation tracing 是 &lt;em>資料變動到所有 cache 副本收斂的時間軸&lt;/em> 追蹤、跟一般 cache hit rate 屬不同維度。常見的工程實踐指標（屬 case-derived 推論、非 Meta case 直接揭露具體 SLO）：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Inconsistency window&lt;/strong>：從 source-of-truth 寫入到所有 cache 副本反映的耗時（平均 / p99）&lt;/li>
&lt;li>&lt;strong>Inconsistency rate&lt;/strong>：query 取到 stale 副本的比例&lt;/li>
&lt;li>&lt;strong>Inconsistency duration distribution&lt;/strong>：stale 持續時間的分布（看長尾才能識別事故風險、平均值會掩蓋）&lt;/li>
&lt;/ul>
&lt;p>這些指標要接到告警跟回退條件、用法接近一般 SLO（例：inconsistency window p99 超過 &lt;em>服務可接受 stale window&lt;/em> 觸發保護動作）。具體門檻依業務型態定 — 付款 / 庫存 / 權限類資料的容忍可能在秒級、商品描述可能在分鐘級。&lt;/p></description><content:encoded><![CDATA[<p>Cache copy boundary 與 freshness 的核心責任是定義快取副本能承擔什麼判斷，以及它可以不新鮮多久。進入 Redis、Valkey、Memcached 或其他快取服務前，讀者需要先理解快取同時是加速層，也是 source of truth 與讀取壓力之間的風險邊界。</p>
<h2 id="cache-copy-boundary">Cache Copy Boundary</h2>
<p>Cache copy boundary 的責任是把可重建副本和正式狀態分開。<a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 承擔最終判斷，cache 承擔低延遲讀取與來源保護。</p>
<p>商品描述、公開設定、推薦摘要通常是可重建副本。價格、庫存、權限、配額與付款狀態雖然可以被快取，但它們的錯誤會直接影響交易或安全判斷，因此 freshness 與 invalidation 要更嚴格。</p>
<h2 id="freshness">Freshness</h2>
<p>Freshness 的責任是定義資料可接受的 stale window。不同欄位需要不同 window，TTL 策略要跟欄位風險分層對齊。</p>
<table>
  <thead>
      <tr>
          <th>資料類型</th>
          <th>可接受 stale</th>
          <th>判斷重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商品描述</td>
          <td>秒到分鐘級</td>
          <td>主要影響體驗</td>
      </tr>
      <tr>
          <td>推薦清單</td>
          <td>秒到分鐘級</td>
          <td>主要影響排序與轉換率</td>
      </tr>
      <tr>
          <td>價格</td>
          <td>秒級或事件失效</td>
          <td>影響交易正確性</td>
      </tr>
      <tr>
          <td>庫存</td>
          <td>秒級或即時查詢</td>
          <td>影響超賣與履約</td>
      </tr>
      <tr>
          <td>權限</td>
          <td>極短或強制失效</td>
          <td>影響資料外洩與越權</td>
      </tr>
      <tr>
          <td>配額</td>
          <td>極短或原子更新</td>
          <td>影響濫用與計費</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/stale-data/" data-link-title="Stale Data" data-link-desc="說明過期資料在快取、replica 與衍生資料中的產品影響">stale data</a> 本身是快取常態成本，定義 stale 代價能讓團隊選擇對應保護。可接受 stale 的資料可用 <a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 管理，高代價 stale 的資料需要事件失效、版本化 key 或回源確認。</p>
<p>商品描述與推薦清單偏向體驗資料，短暫 stale 的主要代價是使用者看到較舊內容。價格與庫存偏向交易資料，stale 會改變付款、履約或客服判斷。權限與配額偏向控制資料，stale 會放大越權、濫用或計費風險。這些差異決定快取策略要分欄位設計，並以服務層邊界統一交接。</p>
<h2 id="invalidation">Invalidation</h2>
<p>Invalidation 的責任是讓快取副本在正式狀態變更後收斂。常見模型包含刪除 key、更新 key、版本化 key、事件驅動失效與 TTL 保底。</p>
<p><a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache invalidation</a> 要和資料責任對齊。價格類資料適合事件驅動失效加短 TTL；商品描述可以長 TTL 加背景刷新；權限類資料要能在撤權後快速失效。</p>
<h3 id="cache-不一致的主要來源點">Cache 不一致的主要來源點</h3>
<p>規模化 cache 的不一致主要由 <em>topology 變動事件</em> 觸發、不是 TTL 設定。對應 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1 Meta Cache Consistency Upgrade</a> — 案例指出 promotion、shard move、故障恢復是三類主要事件來源、傳統 invalidation 在大規模系統難以維持穩定。</p>
<p><strong>三類事件的典型機制</strong>（具體實作依 cluster 設計而異）：</p>
<ul>
<li><strong>Promotion / failover</strong>：primary 切到 replica 過程中、寫入順序可能跨節點不一致、replica 變 primary 後可能讀到舊資料</li>
<li><strong>Shard move / rebalance</strong>：cluster topology 變更時、部分 key 在搬遷窗口內可能讀到舊 shard 的副本</li>
<li><strong>故障恢復</strong>：節點重啟後、cache 從 backing store 重建、跟 application 寫入的新值可能交錯</li>
</ul>
<p>在這些事件中、cache 拓樸隨著事件改變、需要追蹤 mutation 收斂、不只清 key。Meta 的解法是把 <em>mutation tracing</em> 制度化、追蹤每次資料變動是否在所有 cache 副本都收斂。</p>
<h3 id="mutation-tracing-跟一致性指標">Mutation tracing 跟一致性指標</h3>
<p>Mutation tracing 是 <em>資料變動到所有 cache 副本收斂的時間軸</em> 追蹤、跟一般 cache hit rate 屬不同維度。常見的工程實踐指標（屬 case-derived 推論、非 Meta case 直接揭露具體 SLO）：</p>
<ul>
<li><strong>Inconsistency window</strong>：從 source-of-truth 寫入到所有 cache 副本反映的耗時（平均 / p99）</li>
<li><strong>Inconsistency rate</strong>：query 取到 stale 副本的比例</li>
<li><strong>Inconsistency duration distribution</strong>：stale 持續時間的分布（看長尾才能識別事故風險、平均值會掩蓋）</li>
</ul>
<p>這些指標要接到告警跟回退條件、用法接近一般 SLO（例：inconsistency window p99 超過 <em>服務可接受 stale window</em> 觸發保護動作）。具體門檻依業務型態定 — 付款 / 庫存 / 權限類資料的容忍可能在秒級、商品描述可能在分鐘級。</p>
<p>當 inconsistency window 突然拉長、可能是 invalidation pipeline 卡住或 cache topology 變更中、應觸發保護動作（停止寫入、降級到回源、或回退近期變更）。</p>
<p>對應 <a href="/blog/backend/04-observability/telemetry-data-quality/" data-link-title="4.17 Telemetry Data Quality" data-link-desc="把 missing signal、schema drift、sampling bias 與 timestamp skew 變成資料品質問題">4.17 telemetry data quality</a> — cache 一致性指標屬於 <em>資料品質指標</em>、要進 evidence chain、跟效能指標分開追蹤。</p>
<h2 id="origin-protection">Origin Protection</h2>
<p>Origin protection 的責任是避免 cache miss 把壓力集中打回資料庫或下游服務。快取越接近高流量路徑，越要把 miss 視為需要治理的事件。</p>
<p>保護策略包含：</p>
<ol>
<li><a href="/blog/backend/knowledge-cards/cache-warmup/" data-link-title="Cache Warmup" data-link-desc="說明服務啟動或活動前如何預先建立快取資料">cache warmup</a> 先建立熱門資料覆蓋。</li>
<li><a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight</a> 或 request coalescing 合併同 key 回源。</li>
<li>對回源設 rate limit、timeout 與 fallback。</li>
<li>對短暫找不到的結果使用短 TTL negative cache。</li>
</ol>
<p>這些策略的共同目標是優先保護正式狀態來源，再提升命中率與延遲表現。</p>
<h2 id="跨區一致性窗口">跨區一致性窗口</h2>
<p>當 cache 跨多 region 部署、一致性問題從「副本 vs source-of-truth」變成「副本 vs 副本」。同一個用戶在不同 region 看到 cache 內容差異、可能影響業務邏輯（庫存超賣、配額超用、權限延遲）。規模化的 cache 把跨區一致性窗口跟區域容錯設計納入同一模型、不是分開治理（對應 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6 Netflix EVCache</a> 跟 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2 Meta mcrouter</a>）。</p>
<p><strong>Strong sync</strong> 採每次寫入同步到所有 region、延遲高、可靠性高。適合付款 / 庫存 / 權限類資料 — 庫存超賣的代價是業務直接損失（賣出實際沒有的商品）、權限不一致的代價是越權或拒服務、付款延遲一致的代價是重複扣款。這些代價高到值得付跨 region <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a> 的 latency 成本。失敗代價路徑：跨 region quorum 不可達時 → 寫入失敗 → 用戶看到操作失敗、業務不繼續寫錯資料。</p>
<p><strong>Async with bounded staleness</strong> 寫入主 region、其他 region 在 N 秒內收斂、多數場景夠用、要明確 stale window。適合 B2B SaaS、社群動態、推薦資料 — 用戶可以接受短暫看到舊版內容、但長時間 stale 會影響體驗。失敗代價路徑：跨 region 同步 lag 增長 → 用戶看到不同版本內容 → 累積到 stale window 上限時觸發 alert 跟保護。</p>
<p><strong>Per-region cache</strong> 每 region 各自獨立、不跨區同步、靠 backing store 收斂。適合本地用戶為主的資料（區域電商、本地內容平台） — 同一用戶極少跨 region、跨區一致性需求低、為了少數情境付跨區同步成本不划算。失敗代價路徑：跨 region 操作的用戶看到 region 之間不一致 → 業務側手動補償或要求用戶重試。</p>
<p>判讀重點：選哪種跨區一致性跟「同一用戶會不會跨 region 操作」直接相關。全球漫遊用戶（旅遊、跨國商務）要更強的同步；本地用戶為主的服務可以 per-region。</p>
<h3 id="跨-cloud-部署的資料引力">跨 cloud 部署的資料引力</h3>
<p>當 application 跟 cache 不在同一 cloud / region、每次 cache lookup 吃跨網路 latency（視 region pair 而定、9.C35 觀察值為 5-30ms）。對「每次互動查多個 cache」的服務、5ms × 10 lookup = 50ms 額外延遲、用戶感受明顯。</p>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">9.C35 Snap KeyDB cross-cloud</a> — Snap 把 KeyDB cache 放在 GCP 上、減少跨 cloud cache lookup latency。資料引力原則：data 在哪、cache 跟著去、跨 cloud 走 batch sync 降頻、應用與 cache 共置主資料 cloud。</p>
<p><strong>Multi-cloud cache 部署原則</strong>：</p>
<ul>
<li><strong>同 cloud 內</strong>：cache + application + DB 都在同一 cloud、cache lookup 在 ms 級內</li>
<li><strong>跨 cloud 採 batch sync 降頻</strong>：低頻、高延遲容忍的資料同步（每小時 / 每天）、應用本地讀 cache</li>
<li><strong>應用與 cache 共置主資料 cloud</strong>：高頻、低延遲容忍的路徑跟主資料同 cloud、避免跨 cloud RTT</li>
</ul>
<p>判讀重點：multi-cloud 架構的 cache 設計要先確定 data 主要在哪個 cloud、其他 cloud 的 application 要靠 batch sync 拿資料。Snap 從 zero-day 就在 GCP、近年走 multi-cloud 時、把 KeyDB 留在 GCP（data 一直在的地方）、避免反向部署引發的隱性 latency。違反這原則會踩到用戶層難以 debug 的延遲瓶頸。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>快取服務選型前要先回答四個問題：</p>
<ol>
<li>快取值是可重建副本，還是被拿來做正式判斷。</li>
<li>每種值的 freshness window 是多久。</li>
<li>miss 時來源系統能承受多少回源 QPS。</li>
<li>錯誤資料要如何失效、降級與回寫事故證據。</li>
</ol>
<p>這些問題先回答後，才進入 Redis data structure、Memcached 設計、Valkey 相容性或 managed cache 的討論。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體快取服務文章要承接本篇的 copy boundary 與 freshness。Redis、Valkey、Memcached、DragonflyDB 或 managed cache 的比較，應先問它們如何支援 key 失效、TTL、eviction、warmup、回源保護與觀測訊號，再進入 command 或部署細節。</p>
<p>若服務需要嚴格 freshness，後續文章要比較事件失效、版本化 key、原子更新與 fallback 能力。若服務主要面對高讀取壓力，後續文章要比較連線模型、hot key 保護、memory policy 與 cluster/sharding 行為。若服務需要事故回退，後續文章要比較 key migration、dual read、metrics 與 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">rollback window</a>。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要進一步處理讀寫流程，接著讀 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside 與失效策略</a>。要把 freshness 放進 rollout 與停損，接著讀 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a>。</p>
]]></content:encoded></item><item><title>2.C7 Cloudflare：Cache Reserve 分層儲存快取</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/</guid><description>&lt;p>這個案例的核心責任是把快取從短期命中策略擴展到長期容量策略。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Cloudflare Cache Reserve 透過分層儲存延長快取可用性，降低 origin 回源成本。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當熱門資料長尾明顯，僅靠 edge cache 會有命中率上限，需引入分層儲存。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>定義 edge 與 reserve 的資料分層規則。&lt;/li>
&lt;li>把回源成本納入快取策略評估。&lt;/li>
&lt;li>監控命中率、延遲與儲存成本三者平衡。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://blog.cloudflare.com/introducing-cache-reserve/">Cloudflare Cache Reserve&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是把快取從短期命中策略擴展到長期容量策略。</p>
<h2 id="觀察">觀察</h2>
<p>Cloudflare Cache Reserve 透過分層儲存延長快取可用性，降低 origin 回源成本。</p>
<h2 id="判讀">判讀</h2>
<p>當熱門資料長尾明顯，僅靠 edge cache 會有命中率上限，需引入分層儲存。</p>
<h2 id="策略">策略</h2>
<ol>
<li>定義 edge 與 reserve 的資料分層規則。</li>
<li>把回源成本納入快取策略評估。</li>
<li>監控命中率、延遲與儲存成本三者平衡。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3</a> 與 <a href="/blog/backend/06-reliability/capacity-cost/" data-link-title="6.9 容量與成本邊界" data-link-desc="把容量規劃跟成本約束變成驗證輸入">6.9</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.cloudflare.com/introducing-cache-reserve/">Cloudflare Cache Reserve</a></li>
</ul>
]]></content:encoded></item><item><title>Caffeine</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/</guid><description>&lt;p>Caffeine 是 JVM 上的 high-performance process-local cache library、承擔三個責任：在 application 進程內（on-heap）提供奈秒到微秒級的 cache（沒有網路往返）、用 Window TinyLFU 淘汰演算法逼近最佳命中率（優於傳統 LRU）、提供 expire / refresh / size-based eviction 等完整 cache 語意。設計取捨偏向「最低延遲 + 最高命中率 + 嵌進 application」、是 Redis 之外的另一層 cache，不是 Redis 的替代。&lt;/p>
&lt;p>對「每個請求重複讀同一份小資料、Redis 的網路往返都嫌慢、資料可在每個實例各存一份」這條路徑、Caffeine 是 process-local 層的標準選擇。它常跟 Redis 組成兩層 cache（Caffeine L1 + Redis L2）、不是二選一。Caffeine 是 &lt;a href="https://github.com/google/guava">Guava Cache&lt;/a> 的後繼、由同作者重寫、Spring Boot 等框架的預設 local cache。&lt;/p>
&lt;h2 id="本章目標">本章目標&lt;/h2>
&lt;p>讀完本章後、你應該能：&lt;/p>
&lt;ol>
&lt;li>用 Maven / Gradle 引入 Caffeine、寫出基本 cache&lt;/li>
&lt;li>理解 Window TinyLFU 為何命中率優於 LRU&lt;/li>
&lt;li>設計 expire-after-write / refresh-after-write / 容量上限&lt;/li>
&lt;li>判斷 process-local cache 跟 Redis 的兩層 cache 分工&lt;/li>
&lt;li>評估跨實例 invalidation 的限制與 GC 壓力&lt;/li>
&lt;/ol>
&lt;h2 id="最短路徑引入-caffeine-寫一個-cache">最短路徑：引入 Caffeine 寫一個 cache&lt;/h2>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-xml" data-lang="xml">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c">&amp;lt;!-- Maven 依賴（version 為範例、實際以 Maven Central 最新為準、最後檢查日 2026-06-16）--&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="nt">&amp;lt;dependency&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;groupId&amp;gt;&lt;/span>com.github.ben-manes.caffeine&lt;span class="nt">&amp;lt;/groupId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;artifactId&amp;gt;&lt;/span>caffeine&lt;span class="nt">&amp;lt;/artifactId&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> &lt;span class="nt">&amp;lt;version&amp;gt;&lt;/span>3.2.4&lt;span class="nt">&amp;lt;/version&amp;gt;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="nt">&amp;lt;/dependency&amp;gt;&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>




&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1">// 基本 cache：容量上限 10000、寫入後 5 分鐘過期&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">Cache&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Caffeine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">newBuilder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">maximumSize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">10_000&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">expireAfterWrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Duration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ofMinutes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">5&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">();&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">user&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">cache&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getIfPresent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="c1">// loading cache：miss 時自動回源（取代手寫 cache-aside）&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">LoadingCache&lt;/span>&lt;span class="o">&amp;lt;&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="o">&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">loading&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">Caffeine&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">newBuilder&lt;/span>&lt;span class="p">()&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">maximumSize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">10_000&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">refreshAfterWrite&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">Duration&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">ofMinutes&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">1&lt;/span>&lt;span class="p">))&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 背景非同步 refresh、不阻塞讀&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">build&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">-&amp;gt;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">key&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// miss / refresh 時呼叫&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u2&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">loading&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:123&amp;#34;&lt;/span>&lt;span class="p">);&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Caffeine 是 library 不是 server、跑在 application JVM 內、無法 docker 獨立驗證；上面是依官方 API 的範例（API 以 &lt;a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki&lt;/a> 為準）。&lt;/p>
&lt;h2 id="日常操作與決策形狀">日常操作與決策形狀&lt;/h2>
&lt;h3 id="淘汰與過期策略">淘汰與過期策略&lt;/h3>
&lt;p>Caffeine 把 cache 行為拆成幾個正交的旋鈕。子議題：&lt;/p>
&lt;ul>
&lt;li>&lt;code>maximumSize&lt;/code> / &lt;code>maximumWeight&lt;/code>：容量上限（筆數或加權大小）、超過用 W-TinyLFU 淘汰&lt;/li>
&lt;li>&lt;code>expireAfterWrite&lt;/code>：寫入後固定時間過期（資料新鮮度上限）&lt;/li>
&lt;li>&lt;code>expireAfterAccess&lt;/code>：最後存取後過期（淘汰冷資料）&lt;/li>
&lt;li>&lt;code>refreshAfterWrite&lt;/code>：到期後背景 refresh、舊值先服務、不阻塞（跟 expire 不同）&lt;/li>
&lt;/ul>
&lt;h3 id="window-tinylfu-淘汰">Window TinyLFU 淘汰&lt;/h3>
&lt;p>子議題：&lt;/p></description><content:encoded><![CDATA[<p>Caffeine 是 JVM 上的 high-performance process-local cache library、承擔三個責任：在 application 進程內（on-heap）提供奈秒到微秒級的 cache（沒有網路往返）、用 Window TinyLFU 淘汰演算法逼近最佳命中率（優於傳統 LRU）、提供 expire / refresh / size-based eviction 等完整 cache 語意。設計取捨偏向「最低延遲 + 最高命中率 + 嵌進 application」、是 Redis 之外的另一層 cache，不是 Redis 的替代。</p>
<p>對「每個請求重複讀同一份小資料、Redis 的網路往返都嫌慢、資料可在每個實例各存一份」這條路徑、Caffeine 是 process-local 層的標準選擇。它常跟 Redis 組成兩層 cache（Caffeine L1 + Redis L2）、不是二選一。Caffeine 是 <a href="https://github.com/google/guava">Guava Cache</a> 的後繼、由同作者重寫、Spring Boot 等框架的預設 local cache。</p>
<h2 id="本章目標">本章目標</h2>
<p>讀完本章後、你應該能：</p>
<ol>
<li>用 Maven / Gradle 引入 Caffeine、寫出基本 cache</li>
<li>理解 Window TinyLFU 為何命中率優於 LRU</li>
<li>設計 expire-after-write / refresh-after-write / 容量上限</li>
<li>判斷 process-local cache 跟 Redis 的兩層 cache 分工</li>
<li>評估跨實例 invalidation 的限制與 GC 壓力</li>
</ol>
<h2 id="最短路徑引入-caffeine-寫一個-cache">最短路徑：引入 Caffeine 寫一個 cache</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-xml" data-lang="xml"><span class="line"><span class="ln">1</span><span class="cl"><span class="c">&lt;!-- Maven 依賴（version 為範例、實際以 Maven Central 最新為準、最後檢查日 2026-06-16）--&gt;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nt">&lt;dependency&gt;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&lt;groupId&gt;</span>com.github.ben-manes.caffeine<span class="nt">&lt;/groupId&gt;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nt">&lt;artifactId&gt;</span>caffeine<span class="nt">&lt;/artifactId&gt;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nt">&lt;version&gt;</span>3.2.4<span class="nt">&lt;/version&gt;</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="nt">&lt;/dependency&gt;</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// 基本 cache：容量上限 10000、寫入後 5 分鐘過期</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">Cache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">cache</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">expireAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofMinutes</span><span class="p">(</span><span class="n">5</span><span class="p">))</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="n">cache</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">user</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">cache</span><span class="p">.</span><span class="na">getIfPresent</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="c1">// loading cache：miss 時自動回源（取代手寫 cache-aside）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w"></span><span class="n">LoadingCache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">loading</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">refreshAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofMinutes</span><span class="p">(</span><span class="n">1</span><span class="p">))</span><span class="w">   </span><span class="c1">// 背景非同步 refresh、不阻塞讀</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">(</span><span class="n">key</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">userRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">key</span><span class="p">));</span><span class="w"> </span><span class="c1">// miss / refresh 時呼叫</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="n">User</span><span class="w"> </span><span class="n">u2</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">loading</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">&#34;user:123&#34;</span><span class="p">);</span></span></span></code></pre></div><p>Caffeine 是 library 不是 server、跑在 application JVM 內、無法 docker 獨立驗證；上面是依官方 API 的範例（API 以 <a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki</a> 為準）。</p>
<h2 id="日常操作與決策形狀">日常操作與決策形狀</h2>
<h3 id="淘汰與過期策略">淘汰與過期策略</h3>
<p>Caffeine 把 cache 行為拆成幾個正交的旋鈕。子議題：</p>
<ul>
<li><code>maximumSize</code> / <code>maximumWeight</code>：容量上限（筆數或加權大小）、超過用 W-TinyLFU 淘汰</li>
<li><code>expireAfterWrite</code>：寫入後固定時間過期（資料新鮮度上限）</li>
<li><code>expireAfterAccess</code>：最後存取後過期（淘汰冷資料）</li>
<li><code>refreshAfterWrite</code>：到期後背景 refresh、舊值先服務、不阻塞（跟 expire 不同）</li>
</ul>
<h3 id="window-tinylfu-淘汰">Window TinyLFU 淘汰</h3>
<p>子議題：</p>
<ul>
<li>W-TinyLFU 結合 recency（window）+ frequency（TinyLFU sketch）、命中率逼近最佳</li>
<li>比 LRU 更抗一次性掃描污染（scan resistance）、跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis LFU</a> 的動機類似但演算法更先進</li>
<li>frequency 用 count-min sketch 近似、記憶體開銷小</li>
</ul>
<h3 id="兩層-cachel1-caffeine--l2-redis">兩層 cache（L1 Caffeine + L2 Redis）</h3>
<p>子議題：</p>
<ul>
<li>L1 Caffeine（process-local、奈秒級、每實例一份）擋掉大部分讀</li>
<li>L2 Redis（共享、毫秒級、跨實例一致）擋掉 L1 miss</li>
<li>對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency 的 hot key 兩層解法</a></li>
</ul>
<h2 id="進階主題按需閱讀">進階主題（按需閱讀）</h2>
<h3 id="跨實例-invalidation-的根本限制">跨實例 invalidation 的根本限制</h3>
<p>子議題：</p>
<ul>
<li>每個 JVM 實例有自己的 Caffeine 副本、一個實例更新不會通知其他實例</li>
<li>解法：短 TTL 容忍 stale、或用 Redis pub/sub 廣播 invalidation 訊息給各實例</li>
<li>這是 process-local cache 的固有取捨：最低延遲換來最弱的跨實例一致性</li>
</ul>
<h3 id="gc-壓力與-on-heap-vs-off-heap">GC 壓力與 on-heap vs off-heap</h3>
<p>子議題：</p>
<ul>
<li>Caffeine 預設 on-heap、大 cache 會增加 JVM heap 與 GC 壓力</li>
<li>容量上限要對齊 heap 預算、避免 cache 把 heap 撐爆觸發 full GC</li>
<li>極大 local cache 考慮 off-heap 方案（如 Ehcache 的 off-heap tier），但 Caffeine 本身專注 on-heap</li>
</ul>
<h3 id="async-與-refresh-語意">async 與 refresh 語意</h3>
<p>子議題：</p>
<ul>
<li><code>AsyncCache</code> / <code>AsyncLoadingCache</code>：回傳 CompletableFuture、不阻塞 caller</li>
<li><code>refreshAfterWrite</code>：到期後第一個讀觸發背景 refresh、舊值立即回、避免 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">stampede</a></li>
<li>refresh vs expire 的差異是「舊值能不能先服務」</li>
</ul>
<h2 id="排錯快速判讀">排錯快速判讀</h2>
<h3 id="跨實例讀到舊值">跨實例讀到舊值</h3>
<p>操作原則：process-local cache 各實例獨立、更新不傳播。判讀：縮短 TTL 容忍 stale、或加 Redis pub/sub 廣播 invalidation；強一致需求不該用 process-local cache。</p>
<h3 id="命中率低--cache-沒效果">命中率低 / cache 沒效果</h3>
<p>操作原則：先看 <code>maximumSize</code> 是否太小（working set 放不下）、再看 TTL 是否太短。判讀：用 <code>recordStats()</code> 看 hit rate / eviction count、對齊 working set。</p>
<h3 id="full-gc-頻繁">Full GC 頻繁</h3>
<p>操作原則：on-heap cache 太大撐爆 heap。判讀：降 <code>maximumSize</code> 或用 <code>maximumWeight</code> 控制實際記憶體、對齊 JVM heap 預算。</p>
<h2 id="何時改走其他服務">何時改走其他服務</h2>
<table>
  <thead>
      <tr>
          <th>需求形狀</th>
          <th>改走</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>需要跨實例共享 / 一致</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a>（共享 cache 層）</td>
      </tr>
      <tr>
          <td>非 JVM 語言</td>
          <td>該語言的 process-local cache（Go ristretto、Python cachetools 等）</td>
      </tr>
      <tr>
          <td>需要持久化 / durable</td>
          <td>Redis with AOF / AWS MemoryDB</td>
      </tr>
      <tr>
          <td>極大 cache 超過 heap</td>
          <td>off-heap cache（Ehcache off-heap）或外部 cache（Redis）</td>
      </tr>
      <tr>
          <td>不想管容量 / serverless</td>
          <td><a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a>（serverless、但有網路延遲）</td>
      </tr>
  </tbody>
</table>
<h2 id="不在本頁內的主題">不在本頁內的主題</h2>
<ul>
<li>Caffeine 完整 API（以官方 wiki 為準）</li>
<li>各 JVM 框架（Spring Cache abstraction）的整合細節</li>
<li>Guava Cache 到 Caffeine 的完整 API 對照</li>
<li>off-heap cache 方案比較</li>
</ul>
<h2 id="案例回寫">案例回寫</h2>
<h3 id="跨-vendor-對照本模組-case-庫暫無-caffeine-specific-case">跨 vendor 對照（本模組 case 庫暫無 Caffeine-specific case）</h3>
<p>Caffeine 是 library 層元件、本 blog cache case 庫（Meta / Shopify / Netflix / Cloudflare / Tinder / Tubi / Snap）暫無 Caffeine-specific case。以下用 process-local 的角度對照。</p>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>對 Caffeine 的對應</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4 Meta CacheLib + Kangaroo</a></td>
          <td>CacheLib 是 C++ 的 process-local + flash 分層 library、Caffeine 是 JVM 的 on-heap 對應</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8 Meta TAO</a></td>
          <td>TAO 有 application-tier local cache、process-local 擋掉大部分讀的思路一致</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a></td>
          <td>每次互動查多個 cache、process-local L1 可擋掉重複讀、降低 L2（Redis）的 RTT 壓力</td>
      </tr>
  </tbody>
</table>
<p><strong>待補 Caffeine-specific 案例</strong>：L1 Caffeine + L2 Redis 兩層 cache 的 production 命中率分層數據、跨實例 invalidation 的 Redis pub/sub 廣播實作、W-TinyLFU vs LRU 的實測命中率對照。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>deep article：<a href="/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/" data-link-title="Caffeine &#43; Redis 兩層 cache：搭起來很容易，跨實例失效才是全部的問題" data-link-desc="L1 Caffeine（process-local）&#43; L2 Redis（共享）的兩層 cache 程式碼三十行就寫完，但每個 JVM 實例有自己的 L1 副本、一個實例更新不會通知其他實例——跨實例 invalidation 才是這個架構的全部難度。本文展開兩層讀寫路徑、用 Redis pub/sub 廣播失效、5 個把 L1 stale 與 GC 寫成事故的 production 踩坑，以及哪些資料適合放 L1">Caffeine + Redis 兩層 cache 與跨實例失效</a>（L1+L2 + pub/sub 廣播失效）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>（hot key 兩層解法）、<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL eviction</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（兩層 cache 的 L2）、<a href="/blog/backend/02-cache-redis/vendors/momento/" data-link-title="Momento" data-link-desc="Serverless cache、按用量計費、無容量規劃">Momento</a>（另一端：serverless）</li>
<li>下游能力：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a>（跨實例一致性窗口）</li>
</ul>
]]></content:encoded></item><item><title>2.8 Cache Data Shape 與 Access Pattern</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/</guid><description>&lt;p>Cache data shape 與 access pattern 的核心責任是讓快取資料結構反映服務語意。進入 Redis command 或特定快取服務前，讀者需要先知道 key、value、hash、set、sorted set、stream 與多層 cache 各自適合承擔哪種讀取責任。&lt;/p>
&lt;h2 id="key-space">Key Space&lt;/h2>
&lt;p>Key space 的責任是定義快取資料如何被定位、分組、失效與遷移。key 命名要包含資料責任、版本、租戶或區域等必要維度，讓失效與回退可控。&lt;/p>
&lt;p>常見 key 維度包含：&lt;/p>
&lt;ol>
&lt;li>資料類型，例如 &lt;code>product&lt;/code>、&lt;code>user-permission&lt;/code>、&lt;code>quota&lt;/code>。&lt;/li>
&lt;li>版本，例如 &lt;code>v1&lt;/code>、&lt;code>v2&lt;/code>。&lt;/li>
&lt;li>租戶或區域，例如 tenant、region、locale。&lt;/li>
&lt;li>實體識別，例如 product id、user id。&lt;/li>
&lt;/ol>
&lt;p>key 缺少版本時，cache migration 會變成破壞性替換。key 缺少租戶或區域時，失效範圍會被放大。&lt;/p>
&lt;h2 id="value-shape">Value Shape&lt;/h2>
&lt;p>Value shape 的責任是定義快取值的語意與演進方式。完整 JSON blob 適合一次讀取完整資料，但欄位更新與版本相容成本高；hash 適合欄位局部更新，但需要明確欄位責任；set 與 sorted set 適合集合與排名；counter 適合限流或計數。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料形狀&lt;/th>
 &lt;th>適合場景&lt;/th>
 &lt;th>主要風險&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>string / blob&lt;/td>
 &lt;td>商品詳情、設定快照&lt;/td>
 &lt;td>schema 變更容易破壞相容&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>hash&lt;/td>
 &lt;td>使用者摘要、商品局部欄位&lt;/td>
 &lt;td>欄位責任不清會變成半正式狀態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>set&lt;/td>
 &lt;td>membership、權限集合&lt;/td>
 &lt;td>stale membership 可能造成越權&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>sorted set&lt;/td>
 &lt;td>排名、時間排序、優先級&lt;/td>
 &lt;td>score 語意錯誤會造成排序漂移&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>counter&lt;/td>
 &lt;td>rate limit、配額&lt;/td>
 &lt;td>原子性與過期窗口要對齊&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>stream&lt;/td>
 &lt;td>輕量事件流&lt;/td>
 &lt;td>容易和正式 message queue 責任混淆&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>資料形狀的本質是服務責任選擇，Redis 語法是落地方式。&lt;/p>
&lt;p>&lt;code>string / blob&lt;/code> 的判讀重點是整包資料是否需要一起讀取與一起失效。&lt;code>hash&lt;/code> 的判讀重點是欄位是否真的能獨立更新。&lt;code>set&lt;/code> 與 &lt;code>sorted set&lt;/code> 的判讀重點是 membership 或排序錯誤會造成什麼後果。&lt;code>counter&lt;/code> 的判讀重點是原子性與過期窗口。&lt;code>stream&lt;/code> 的判讀重點是這條路徑是否已經接近 message queue 責任。&lt;/p>
&lt;h2 id="access-pattern">Access Pattern&lt;/h2>
&lt;p>Access pattern 的責任是定義快取面對的讀寫節奏。高讀低寫、熱點讀取、短期活動尖峰、租戶隔離與跨區讀取，都會影響 key 設計與容量策略。&lt;/p>
&lt;p>高讀低寫適合長 TTL 與背景刷新；熱點讀取需要 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key&lt;/a> 保護；短期尖峰需要 warmup 與分散過期；多租戶場景需要避免單租戶 key 壓垮共享 cache。&lt;/p>
&lt;h2 id="multi-layer-cache">Multi-layer Cache&lt;/h2>
&lt;p>多層快取的責任是分散延遲與來源壓力。常見層次包含 process local cache、distributed cache、CDN 或 search/read model。每一層都需要定義 freshness、失效來源與 fallback。&lt;/p>
&lt;p>多層 cache 的主要風險是 stale 疊加。local cache stale、distributed cache stale 與 CDN stale 缺少共同失效策略時，讀者看到的錯誤會很難追。&lt;/p>
&lt;h3 id="ml-feature-store-的多層-cache-設計模式">ML feature store 的多層 cache 設計模式&lt;/h3>
&lt;p>ML inference 場景的 feature lookup 是多層 cache 的典型應用。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store&lt;/a> 的策略段提出 &lt;em>可重用做法&lt;/em>：用 L1 in-process cache + L2 distributed cache + L3 持久 store 三層。Tubi 實做的是把 feature store 從 ScyllaDB 遷到 ElastiCache（屬於 L2 層的選擇）、p99 &amp;lt; 10ms；三層架構是策略段推導出的通用設計、不一定 Tubi 完整實做。&lt;/p></description><content:encoded><![CDATA[<p>Cache data shape 與 access pattern 的核心責任是讓快取資料結構反映服務語意。進入 Redis command 或特定快取服務前，讀者需要先知道 key、value、hash、set、sorted set、stream 與多層 cache 各自適合承擔哪種讀取責任。</p>
<h2 id="key-space">Key Space</h2>
<p>Key space 的責任是定義快取資料如何被定位、分組、失效與遷移。key 命名要包含資料責任、版本、租戶或區域等必要維度，讓失效與回退可控。</p>
<p>常見 key 維度包含：</p>
<ol>
<li>資料類型，例如 <code>product</code>、<code>user-permission</code>、<code>quota</code>。</li>
<li>版本，例如 <code>v1</code>、<code>v2</code>。</li>
<li>租戶或區域，例如 tenant、region、locale。</li>
<li>實體識別，例如 product id、user id。</li>
</ol>
<p>key 缺少版本時，cache migration 會變成破壞性替換。key 缺少租戶或區域時，失效範圍會被放大。</p>
<h2 id="value-shape">Value Shape</h2>
<p>Value shape 的責任是定義快取值的語意與演進方式。完整 JSON blob 適合一次讀取完整資料，但欄位更新與版本相容成本高；hash 適合欄位局部更新，但需要明確欄位責任；set 與 sorted set 適合集合與排名；counter 適合限流或計數。</p>
<table>
  <thead>
      <tr>
          <th>資料形狀</th>
          <th>適合場景</th>
          <th>主要風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>string / blob</td>
          <td>商品詳情、設定快照</td>
          <td>schema 變更容易破壞相容</td>
      </tr>
      <tr>
          <td>hash</td>
          <td>使用者摘要、商品局部欄位</td>
          <td>欄位責任不清會變成半正式狀態</td>
      </tr>
      <tr>
          <td>set</td>
          <td>membership、權限集合</td>
          <td>stale membership 可能造成越權</td>
      </tr>
      <tr>
          <td>sorted set</td>
          <td>排名、時間排序、優先級</td>
          <td>score 語意錯誤會造成排序漂移</td>
      </tr>
      <tr>
          <td>counter</td>
          <td>rate limit、配額</td>
          <td>原子性與過期窗口要對齊</td>
      </tr>
      <tr>
          <td>stream</td>
          <td>輕量事件流</td>
          <td>容易和正式 message queue 責任混淆</td>
      </tr>
  </tbody>
</table>
<p>資料形狀的本質是服務責任選擇，Redis 語法是落地方式。</p>
<p><code>string / blob</code> 的判讀重點是整包資料是否需要一起讀取與一起失效。<code>hash</code> 的判讀重點是欄位是否真的能獨立更新。<code>set</code> 與 <code>sorted set</code> 的判讀重點是 membership 或排序錯誤會造成什麼後果。<code>counter</code> 的判讀重點是原子性與過期窗口。<code>stream</code> 的判讀重點是這條路徑是否已經接近 message queue 責任。</p>
<h2 id="access-pattern">Access Pattern</h2>
<p>Access pattern 的責任是定義快取面對的讀寫節奏。高讀低寫、熱點讀取、短期活動尖峰、租戶隔離與跨區讀取，都會影響 key 設計與容量策略。</p>
<p>高讀低寫適合長 TTL 與背景刷新；熱點讀取需要 <a href="/blog/backend/knowledge-cards/hot-key/" data-link-title="Hot Key" data-link-desc="說明單一 key 承受大量讀寫時如何形成容量瓶頸">hot key</a> 保護；短期尖峰需要 warmup 與分散過期；多租戶場景需要避免單租戶 key 壓垮共享 cache。</p>
<h2 id="multi-layer-cache">Multi-layer Cache</h2>
<p>多層快取的責任是分散延遲與來源壓力。常見層次包含 process local cache、distributed cache、CDN 或 search/read model。每一層都需要定義 freshness、失效來源與 fallback。</p>
<p>多層 cache 的主要風險是 stale 疊加。local cache stale、distributed cache stale 與 CDN stale 缺少共同失效策略時，讀者看到的錯誤會很難追。</p>
<h3 id="ml-feature-store-的多層-cache-設計模式">ML feature store 的多層 cache 設計模式</h3>
<p>ML inference 場景的 feature lookup 是多層 cache 的典型應用。<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi feature store</a> 的策略段提出 <em>可重用做法</em>：用 L1 in-process cache + L2 distributed cache + L3 持久 store 三層。Tubi 實做的是把 feature store 從 ScyllaDB 遷到 ElastiCache（屬於 L2 層的選擇）、p99 &lt; 10ms；三層架構是策略段推導出的通用設計、不一定 Tubi 完整實做。</p>
<p><strong>通用三層模式</strong>（推導自 9.C25 策略段、實際分層深度視 workload）：</p>
<ul>
<li><strong>L1：in-process cache</strong>：跟 application 同一 process、避免 network hop、適合最熱的少量 features</li>
<li><strong>L2：distributed cache</strong>（ElastiCache / Memcached）：跨 application instance 共享、能擴容、Tubi 在這層用 ElastiCache 達 p99 &lt; 10ms</li>
<li><strong>L3：持久 store</strong>（ScyllaDB / DynamoDB / S3 + Parquet）：全量資料、cache miss 時的 fallback</li>
</ul>
<p>判讀重點：每層的 latency budget 跟 stale window 都應依 workload 跟業務容忍度設定。相對序列是 L1 stale window 最嚴、L2 中等、L3 為 source-of-truth 或可重算來源。三層 stale 若無共同失效策略、業務代價會落到 <em>推薦結果不穩定</em>、用戶看到不同 session 推不同內容。</p>
<h3 id="跨-cloud-部署的資料引力路由見-27">跨 cloud 部署的資料引力（路由：見 2.7）</h3>
<p>跨 cloud cache 部署的 <em>資料引力</em> 原則跟 <em>跨區一致性</em> 議題密切相關、主寫場域是 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的跨區一致性窗口</a>。本章從 <em>data shape / access pattern</em> 角度補充：當 cache value 包含跨 region 共享的業務資料時、access pattern 自然偏向 <em>同 cloud read</em> + <em>跨 cloud batch sync</em>、不適合即時跨 cloud lookup。詳見 9.C35 Snap KeyDB 案例。</p>
<h2 id="選型前判準">選型前判準</h2>
<p>快取資料形狀選型前要先回答：</p>
<ol>
<li>讀取是單 key、批次 key、集合、排序還是計數。</li>
<li>寫入是整體替換、局部更新、追加還是原子遞增。</li>
<li>失效是單 key、群組、版本、租戶還是全域。</li>
<li>資料結構是否會讓快取承擔正式狀態責任。</li>
</ol>
<p>這些問題決定後續要比較 Redis data type、Memcached blob、CDN cache 或應用端 local cache。</p>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體快取服務文章要承接本篇的 data shape 與 access pattern。Redis/Valkey 的 hash、set、sorted set、stream 能表達多種資料形狀；Memcached 偏向簡單 key/value blob；CDN 與 local cache 則承擔不同層次的讀取加速。比較服務時要先問 access pattern，再問語法。</p>
<p>若讀取是單 key 或 blob，後續文章要比較 serialization、value size、TTL 與 eviction。若讀取是集合、排名或計數，後續文章要比較資料結構、原子性與容量行為。若讀取跨多層 cache，後續文章要比較失效傳播、stale 疊加與 observability。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 TTL 與容量策略，接著讀 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>。要看選定形狀後各型別的操作語意、原子性與記憶體曲線，接著讀 <a href="/blog/backend/02-cache-redis/redis-data-types/" data-link-title="2.11 Redis data types 實作" data-link-desc="說明 sorted set、bitmap、HyperLogLog、counter 與 hash 各自承擔的服務語意、容量行為與原子性邊界">2.11 Redis data types 實作</a>。要處理 presence 類即時狀態，接著讀 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store 與即時狀態</a>。</p>
]]></content:encoded></item><item><title>2.C8 Meta：TAO 社交圖快取演進</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/</guid><description>&lt;p>這個案例的核心責任是說明快取在高關聯查詢場景會接近資料庫層角色。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Meta TAO 用於社交圖讀取，演進重點在一致性、可擴展性與資料關聯查詢效率。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>當查詢負載是高度關聯圖資料，快取策略需從 key-value 轉向資料模型治理。&lt;/p>
&lt;h2 id="策略">策略&lt;/h2>
&lt;ol>
&lt;li>把資料關聯模型納入快取鍵設計。&lt;/li>
&lt;li>以一致性窗口設計更新策略。&lt;/li>
&lt;li>定期驗證讀取正確性與延遲目標。&lt;/li>
&lt;/ol>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/">TAO: Facebook&amp;rsquo;s Distributed Data Store&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明快取在高關聯查詢場景會接近資料庫層角色。</p>
<h2 id="觀察">觀察</h2>
<p>Meta TAO 用於社交圖讀取，演進重點在一致性、可擴展性與資料關聯查詢效率。</p>
<h2 id="判讀">判讀</h2>
<p>當查詢負載是高度關聯圖資料，快取策略需從 key-value 轉向資料模型治理。</p>
<h2 id="策略">策略</h2>
<ol>
<li>把資料關聯模型納入快取鍵設計。</li>
<li>以一致性窗口設計更新策略。</li>
<li>定期驗證讀取正確性與延遲目標。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a> 與 <a href="/blog/backend/01-database/schema-design/" data-link-title="1.2 Schema Design 與資料建模" data-link-desc="整理 table、index、key、partition、denormalization 與命名規則">1.2</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://engineering.fb.com/2013/06/25/core-infra/tao-the-power-of-the-graph/">TAO: Facebook&rsquo;s Distributed Data Store</a></li>
</ul>
]]></content:encoded></item><item><title>2.9 Cache Migration 與 Stampede Rollback（實作示範）</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/</link><pubDate>Mon, 11 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cache-migration-stampede-rollback/</guid><description>&lt;p>Cache migration 與 stampede rollback 的核心責任是讓快取副本在格式、鍵名與覆蓋範圍演進時，仍能保護 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 不被回源流量打穿。這篇以商品詳情與價格快取為例，示範如何把 key schema 演進、freshness 控制、warmup、放行與停損交給可交接 artifact。&lt;/p>
&lt;h2 id="服務路徑與失敗代價">服務路徑與失敗代價&lt;/h2>
&lt;p>這條路徑是 &lt;code>product-page -&amp;gt; cache -&amp;gt; product-db/pricing-service&lt;/code>。商品頁會同時讀取描述、價格、庫存與促銷標籤，快取需要在低延遲與正確性間平衡。&lt;/p>
&lt;p>這篇示範的變更是把舊 key &lt;code>product:{id}&lt;/code> 演進到版本化 key &lt;code>product:v2:{region}:{id}&lt;/code>。演進動機是支援區域價格與促銷欄位拆分，避免舊序列化格式在多區域路徑下持續膨脹。&lt;/p>
&lt;p>失敗代價分三層：描述欄位 stale 主要影響體驗，價格 stale 直接影響交易正確性，回源尖峰會擠壓正式狀態查詢容量。這三層要分別設 freshness、gate 與 rollback 條件。&lt;/p>
&lt;h2 id="key-schema-與相容窗口">Key Schema 與相容窗口&lt;/h2>
&lt;p>Key schema 的責任是讓新舊值可共存，不讓切換變成一次性替換。這條路徑採 &lt;code>dual-read&lt;/code> 再 &lt;code>dual-write&lt;/code> 再 &lt;code>single-read-v2&lt;/code>：&lt;/p>
&lt;ol>
&lt;li>讀取先查 &lt;code>v2&lt;/code>，miss 再查舊 key，最後才回源。&lt;/li>
&lt;li>回填期間新舊 key 同時寫入，保留可回退窗口。&lt;/li>
&lt;li>&lt;code>v2&lt;/code> 命中穩定後，關閉舊 key 寫入，保留舊 key 讀 fallback 一段時間。&lt;/li>
&lt;/ol>
&lt;p>相容窗口的重點是讀語意一致。舊 key 與新 key 的值結構不同時，要先有轉換層，避免同一商品在不同 API path 回傳不同語意。&lt;/p>
&lt;h2 id="freshness-window-與資料分級">Freshness Window 與資料分級&lt;/h2>
&lt;p>Freshness window 的責任是把 stale 代價寫成可執行規則，而不是只寫全域 TTL。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>資料欄位&lt;/th>
 &lt;th>freshness window&lt;/th>
 &lt;th>原因&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>商品描述&lt;/td>
 &lt;td>5-15 分鐘&lt;/td>
 &lt;td>體驗導向，短時間 stale 可接受&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>促銷標籤&lt;/td>
 &lt;td>1-3 分鐘&lt;/td>
 &lt;td>促銷切換頻繁，錯誤會影響轉換率&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>庫存可售狀態&lt;/td>
 &lt;td>10-30 秒&lt;/td>
 &lt;td>超賣風險高，需接近即時&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>價格與幣別&lt;/td>
 &lt;td>5-15 秒&lt;/td>
 &lt;td>交易正確性高風險，需短 TTL 並搭配事件失效&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗回源保護值&lt;/td>
 &lt;td>3-10 秒&lt;/td>
 &lt;td>下游暫時異常時保護來源，避免反覆 miss 放大回源壓力&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL&lt;/a> 與事件失效要同時存在。TTL 控上限，事件失效控即時性；只用其一都會造成隱性風險。&lt;/p>
&lt;h2 id="warmup-與回源保護">Warmup 與回源保護&lt;/h2>
&lt;p>Warmup 的責任是先建立新 key 的可服務覆蓋率，再擴大流量。這條路徑採分批 warmup：&lt;code>region -&amp;gt; category -&amp;gt; hot key list -&amp;gt; 全量&lt;/code>。&lt;/p>
&lt;p>Warmup completion 的判讀訊號：&lt;/p>
&lt;ol>
&lt;li>&lt;code>v2&lt;/code> 命中率在目標區間連續穩定。&lt;/li>
&lt;li>origin QPS 未突破上限。&lt;/li>
&lt;li>熱門 key 的 miss 尖峰已被抹平。&lt;/li>
&lt;/ol>
&lt;p>回源保護策略：&lt;/p>
&lt;ol>
&lt;li>以 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight&lt;/a> 合併同 key 同時 miss。&lt;/li>
&lt;li>對回源查詢設 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit&lt;/a> 與超時。&lt;/li>
&lt;li>回源失敗時寫入短 TTL 降級值，避免瞬時重試風暴。&lt;/li>
&lt;li>針對熱門 key 在切換前做預熱與分散過期。&lt;/li>
&lt;/ol>
&lt;h3 id="cache-切換引發-stampede-的真實事故結構">Cache 切換引發 stampede 的真實事故結構&lt;/h3>
&lt;p>對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：Cache Stampede Rollout Regression&lt;/a> — 看似低風險的 cache key 或 TTL 切換、若回源保護不足、會讓熱門資料同時 miss。事故結構屬「讀取路徑同時失去緩衝」的系統性失敗、不只是單一 key 問題。&lt;/p>
&lt;p>切換引發 stampede 的三個放大機制會 &lt;em>疊加&lt;/em>、不是獨立失效。在 read-heavy 規模化服務（如 Tinder 47M MAU、Tubi feature store）這類場景、典型疊加順序：重試放大先觸發 → 下游放大跟進 → 應用層放大終結：&lt;/p></description><content:encoded><![CDATA[<p>Cache migration 與 stampede rollback 的核心責任是讓快取副本在格式、鍵名與覆蓋範圍演進時，仍能保護 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 不被回源流量打穿。這篇以商品詳情與價格快取為例，示範如何把 key schema 演進、freshness 控制、warmup、放行與停損交給可交接 artifact。</p>
<h2 id="服務路徑與失敗代價">服務路徑與失敗代價</h2>
<p>這條路徑是 <code>product-page -&gt; cache -&gt; product-db/pricing-service</code>。商品頁會同時讀取描述、價格、庫存與促銷標籤，快取需要在低延遲與正確性間平衡。</p>
<p>這篇示範的變更是把舊 key <code>product:{id}</code> 演進到版本化 key <code>product:v2:{region}:{id}</code>。演進動機是支援區域價格與促銷欄位拆分，避免舊序列化格式在多區域路徑下持續膨脹。</p>
<p>失敗代價分三層：描述欄位 stale 主要影響體驗，價格 stale 直接影響交易正確性，回源尖峰會擠壓正式狀態查詢容量。這三層要分別設 freshness、gate 與 rollback 條件。</p>
<h2 id="key-schema-與相容窗口">Key Schema 與相容窗口</h2>
<p>Key schema 的責任是讓新舊值可共存，不讓切換變成一次性替換。這條路徑採 <code>dual-read</code> 再 <code>dual-write</code> 再 <code>single-read-v2</code>：</p>
<ol>
<li>讀取先查 <code>v2</code>，miss 再查舊 key，最後才回源。</li>
<li>回填期間新舊 key 同時寫入，保留可回退窗口。</li>
<li><code>v2</code> 命中穩定後，關閉舊 key 寫入，保留舊 key 讀 fallback 一段時間。</li>
</ol>
<p>相容窗口的重點是讀語意一致。舊 key 與新 key 的值結構不同時，要先有轉換層，避免同一商品在不同 API path 回傳不同語意。</p>
<h2 id="freshness-window-與資料分級">Freshness Window 與資料分級</h2>
<p>Freshness window 的責任是把 stale 代價寫成可執行規則，而不是只寫全域 TTL。</p>
<table>
  <thead>
      <tr>
          <th>資料欄位</th>
          <th>freshness window</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>商品描述</td>
          <td>5-15 分鐘</td>
          <td>體驗導向，短時間 stale 可接受</td>
      </tr>
      <tr>
          <td>促銷標籤</td>
          <td>1-3 分鐘</td>
          <td>促銷切換頻繁，錯誤會影響轉換率</td>
      </tr>
      <tr>
          <td>庫存可售狀態</td>
          <td>10-30 秒</td>
          <td>超賣風險高，需接近即時</td>
      </tr>
      <tr>
          <td>價格與幣別</td>
          <td>5-15 秒</td>
          <td>交易正確性高風險，需短 TTL 並搭配事件失效</td>
      </tr>
      <tr>
          <td>失敗回源保護值</td>
          <td>3-10 秒</td>
          <td>下游暫時異常時保護來源，避免反覆 miss 放大回源壓力</td>
      </tr>
  </tbody>
</table>
<p><a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">TTL</a> 與事件失效要同時存在。TTL 控上限，事件失效控即時性；只用其一都會造成隱性風險。</p>
<h2 id="warmup-與回源保護">Warmup 與回源保護</h2>
<p>Warmup 的責任是先建立新 key 的可服務覆蓋率，再擴大流量。這條路徑採分批 warmup：<code>region -&gt; category -&gt; hot key list -&gt; 全量</code>。</p>
<p>Warmup completion 的判讀訊號：</p>
<ol>
<li><code>v2</code> 命中率在目標區間連續穩定。</li>
<li>origin QPS 未突破上限。</li>
<li>熱門 key 的 miss 尖峰已被抹平。</li>
</ol>
<p>回源保護策略：</p>
<ol>
<li>以 <a href="/blog/backend/knowledge-cards/singleflight/" data-link-title="Singleflight" data-link-desc="說明相同工作同時發生時如何合併成一次下游請求">singleflight</a> 合併同 key 同時 miss。</li>
<li>對回源查詢設 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 與超時。</li>
<li>回源失敗時寫入短 TTL 降級值，避免瞬時重試風暴。</li>
<li>針對熱門 key 在切換前做預熱與分散過期。</li>
</ol>
<h3 id="cache-切換引發-stampede-的真實事故結構">Cache 切換引發 stampede 的真實事故結構</h3>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例：Cache Stampede Rollout Regression</a> — 看似低風險的 cache key 或 TTL 切換、若回源保護不足、會讓熱門資料同時 miss。事故結構屬「讀取路徑同時失去緩衝」的系統性失敗、不只是單一 key 問題。</p>
<p>切換引發 stampede 的三個放大機制會 <em>疊加</em>、不是獨立失效。在 read-heavy 規模化服務（如 Tinder 47M MAU、Tubi feature store）這類場景、典型疊加順序：重試放大先觸發 → 下游放大跟進 → 應用層放大終結：</p>
<ul>
<li><strong>重試放大</strong>：用戶請求 miss、應用層或 client SDK 內建重試、每次重試又 miss、單一用戶請求變多次 origin QPS</li>
<li><strong>下游放大</strong>：cache miss 同時打到 DB、DB 變慢、應用對 cache 設的 timeout 又觸發新 miss、回到 DB 更慢、形成正向循環</li>
<li><strong>應用層放大</strong>：等待 cache 的 request 堆積、application thread / connection pool 滿、新請求被拒、被拒的請求觸發更多重試</li>
</ul>
<p>判讀重點：stampede 的早期訊號通常出現在下游 origin（DB QPS 突然超 baseline 數倍）跟 application（latency p99 拉高、request queue length 增加）、不一定先在 cache 層看到。cache hit rate 顯示異常時、事故通常已在中後段。</p>
<h3 id="切換順序決定-stampede-風險">切換順序決定 stampede 風險</h3>
<p>對應 <a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10 對照：規模差異下的快取策略</a> — 切換順序（先改 key 結構 vs 先改 TTL）會決定是否出現 stampede 連鎖反應、特別在中型服務同時承受活動流量跟版本切換時。</p>
<p><strong>安全切換順序</strong>（dual-read 模式、每步停損點不同）：</p>
<ol>
<li><strong>新 key 寫入啟用</strong>：應用層同時寫舊 key + 新 key、讀路徑不變。停損點是「寫入失敗率」、若雙寫失敗率超基線、回退停止啟用。</li>
<li><strong>新 key 命中觀察</strong>：讀路徑加入 v2 first / fallback to v1 邏輯、v2 命中率隨自然回填爬升。停損點是「v2 hit rate 爬升曲線」、若曲線停滯、表示 warmup 沒擴散到熱資料、要先 manual warmup。</li>
<li><strong>舊 key 命中率穩定下降</strong>：表示新 key 自然 warmup 完成、可進入下一階段。停損點是「舊 key hit rate 是否真的降到目標」、不能只看 v2 hit rate。</li>
<li><strong>舊 key 寫入停止</strong>：只寫 v2、舊 key 自然 TTL 過期。停損點是「v2 唯一寫入是否穩定」、若出現 v2 寫入失敗、回退到雙寫。</li>
<li><strong>舊 key 讀 fallback 移除</strong>：完全切到 v2 only。停損點是「v2 hit rate 是否已達切換前舊 key 水位」、否則 fallback 移除後直接回源。</li>
</ol>
<p><strong>應該注意的反模式</strong>（會引發 stampede）：</p>
<ul>
<li>應先 warmup 新 key 再刪除舊 key、避免所有讀立即 miss</li>
<li>應拆維度切換（key OR TTL OR 序列化各自獨立）、避免多變化疊加讓 debug 困難</li>
<li>應先在低流量 region 試跑、再擴大到全量、避免事故時無回退時間</li>
</ul>
<p>判讀順序：每次切換只動 <em>一個維度</em>（key OR TTL OR 序列化）、先在低流量 region / tenant 試跑、命中率穩定後再擴大。在 Shopify 序列化遷移（<a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a>）類場景、停損 KPI 是「新格式編碼成功率」+「舊格式 fallback 觸發率」；在 Tinder 類 schema 變化頻繁場景、停損 KPI 是「v2 cache hit rate 是否在預估 warmup 時間內達標」。對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> 跟 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout Evidence</a> 的同類 expand-contract 思維。</p>
<h3 id="schema-變更引發的隱性-cache-invalidation路由見-27">Schema 變更引發的隱性 cache invalidation（路由：見 2.7）</h3>
<p>Cache invalidation <em>模型</em> 主寫於 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary 的 Invalidation 段</a>；本章從 migration <em>實作步驟</em> 角度補充：schema migration 是 cache stampede 的隱藏觸發點。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder</a> 案例的警惕段提出 <em>風險推測</em>：「configurable matching」業務邏輯複雜、快取資料的 schema 變化頻繁、一個 schema 變更可能引發 cache invalidation 風險。</p>
<p>Schema 變化讓 cache 失效的三種模式（屬工程實踐推導、非案例直接揭露）：</p>
<ul>
<li><strong>欄位重命名 / 刪除</strong>：舊 cache value 反序列化失敗、application 視為 miss、全部回源</li>
<li><strong>type 變更</strong>（int → string、enum 增 case）：反序列化可能成功但語意錯、業務邏輯踩錯</li>
<li><strong>序列化格式換</strong>（Marshal → MessagePack）：舊格式無法用新 decoder 讀、對應 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify</a> 的雙軌策略</li>
</ul>
<p><strong>Migration 實作步驟</strong>（按優先序）：</p>
<ol>
<li><strong>Schema migration 前盤點 cache key</strong>（最先）：哪些 cache 包含這個 schema 的資料、估算 invalid 範圍。沒這步無法估算 warmup 計畫規模。</li>
<li><strong>大規模 schema migration 配 cache warmup 計畫</strong>：預先 warmup、避免用戶觸發 cache miss。warmup 計畫主寫於本章的「Warmup 與回源保護」段。</li>
<li><strong>新欄位用 versioned key</strong>（同步進行）：<code>product:v2:{id}</code> 跟 <code>product:v1:{id}</code> 並存、避免雙寫干擾。對應 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify 雙軌策略</a>。</li>
<li><strong>降級 fallback</strong>（最後保險）：cache miss 後 origin 也準備好被打、避免假設「cache hit rate 永遠維持高水位」。對應本章「回源保護策略」段。</li>
</ol>
<p>判讀重點：四步應同步落地、缺一個就會在 migration 期間踩 stampede。一致性 invalidation 模型回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7</a>。</p>
<h2 id="rollout--cutover--rollback">Rollout / Cutover / Rollback</h2>
<p>Rollout 的責任是把快取切換拆成可停損批次，不把風險一次放大。</p>
<table>
  <thead>
      <tr>
          <th>階段</th>
          <th>判讀重點</th>
          <th>停損動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Dual read</td>
          <td><code>v2</code> miss 是否快速收斂</td>
          <td>維持舊 key 讀 fallback，暫停擴批</td>
      </tr>
      <tr>
          <td>Dual write</td>
          <td>新舊值語意是否一致</td>
          <td>停新格式寫入，保留舊格式</td>
      </tr>
      <tr>
          <td>Single read on <code>v2</code></td>
          <td>origin QPS 是否受控、價格 stale 是否達門檻</td>
          <td>回退到 dual read，恢復舊 key 讀路徑</td>
      </tr>
      <tr>
          <td>Contract old key</td>
          <td>舊 key 是否仍被依賴</td>
          <td>停 contract，延長相容窗口</td>
      </tr>
  </tbody>
</table>
<p>Rollback 不是只「切回舊 key」。若新格式已經被下游依賴，回退時要同時保留新舊讀寫相容，避免第二次不一致。</p>
<h2 id="evidence-package">Evidence Package</h2>
<p>快取 migration evidence 的責任是證明「效能提升」沒有交換成「來源壓力失控」或「交易資料錯誤」。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>cache metrics、origin metrics、query logs、warmup job logs</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/time-range/" data-link-title="Time Range" data-link-desc="說明證據、查詢與事故判讀如何用時間窗保留可回放上下文">Time range</a></td>
          <td>每個 rollout batch 的觀察窗口</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/query-link/" data-link-title="Query Link" data-link-desc="說明證據包如何保存可重跑查詢入口，而不是只保留截圖或口頭結論">Query link</a></td>
          <td>hit/miss、origin QPS、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale read</a>、eviction、latency 分布</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>cache owner、product owner、pricing owner</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/data-quality/" data-link-title="Data Quality" data-link-desc="說明證據欄位如何標示 completeness、freshness、sampling 與資料限制">Data quality</a></td>
          <td>指標延遲、抽樣覆蓋率、分區漏報</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/confidence/" data-link-title="Confidence" data-link-desc="說明證據包如何標示 confirmed、suspected 或 needs follow-up 的判讀信心">Confidence</a></td>
          <td>confirmed / suspected / needs follow-up</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/known-gap/" data-link-title="Known Gap" data-link-desc="說明證據包如何明確保存已知缺口，避免下游高估證據完整性">Known gap</a></td>
          <td>未涵蓋低流量區域、尚未演練的促銷尖峰窗口</td>
      </tr>
  </tbody>
</table>
<p>這份 evidence 要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="release-gate">Release Gate</h2>
<p>Release gate 的責任是決定是否放行下一批切換，而不是只報告觀測結果。</p>
<table>
  <thead>
      <tr>
          <th>Gate 欄位</th>
          <th>最小內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/knowledge-cards/gate-decision/" data-link-title="Gate Decision" data-link-desc="說明 release gate 如何把證據轉成放行、暫停、回退或補證據的決策">Gate decision</a></td>
          <td>放行下一批、維持當前批、回退到 dual read</td>
      </tr>
      <tr>
          <td>Checks</td>
          <td><code>v2</code> 命中率、origin QPS ceiling、stale price ratio</td>
      </tr>
      <tr>
          <td>Stop condition</td>
          <td>回源尖峰、價格 stale 超門檻、熱門 key miss 反彈</td>
      </tr>
      <tr>
          <td>Rollback window</td>
          <td>舊 key fallback 可維持時間、舊格式寫入可恢復時間</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>cache on-call、pricing on-call</td>
      </tr>
  </tbody>
</table>
<p>這組欄位要對齊 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">6.8 Release Gate</a> 與 <a href="/blog/backend/06-reliability/experiment-safety-boundary/" data-link-title="6.20 Experiment Safety Boundary" data-link-desc="定義 chaos、load test、DR drill 的 [blast radius](/backend/knowledge-cards/blast-radius/)、停止條件與權限約束">6.20 Experiment Safety Boundary</a>。</p>
<h2 id="incident-decision-log">Incident Decision Log</h2>
<p>切換過程中的停用新 key、延長 TTL、凍結 invalidation、回退讀路徑都屬於事故決策。每筆決策都要留在 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.19 Incident Decision Log</a>。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-yaml" data-lang="yaml"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="nt">incident_decision</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">  </span><span class="nt">timestamp</span><span class="p">:</span><span class="w"> </span><span class="ld">2026-05-11T11:42:00Z</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">  </span><span class="nt">decision</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;rollback to dual-read and freeze v2-only rollout&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">  </span><span class="nt">context</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;origin QPS exceeded ceiling and stale price ratio increased in TW region&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">  </span><span class="nt">evidence</span><span class="p">:</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">cache_v2_origin_qps_region_tw</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span>- <span class="nt">query</span><span class="p">:</span><span class="w"> </span><span class="l">stale_price_ratio_by_region</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">  </span><span class="nt">owner</span><span class="p">:</span><span class="w"> </span><span class="l">cache-incident-commander</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">  </span><span class="nt">expected_effect</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;reduce origin pressure and restore price freshness baseline&#34;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">  </span><span class="nt">rollback_condition</span><span class="p">:</span><span class="w"> </span><span class="s2">&#34;origin qps or stale ratio does not recover within 15 minutes&#34;</span></span></span></code></pre></div><h2 id="case-write-back-與邊界">Case Write-back 與邊界</h2>
<p>這篇回寫重點對齊 <a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3 Shopify：Cache Serialization Migration</a> 與 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 反例</a>：前者看格式演進與相容窗口，後者看回源尖峰與停損節奏。</p>
<p>這篇不處理分散式鎖正確性、queue replay 或資料庫正式狀態切換。若核心風險在互斥語意、事件重播或資料 schema，路由到 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.4 distributed lock</a>、<a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計與去重</a> 或 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a>。</p>
]]></content:encoded></item><item><title>2.C9 反例：快取切換引發 Stampede 回歸</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/</guid><description>&lt;p>這個反例的核心責任是說明快取轉換最常失敗在回源保護不足。&lt;/p>
&lt;h2 id="事故長相">事故長相&lt;/h2>
&lt;p>一次看似低風險的 cache key 或 TTL 切換，會讓熱門資料同時 miss。使用者看到的是 API 變慢與錯誤率上升，資料庫看到的是原本被快取吸收的流量突然全部回源。&lt;/p>
&lt;h2 id="為什麼會擴大">為什麼會擴大&lt;/h2>
&lt;p>快取切換如果沒有 warmup、singleflight、節流與降級保護，miss 會引發重試，重試又會增加 origin 壓力。影響面是讀取路徑同時失去緩衝，單一 key 層級的思考抓不到全貌。&lt;/p>
&lt;h2 id="回退判讀">回退判讀&lt;/h2>
&lt;p>回退不應只把程式版本切回去。若新舊快取 key、TTL 或序列化格式已經混在一起，回退還要處理資料可讀性與回源壓力。實務上要先降載或恢復舊 key 讀取，再逐步清理新策略留下的快取狀態。&lt;/p>
&lt;h2 id="快取專屬告警條件">快取專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>熱門 key miss 同步上升，且 origin QPS 快速超過平日基線&lt;/li>
&lt;li>response time 拉長並伴隨重試流量增加&lt;/li>
&lt;li>stale read 與 cache miss 同時惡化&lt;/li>
&lt;/ul>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明快取轉換最常失敗在回源保護不足。</p>
<h2 id="事故長相">事故長相</h2>
<p>一次看似低風險的 cache key 或 TTL 切換，會讓熱門資料同時 miss。使用者看到的是 API 變慢與錯誤率上升，資料庫看到的是原本被快取吸收的流量突然全部回源。</p>
<h2 id="為什麼會擴大">為什麼會擴大</h2>
<p>快取切換如果沒有 warmup、singleflight、節流與降級保護，miss 會引發重試，重試又會增加 origin 壓力。影響面是讀取路徑同時失去緩衝，單一 key 層級的思考抓不到全貌。</p>
<h2 id="回退判讀">回退判讀</h2>
<p>回退不應只把程式版本切回去。若新舊快取 key、TTL 或序列化格式已經混在一起，回退還要處理資料可讀性與回源壓力。實務上要先降載或恢復舊 key 讀取，再逐步清理新策略留下的快取狀態。</p>
<h2 id="快取專屬告警條件">快取專屬告警條件</h2>
<ul>
<li>熱門 key miss 同步上升，且 origin QPS 快速超過平日基線</li>
<li>response time 拉長並伴隨重試流量增加</li>
<li>stale read 與 cache miss 同時惡化</li>
</ul>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2</a> 與 <a href="/blog/backend/06-reliability/rule-rollout-safety-gate/" data-link-title="6.24 規則推送安全閘門" data-link-desc="把規則、策略與控制面配置推送從部署步驟升級為可靠性 gate，避免小變更在秒級擴散成全網事故。">6.24</a>。</p>
]]></content:encoded></item><item><title>2.10 Pub/Sub 與即時 fan-out</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/pub-sub/</guid><description>&lt;p>Redis &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub&lt;/a> 的核心責任是把一則訊息即時推送給當下所有訂閱者，讓跨節點的狀態變更可以在同一瞬間擴散。它承擔的是「現在發生的事，立刻讓所有人知道」，正式的可靠投遞與重播責任由 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams&lt;/a> 承擔。把這條邊界放在最前面，是因為 Pub/Sub 的多數事故都來自把它當成可靠訊息系統使用。&lt;/p>
&lt;h2 id="at-most-once訊息只送給此刻在線的訂閱者">at-most-once：訊息只送給此刻在線的訂閱者&lt;/h2>
&lt;p>訊息&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">投遞語意&lt;/a>有三種：at-most-once（最多送一次、可能漏）、at-least-once（至少送一次、可能重複）、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/exactly-once/" data-link-title="Exactly-Once" data-link-desc="說明訊息剛好被處理一次的語意承諾、它的代價，以及多數時候該用的替代路">exactly-once&lt;/a>（剛好一次、最難實作）。Pub/Sub 採 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">at-most-once&lt;/a>，用「可能漏」換取低延遲與無狀態，後兩種語意由 Streams 或 message queue 承擔。具體來說：&lt;code>PUBLISH&lt;/code> 把訊息送給發布當下已經 &lt;code>SUBSCRIBE&lt;/code> 該 channel 的連線，沒有訂閱者就直接丟棄，訊息不寫入任何持久結構。訂閱者離線、重連、或處理速度跟不上時，那段時間的訊息不會補送。&lt;/p>
&lt;p>這個語意決定了 Pub/Sub 適合承擔什麼。可以接受「偶爾漏一則、下一則狀態會蓋過來」的場景，Pub/Sub 的低延遲與簡單模型是優勢；要求「每一則都不能掉」的場景，例如訂單事件、扣款通知、稽核軌跡，這些責任屬於 durable queue，不該放在 Pub/Sub。&lt;/p>
&lt;p>判讀的關鍵問題是：漏掉一則訊息的代價是什麼。presence 狀態廣播漏一則，下次 heartbeat 會修正；cache invalidation 廣播漏一則，該節點會保留 stale 副本直到 TTL 到期，代價是短暫不一致；扣款事件漏一則，代價是金額錯誤且無法自動修復。前兩者落在 Pub/Sub 的能力範圍，第三者越界。&lt;/p>
&lt;h2 id="適用場景狀態變更的即時扇出">適用場景：狀態變更的即時扇出&lt;/h2>
&lt;p>Pub/Sub 的典型用途是把一個節點上發生的狀態變更，即時扇出給其他節點。這類場景的共同特徵是「最終狀態會自我修正」，所以單則訊息可丟。&lt;/p>
&lt;p>fan-out 有兩種語意要先分清，因為它們決定能不能用 Pub/Sub。一種是全量 fan-out：每個訂閱者都收到同一則訊息的完整副本，適合「所有節點都要知道這件事」的廣播（presence、cache invalidation、config reload）。另一種是分攤 fan-out：同一則訊息只交給一個 consumer 處理、多個 consumer 之間分攤負載，適合「這件工作只要有一個人做」的任務分派。Pub/Sub 只提供全量 fan-out——&lt;code>PUBLISH&lt;/code> 把訊息送給所有訂閱者，沒有「只給其中一個」的語意。需要分攤 fan-out 時要轉 Redis Streams 的 consumer group（&lt;code>XREADGROUP&lt;/code> 讓一則訊息只有一個 consumer 拿到），這條邊界在本章末的升級段展開。&lt;/p>
&lt;p>presence 變更廣播是最直接的應用。&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store&lt;/a> 的 cross-node query 回答「現在誰在線」，但當某個使用者上線或離線時，其他節點需要被即時通知才能推播給好友列表。presence key 寫入時同步 &lt;code>PUBLISH&lt;/code> 一則 &lt;code>user:online&lt;/code> 訊息，訂閱該 channel 的節點立刻更新本地視圖。漏一則的代價是某個好友的線上狀態延遲幾秒，下次狀態同步會補正，落在可接受範圍。&lt;/p>
&lt;p>cache invalidation 扇出是第二類應用。當一個節點更新了 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth&lt;/a> 並失效了自己的本地 cache，其他持有同一份 process-local cache 的節點需要被通知一起失效。&lt;code>PUBLISH cache:invalidate product:123&lt;/code> 讓所有節點丟棄該 key 的本地副本。這條路徑要跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside&lt;/a> 的失效策略對齊：Pub/Sub 負責「通知」，實際失效仍由各節點執行，且因為 at-most-once，必須有 TTL 作為兜底，避免廣播漏送讓某節點永久持有 stale 副本。&lt;/p>
&lt;p>即時配置熱刷新是第三類。feature flag、限流閾值、路由表這類低頻變更的配置，更新時 &lt;code>PUBLISH config:reload&lt;/code>，各節點收到後重新拉取最新配置。低頻特性讓 at-most-once 風險很低，而即時性比輪詢配置中心更省資源。&lt;/p>
&lt;h2 id="subscribe-的連線模型">SUBSCRIBE 的連線模型&lt;/h2>
&lt;p>訂閱會把連線切換進專用模式：一旦 &lt;code>SUBSCRIBE&lt;/code>，該連線只能再執行 &lt;code>SUBSCRIBE&lt;/code>、&lt;code>UNSUBSCRIBE&lt;/code>、&lt;code>PING&lt;/code> 與訂閱相關命令，不能在同一條連線上跑 &lt;code>GET&lt;/code>、&lt;code>SET&lt;/code> 等一般命令。原因是訂閱連線進入了等待推送的狀態，伺服器隨時可能把訊息推過來，與請求應答式命令的時序會衝突。&lt;/p>
&lt;p>這個模型的工程含義是：訂閱要用獨立的連線，不能跟一般讀寫共用同一個 client。共用連線池的應用要為 Pub/Sub 保留專門的訂閱連線，避免訂閱模式污染了拿來做 cache 讀寫的連線。這條限制跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發讀寫邊界&lt;/a> 的連線管理直接相關：訂閱連線是長連線、數量應該受控，與短命的請求應答連線分開計量。&lt;/p>
&lt;p>訂閱連線斷線重連時，要重新 &lt;code>SUBSCRIBE&lt;/code> 所有 channel，且要意識到斷線期間的訊息已經永久丟失。可靠性敏感的設計會在重連後主動拉一次全量狀態，用一次 reconciliation 補上廣播漏掉的窗口。&lt;/p>
&lt;h2 id="cluster-下的-fan-out-與-sharded-pubsub">cluster 下的 fan-out 與 sharded Pub/Sub&lt;/h2>
&lt;p>在單節點與傳統 cluster 中，&lt;code>PUBLISH&lt;/code> 的訊息會傳播到 cluster 內所有節點，確保任何節點上的訂閱者都能收到。這個全傳播模型保證了廣播的完整性，但代價是每則訊息都要在節點間擴散，高頻發布時會佔用 cluster 內部頻寬。&lt;/p></description><content:encoded><![CDATA[<p>Redis <a href="/blog/backend/knowledge-cards/pub-sub/" data-link-title="Pub/Sub" data-link-desc="說明 publish-subscribe 如何把事件即時分發給多個訂閱者">Pub/Sub</a> 的核心責任是把一則訊息即時推送給當下所有訂閱者，讓跨節點的狀態變更可以在同一瞬間擴散。它承擔的是「現在發生的事，立刻讓所有人知道」，正式的可靠投遞與重播責任由 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams</a> 承擔。把這條邊界放在最前面，是因為 Pub/Sub 的多數事故都來自把它當成可靠訊息系統使用。</p>
<h2 id="at-most-once訊息只送給此刻在線的訂閱者">at-most-once：訊息只送給此刻在線的訂閱者</h2>
<p>訊息<a href="/blog/backend/knowledge-cards/delivery-semantics/" data-link-title="Delivery Semantics" data-link-desc="說明事件投遞語意如何定義遺失、重複、順序與補償策略">投遞語意</a>有三種：at-most-once（最多送一次、可能漏）、at-least-once（至少送一次、可能重複）、<a href="/blog/backend/knowledge-cards/exactly-once/" data-link-title="Exactly-Once" data-link-desc="說明訊息剛好被處理一次的語意承諾、它的代價，以及多數時候該用的替代路">exactly-once</a>（剛好一次、最難實作）。Pub/Sub 採 <a href="/blog/backend/knowledge-cards/duplicate-delivery/" data-link-title="Duplicate Delivery" data-link-desc="說明同一個訊息被處理多次時如何保持結果穩定">at-most-once</a>，用「可能漏」換取低延遲與無狀態，後兩種語意由 Streams 或 message queue 承擔。具體來說：<code>PUBLISH</code> 把訊息送給發布當下已經 <code>SUBSCRIBE</code> 該 channel 的連線，沒有訂閱者就直接丟棄，訊息不寫入任何持久結構。訂閱者離線、重連、或處理速度跟不上時，那段時間的訊息不會補送。</p>
<p>這個語意決定了 Pub/Sub 適合承擔什麼。可以接受「偶爾漏一則、下一則狀態會蓋過來」的場景，Pub/Sub 的低延遲與簡單模型是優勢；要求「每一則都不能掉」的場景，例如訂單事件、扣款通知、稽核軌跡，這些責任屬於 durable queue，不該放在 Pub/Sub。</p>
<p>判讀的關鍵問題是：漏掉一則訊息的代價是什麼。presence 狀態廣播漏一則，下次 heartbeat 會修正；cache invalidation 廣播漏一則，該節點會保留 stale 副本直到 TTL 到期，代價是短暫不一致；扣款事件漏一則，代價是金額錯誤且無法自動修復。前兩者落在 Pub/Sub 的能力範圍，第三者越界。</p>
<h2 id="適用場景狀態變更的即時扇出">適用場景：狀態變更的即時扇出</h2>
<p>Pub/Sub 的典型用途是把一個節點上發生的狀態變更，即時扇出給其他節點。這類場景的共同特徵是「最終狀態會自我修正」，所以單則訊息可丟。</p>
<p>fan-out 有兩種語意要先分清，因為它們決定能不能用 Pub/Sub。一種是全量 fan-out：每個訂閱者都收到同一則訊息的完整副本，適合「所有節點都要知道這件事」的廣播（presence、cache invalidation、config reload）。另一種是分攤 fan-out：同一則訊息只交給一個 consumer 處理、多個 consumer 之間分攤負載，適合「這件工作只要有一個人做」的任務分派。Pub/Sub 只提供全量 fan-out——<code>PUBLISH</code> 把訊息送給所有訂閱者，沒有「只給其中一個」的語意。需要分攤 fan-out 時要轉 Redis Streams 的 consumer group（<code>XREADGROUP</code> 讓一則訊息只有一個 consumer 拿到），這條邊界在本章末的升級段展開。</p>
<p>presence 變更廣播是最直接的應用。<a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store</a> 的 cross-node query 回答「現在誰在線」，但當某個使用者上線或離線時，其他節點需要被即時通知才能推播給好友列表。presence key 寫入時同步 <code>PUBLISH</code> 一則 <code>user:online</code> 訊息，訂閱該 channel 的節點立刻更新本地視圖。漏一則的代價是某個好友的線上狀態延遲幾秒，下次狀態同步會補正，落在可接受範圍。</p>
<p>cache invalidation 扇出是第二類應用。當一個節點更新了 <a href="/blog/backend/knowledge-cards/source-of-truth/" data-link-title="Source of Truth" data-link-desc="說明正式資料來源如何決定資料判斷、修復與一致性責任">source of truth</a> 並失效了自己的本地 cache，其他持有同一份 process-local cache 的節點需要被通知一起失效。<code>PUBLISH cache:invalidate product:123</code> 讓所有節點丟棄該 key 的本地副本。這條路徑要跟 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a> 的失效策略對齊：Pub/Sub 負責「通知」，實際失效仍由各節點執行，且因為 at-most-once，必須有 TTL 作為兜底，避免廣播漏送讓某節點永久持有 stale 副本。</p>
<p>即時配置熱刷新是第三類。feature flag、限流閾值、路由表這類低頻變更的配置，更新時 <code>PUBLISH config:reload</code>，各節點收到後重新拉取最新配置。低頻特性讓 at-most-once 風險很低，而即時性比輪詢配置中心更省資源。</p>
<h2 id="subscribe-的連線模型">SUBSCRIBE 的連線模型</h2>
<p>訂閱會把連線切換進專用模式：一旦 <code>SUBSCRIBE</code>，該連線只能再執行 <code>SUBSCRIBE</code>、<code>UNSUBSCRIBE</code>、<code>PING</code> 與訂閱相關命令，不能在同一條連線上跑 <code>GET</code>、<code>SET</code> 等一般命令。原因是訂閱連線進入了等待推送的狀態，伺服器隨時可能把訊息推過來，與請求應答式命令的時序會衝突。</p>
<p>這個模型的工程含義是：訂閱要用獨立的連線，不能跟一般讀寫共用同一個 client。共用連線池的應用要為 Pub/Sub 保留專門的訂閱連線，避免訂閱模式污染了拿來做 cache 讀寫的連線。這條限制跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發讀寫邊界</a> 的連線管理直接相關：訂閱連線是長連線、數量應該受控，與短命的請求應答連線分開計量。</p>
<p>訂閱連線斷線重連時，要重新 <code>SUBSCRIBE</code> 所有 channel，且要意識到斷線期間的訊息已經永久丟失。可靠性敏感的設計會在重連後主動拉一次全量狀態，用一次 reconciliation 補上廣播漏掉的窗口。</p>
<h2 id="cluster-下的-fan-out-與-sharded-pubsub">cluster 下的 fan-out 與 sharded Pub/Sub</h2>
<p>在單節點與傳統 cluster 中，<code>PUBLISH</code> 的訊息會傳播到 cluster 內所有節點，確保任何節點上的訂閱者都能收到。這個全傳播模型保證了廣播的完整性，但代價是每則訊息都要在節點間擴散，高頻發布時會佔用 cluster 內部頻寬。</p>
<p>sharded Pub/Sub（<code>SPUBLISH</code> / <code>SSUBSCRIBE</code>）把這個成本收斂：sharded channel 的訊息只在負責該 channel slot 的分片內傳播，不擴散到整個 cluster。代價是訂閱者必須連到正確的分片才能收到。判讀條件是發布頻率與 cluster 規模：低頻廣播用一般 Pub/Sub 換取部署簡單；高頻發布且 cluster 節點多時，sharded Pub/Sub 避免內部頻寬被廣播流量吃掉。<code>PUBSUB SHARDNUMSUB</code> 可以查某 shard channel 的訂閱者數，用來判讀扇出是否落在預期分片。</p>
<h2 id="keyspace-notifications把-key-事件變成廣播源">keyspace notifications：把 key 事件變成廣播源</h2>
<p>keyspace notifications 讓 Redis 在 key 發生變更（寫入、刪除、過期）時自動 <code>PUBLISH</code> 一則事件，訂閱者不必輪詢就能知道某個 key 變了。開啟後，<code>SET</code>、<code>DEL</code>、TTL 過期都會發出對應 channel 的訊息。</p>
<p>這個能力把 presence cleanup 變得更即時。<a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store</a> 的 cleanup 策略依賴 TTL 過期讓離線狀態消失，但「過期了」這件事本身可以透過 <code>__keyevent@0__:expired</code> 事件廣播出去，讓其他節點即時得知某連線下線，而不必等到下次查詢才發現。</p>
<p>keyspace notifications 同樣採 at-most-once 語意，且過期事件的觸發時機與 Redis 的惰性過期機制有關：key 在被存取或背景掃描到時才真正過期並發出事件。延遲量級取決於 key 下次被存取的時機與背景掃描週期（active expiry 預設每秒約執行 10 輪、每輪抽樣部分過期 key），最差情況下事件可能延遲數秒到數分鐘。需要精確過期時序的設計，仍要保留主動查詢路徑作為依據。</p>
<h2 id="何時從-pubsub-升級">何時從 Pub/Sub 升級</h2>
<p>Pub/Sub 的邊界訊號出現時，責任應該往 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">Redis Streams</a> 或正式 <a href="/blog/backend/knowledge-cards/queue/" data-link-title="Queue" data-link-desc="說明 queue 如何保存等待處理的工作並形成容量邊界">message queue</a> 移動。判準是 durable 與 replayable 這兩個 Pub/Sub 不提供的能力。</p>
<table>
  <thead>
      <tr>
          <th>需求訊號</th>
          <th>Pub/Sub 的限制</th>
          <th>該轉向的能力</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訂閱者離線期間的訊息不能丟</td>
          <td>at-most-once、不持久化</td>
          <td>Redis Streams 的 <a href="/blog/backend/knowledge-cards/message-persistence/" data-link-title="Message Persistence" data-link-desc="說明訊息是否落盤保存，以及 broker 重啟後能否恢復">persistence</a> 與 consumer group</td>
      </tr>
      <tr>
          <td>需要重播歷史訊息</td>
          <td>訊息發布後即丟棄、無法回放</td>
          <td>Streams 的 ID 範圍讀取、message queue 的 replay</td>
      </tr>
      <tr>
          <td>需要確認訊息已被處理</td>
          <td>沒有 ack 機制</td>
          <td>Streams 的 <code>XACK</code>、queue 的 acknowledgement</td>
      </tr>
      <tr>
          <td>消費者失效時訊息要被接手</td>
          <td>訊息隨連線丟失</td>
          <td>Streams consumer group 的 pending list 與 claiming</td>
      </tr>
      <tr>
          <td>需要消費者群組分攤負載</td>
          <td>每個訂閱者都收到全部訊息</td>
          <td>Streams <code>XREADGROUP</code> 的單一 owner 語意</td>
      </tr>
  </tbody>
</table>
<p>Redis Streams 是介於 Pub/Sub 與重量級 broker 之間的選項：它持久化訊息、支援 consumer group 與 ack，又仍在 Redis 內，遷移成本低於引入 Kafka 或 RabbitMQ。Streams 與正式 message queue 的選型、consumer 設計、replay 邊界屬於 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 的責任，本章只負責標出「何時該離開 Pub/Sub」這條邊界。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>訂閱者抱怨偶爾漏訊息</td>
          <td>at-most-once 在重連窗口丟訊息</td>
          <td>重連後補一次全量 reconciliation，或轉 Streams</td>
      </tr>
      <tr>
          <td>cluster 內部頻寬被廣播流量吃掉</td>
          <td>一般 Pub/Sub 全節點傳播成本過高</td>
          <td>改 sharded Pub/Sub、收斂傳播範圍</td>
      </tr>
      <tr>
          <td>訂閱連線數量隨流量無上限成長</td>
          <td>訂閱連線與一般讀寫連線混用</td>
          <td>分離訂閱連線池、獨立計量</td>
      </tr>
      <tr>
          <td>廣播漏送導致某節點長期 stale</td>
          <td>只靠 Pub/Sub 通知失效、缺 TTL 兜底</td>
          <td>補 TTL 作為失效兜底，廣播只當加速</td>
      </tr>
      <tr>
          <td>訂閱者跟不上發布、訊息靜默丟棄</td>
          <td>Pub/Sub 無 backpressure、發布方看不到消費積壓</td>
          <td>改 Streams（pending list 可量積壓）或限發布速率</td>
      </tr>
      <tr>
          <td>開始需要「這則處理了沒」的確認</td>
          <td>Pub/Sub 無 ack、責任已越界</td>
          <td>轉 Redis Streams 或正式 message queue</td>
      </tr>
  </tbody>
</table>
<p>訂閱者抱怨漏訊息時，先確認這是不是 at-most-once 的預期行為而非 bug。Pub/Sub 在訂閱者重連窗口丟訊息是設計而非故障，正確的修法是判斷這個場景能不能接受丟；能接受就保留 Pub/Sub 並補 reconciliation，不能接受就轉向 durable 方案。</p>
<p>廣播漏送導致長期 stale 之所以難防，是因為 cache invalidation 廣播在多數時候成功，讓人把失效當成可靠，直到某次漏送讓一個節點持有錯誤價格或權限數小時而沒有任何報錯。TTL 兜底的意義就是把「廣播失敗」的最壞影響限制在一個 TTL 週期內，把 Pub/Sub 定位成「加速失效」而非「保證失效」。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 Pub/Sub 當成可靠訊息系統，是最常見也代價最大的誤區。Pub/Sub 沒有持久化、沒有 ack、沒有重播，這些是它換取低延遲與簡單模型的設計取捨。需要這些能力時，正確做法是換工具，而不是在 Pub/Sub 外圍補一層補丁去模擬可靠投遞。</p>
<p>把訂閱連線跟一般讀寫連線共用，是第二個誤區。訂閱會讓連線進入專用模式，混用會讓 cache 讀寫命令在該連線上失敗或行為異常。訂閱連線要獨立管理。</p>
<p>只靠 Pub/Sub 廣播做 cache invalidation 而沒有 TTL 兜底，是第三個誤區。廣播的 at-most-once 特性意味著總有漏送的可能，TTL 是讓漏送影響有上界的保險。</p>
<h2 id="情境回寫">情境回寫</h2>
<p>Pub/Sub 的即時扇出語意，回寫到真實服務時最常見的形狀是多節點即時狀態同步。一個多區域部署的即時通訊服務，使用者上線狀態由所在區域的節點寫入，其他區域的節點需要即時得知才能更新好友列表的線上指示。這條路徑用 Pub/Sub 廣播狀態變更，回寫時要保留「跨區傳播有延遲窗口、單則訊息可丟、靠後續 heartbeat 收斂」的判讀，而非把它當成可靠投遞。</p>
<p>這個形狀支撐的是「即時廣播 + 最終狀態收斂」的判讀。若根因是訊息不能丟（狀態變更會觸發扣款、稽核或計費），應回到 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 的 durable 方案；模組三的 fan-out 案例（如 Twitch EventSub 用 SNS + SQS 扇出給第三方）記錄了 durable 扇出的設計，可在需要持久化與重播時對照。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 2.5 的交接：presence 狀態變更的廣播回到 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">presence store 與即時狀態</a>。</li>
<li>與 2.2 的交接：cache invalidation 扇出與 TTL 兜底回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">cache aside 與失效策略</a>。</li>
<li>與 2.1 的交接：訂閱連線管理與一般讀寫連線分離回到 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">高併發下的 Redis 讀寫邊界</a>。</li>
<li>與模組三的交接：需要持久化、ack 與重播時轉向 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">message queue</a> 與 Redis Streams。</li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要看即時狀態本身如何建模與清理，回到 <a href="/blog/backend/02-cache-redis/presence-store/" data-link-title="2.5 presence store 與即時狀態" data-link-desc="整理線上狀態、跨節點查詢與過期清理">2.5 presence store 與即時狀態</a>。要看廣播訊息升級成 durable 投遞後的 consumer 設計與重播邊界，接著讀 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a>。</p>
]]></content:encoded></item><item><title>2.C10 對照：規模差異下的快取策略</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/</guid><description>&lt;p>這篇對照的核心責任是避免把單一快取做法視為通用解。&lt;/p>
&lt;h2 id="小型服務常見判讀">小型服務常見判讀&lt;/h2>
&lt;p>小型服務最常遇到的問題是切換時沒有先保護回源，快取架構本身夠用。用 cache-aside + TTL 完全可行，但如果沒有 warmup 與簡單限流，某次部署就可能讓熱門 key 全部 miss，直接打爆資料庫。&lt;/p>
&lt;h2 id="中型服務常見判讀">中型服務常見判讀&lt;/h2>
&lt;p>中型服務開始同時承受活動流量與版本切換壓力。這時失敗通常出在「切換順序」而不是策略名稱。先改 key 結構還是先改 TTL，會決定是否出現 stampede 連鎖反應。&lt;/p>
&lt;h2 id="大型服務常見判讀">大型服務常見判讀&lt;/h2>
&lt;p>大型服務下，快取已經是資料平面的一部分。跨區路由、分層儲存與一致性窗口會直接影響業務正確性。這個階段若只盯 hit rate，會漏掉最關鍵的資料一致性風險。&lt;/p>
&lt;h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件&lt;/h2>
&lt;ul>
&lt;li>&lt;code>origin QPS&lt;/code> 在 5 分鐘內超過基線 2 倍且持續上升&lt;/li>
&lt;li>熱門 key miss 同步上升，並伴隨重試流量增加&lt;/li>
&lt;li>stale read 比例連續惡化&lt;/li>
&lt;/ul>
&lt;p>任何一條成立就先暫停切換，回退上一個策略狀態，優先保護回源與資料一致性。&lt;/p></description><content:encoded><![CDATA[<p>這篇對照的核心責任是避免把單一快取做法視為通用解。</p>
<h2 id="小型服務常見判讀">小型服務常見判讀</h2>
<p>小型服務最常遇到的問題是切換時沒有先保護回源，快取架構本身夠用。用 cache-aside + TTL 完全可行，但如果沒有 warmup 與簡單限流，某次部署就可能讓熱門 key 全部 miss，直接打爆資料庫。</p>
<h2 id="中型服務常見判讀">中型服務常見判讀</h2>
<p>中型服務開始同時承受活動流量與版本切換壓力。這時失敗通常出在「切換順序」而不是策略名稱。先改 key 結構還是先改 TTL，會決定是否出現 stampede 連鎖反應。</p>
<h2 id="大型服務常見判讀">大型服務常見判讀</h2>
<p>大型服務下，快取已經是資料平面的一部分。跨區路由、分層儲存與一致性窗口會直接影響業務正確性。這個階段若只盯 hit rate，會漏掉最關鍵的資料一致性風險。</p>
<h2 id="這個情境的專屬告警條件">這個情境的專屬告警條件</h2>
<ul>
<li><code>origin QPS</code> 在 5 分鐘內超過基線 2 倍且持續上升</li>
<li>熱門 key miss 同步上升，並伴隨重試流量增加</li>
<li>stale read 比例連續惡化</li>
</ul>
<p>任何一條成立就先暫停切換，回退上一個策略狀態，優先保護回源與資料一致性。</p>
]]></content:encoded></item><item><title>DragonflyDB → Redis / Valkey：回退到標準生態的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。反向路徑見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a>。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（RESP 協定相容），但 HA 和持久化有差異需要處理。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-dragonflydb-遷回">為什麼從 DragonflyDB 遷回&lt;/h2>
&lt;p>DragonflyDB 遷回 Redis/Valkey 的 driver 跟正向遷移互為鏡像：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>Redis Modules 需求&lt;/strong>：業務開始需要 RedisJSON、RediSearch 或 RedisTimeSeries，DragonflyDB 不支援 Redis Modules 生態&lt;/li>
&lt;li>&lt;strong>Cluster mode 需求&lt;/strong>：DragonflyDB 設計為單機 scale-up，當資料量超過單機記憶體上限（數 TB）或需要跨 node sharding 時，Redis Cluster 或 Valkey Cluster 是成熟選擇&lt;/li>
&lt;li>&lt;strong>Sentinel / HA 生態&lt;/strong>：DragonflyDB 的 HA 用自家 replication，不支援 Sentinel。若團隊已有 Sentinel 或 Operator 基礎設施，回到 Redis/Valkey 整合成本更低&lt;/li>
&lt;li>&lt;strong>BSL 授權疑慮&lt;/strong>：DragonflyDB 是 BSL 1.1（4 年後轉 Apache 2.0），部分組織偏好 BSD（Valkey）或即使是 RSALv2（Redis）的已知授權&lt;/li>
&lt;/ul>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>RESP 相容、data types 一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>DragonflyDB replication → Sentinel/Cluster；snapshotting → RDB+AOF&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同（key-value cache）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>DragonflyDB 1-2 nodes → Redis primary + replica + Sentinel（或 Cluster 6 nodes）&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client config 微調（無 API 差異）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>DragonflyDB snapshot → Redis RDB 相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>全域 Low-Medium → &lt;strong>Type B drop-in&lt;/strong>，工作重心在 HA 架構切換和持久化模式對齊。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。反向路徑見 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（RESP 協定相容），但 HA 和持久化有差異需要處理。</p></blockquote>
<h2 id="為什麼從-dragonflydb-遷回">為什麼從 DragonflyDB 遷回</h2>
<p>DragonflyDB 遷回 Redis/Valkey 的 driver 跟正向遷移互為鏡像：</p>
<ul>
<li><strong>Redis Modules 需求</strong>：業務開始需要 RedisJSON、RediSearch 或 RedisTimeSeries，DragonflyDB 不支援 Redis Modules 生態</li>
<li><strong>Cluster mode 需求</strong>：DragonflyDB 設計為單機 scale-up，當資料量超過單機記憶體上限（數 TB）或需要跨 node sharding 時，Redis Cluster 或 Valkey Cluster 是成熟選擇</li>
<li><strong>Sentinel / HA 生態</strong>：DragonflyDB 的 HA 用自家 replication，不支援 Sentinel。若團隊已有 Sentinel 或 Operator 基礎設施，回到 Redis/Valkey 整合成本更低</li>
<li><strong>BSL 授權疑慮</strong>：DragonflyDB 是 BSL 1.1（4 年後轉 Apache 2.0），部分組織偏好 BSD（Valkey）或即使是 RSALv2（Redis）的已知授權</li>
</ul>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>RESP 相容、data types 一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>DragonflyDB replication → Sentinel/Cluster；snapshotting → RDB+AOF</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同（key-value cache）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>DragonflyDB 1-2 nodes → Redis primary + replica + Sentinel（或 Cluster 6 nodes）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client config 微調（無 API 差異）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>DragonflyDB snapshot → Redis RDB 相容</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>全域 Low-Medium → <strong>Type B drop-in</strong>，工作重心在 HA 架構切換和持久化模式對齊。</p>
<h2 id="相容性確認">相容性確認</h2>
<p>DragonflyDB → Redis 的相容方向跟 Redis → DragonflyDB 相反 — Redis 是 superset，回到 Redis 不會有功能缺失。但有幾個操作面差異需要處理：</p>
<table>
  <thead>
      <tr>
          <th>DragonflyDB 行為</th>
          <th>Redis 行為</th>
          <th>處理方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-threaded 吞吐量</td>
          <td>單主線程（I/O threads 輔助）</td>
          <td>回到 Redis 後 throughput 下降是預期行為；若單機不夠需要 Cluster 分片</td>
      </tr>
      <tr>
          <td>Fork-less snapshot</td>
          <td>BGSAVE fork + COW</td>
          <td>關注 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a>，大 dataset 的 fork 會造成延遲 spike</td>
      </tr>
      <tr>
          <td>自家 replication</td>
          <td>Redis replication + Sentinel 或 Cluster</td>
          <td>需要重建 HA 架構，見下方階段二</td>
      </tr>
      <tr>
          <td>無 AOF</td>
          <td>AOF + RDB 混合持久化</td>
          <td>依需求決定是否開 AOF；純 cache 場景可只用 RDB</td>
      </tr>
      <tr>
          <td>無 Cluster mode</td>
          <td>Redis Cluster 或 Valkey Cluster</td>
          <td>資料量大時需要規劃 sharding</td>
      </tr>
  </tbody>
</table>
<h2 id="階段一資料匯出">階段一：資料匯出</h2>
<p>DragonflyDB 支援 <code>SAVE</code> / <code>BGSAVE</code> 產生 RDB 格式 snapshot，跟 Redis RDB 相容。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 在 DragonflyDB 觸發 snapshot</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli -h dragonfly-host BGSAVE
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 等 BGSAVE 完成</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli -h dragonfly-host LASTSAVE
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 複製 snapshot 檔案到 Redis 資料目錄</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">cp /dragonfly-data/dump.rdb /redis-data/dump.rdb</span></span></code></pre></div><p>RDB 載入驗證：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 啟動 Redis 載入 RDB</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-server --dbfilename dump.rdb --dir /redis-data
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 驗證 key count</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli DBSIZE</span></span></code></pre></div><p>若 DragonflyDB 跑的是較新版本產出的 RDB，先在測試環境驗證 Redis 能正常載入。DragonflyDB 的 RDB 基於 Redis 6.x 格式，Redis 7.x 和 Valkey 8.x 向下相容無問題。</p>
<h2 id="階段二ha-架構重建">階段二：HA 架構重建</h2>
<p>DragonflyDB 回到 Redis/Valkey 後，HA 需要從 DragonflyDB replication 切換到 Sentinel 或 Cluster。</p>
<h3 id="sentinel-路徑適合非分片場景">Sentinel 路徑（適合非分片場景）</h3>
<p>1 primary + N replica + 3 Sentinel nodes。配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>。</p>
<h3 id="cluster-路徑適合需要分片的場景">Cluster 路徑（適合需要分片的場景）</h3>
<p>最小 3 primary + 3 replica。配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Resharding</a>。</p>
<p>選擇依據：資料量 &lt; 單機記憶體的 70% 用 Sentinel，需要水平擴展用 Cluster。</p>
<h2 id="階段三client-切換">階段三：Client 切換</h2>
<p>Application 的 Redis client 不需要改 API — DragonflyDB 跟 Redis 用同一套 RESP 協定。需要改的只有：</p>
<ol>
<li><strong>Endpoint</strong>：從 DragonflyDB host:port 改為 Redis primary（或 Sentinel/Cluster endpoint）</li>
<li><strong>認證</strong>：若 DragonflyDB 用 <code>requirepass</code>，Redis 同參數；若要升級到 ACL 趁此機會配置</li>
<li><strong>Sentinel/Cluster 配置</strong>：client library 需要啟用 Sentinel discovery 或 Cluster mode</li>
</ol>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 切換前：直連 DragonflyDB</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">host</span><span class="o">=</span><span class="s2">&#34;dragonfly-host&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 切換後：Sentinel 模式</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">sentinel</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Sentinel</span><span class="p">([(</span><span class="s2">&#34;sentinel-1&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;sentinel-2&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;sentinel-3&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">)])</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">master_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">password</span><span class="o">=</span><span class="s2">&#34;secret&#34;</span><span class="p">)</span></span></span></code></pre></div><h2 id="階段四效能-baseline-與回退">階段四：效能 baseline 與回退</h2>
<h3 id="效能預期">效能預期</h3>
<p>回到 Redis 後，單機 throughput 會低於 DragonflyDB（Redis 單主線程 vs DragonflyDB 多線程）。建立 baseline 時要跟 Redis 的歷史數據比，不是跟 DragonflyDB 比。</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>預期變化</th>
          <th>應對</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐量</td>
          <td>下降（單線程限制）</td>
          <td>Cluster 分片或 read replica 分散</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>BGSAVE 期間可能有 spike</td>
          <td>調整 BGSAVE 排程避開高峰</td>
      </tr>
      <tr>
          <td>記憶體使用</td>
          <td>上升 ~30%（Redis 記憶體效率較低）</td>
          <td>預先調整 maxmemory 和 eviction policy</td>
      </tr>
  </tbody>
</table>
<h3 id="回退路徑">回退路徑</h3>
<p>回退到 DragonflyDB：把 Redis 的 RDB dump 回 DragonflyDB 載入，endpoint 改回。Cache 資料可重建，即使 RDB 不搬，DragonflyDB 重啟後 cache miss 回源到 DB 即可。</p>
<p>DragonflyDB 在遷移完成後保留 7 天再下線。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>反向路徑：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>HA 重建：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster Resharding</a></li>
<li>持久化注意：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Persistence Fork Latency</a></li>
</ul>
]]></content:encoded></item><item><title>KeyDB → Redis / Valkey：從多線程 fork 回歸主線的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（KeyDB 是 Redis fork、RESP 相容、RDB/AOF 相容），但 active-active replication 跟 multi-threading 特性回退需要額外處理。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-keydb-遷回">為什麼從 KeyDB 遷回&lt;/h2>
&lt;p>KeyDB 是 Snap 維護的 Redis fork，主要差異化在多線程和 active-active replication。遷回的 driver：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>維護活躍度疑慮&lt;/strong>：KeyDB 的 release cadence 跟 Redis/Valkey 主線比較慢，部分組織擔心長期維護與安全 patch 的及時性&lt;/li>
&lt;li>&lt;strong>Valkey 生態收斂&lt;/strong>：Valkey 在 Linux Foundation 治理下快速演進（8.x 多線程改進），KeyDB 的多線程優勢逐漸縮小&lt;/li>
&lt;li>&lt;strong>Active-active 不再需要&lt;/strong>：業務不再需要跨 region active-active、或改用 application 層處理衝突解析&lt;/li>
&lt;li>&lt;strong>社群與工具生態&lt;/strong>：Redis/Valkey 的 client library、monitoring exporter、Operator 支援度更廣&lt;/li>
&lt;/ul>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>完全相容（fork 自 Redis 6.x）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>active-active → Sentinel/Cluster；multi-thread config 移除&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>相近（1 primary + N replica + HA）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client config 微調&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB/AOF 完全相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Type B drop-in，工作重心在 active-active replication 拆除和效能 baseline 對齊。&lt;/p>
&lt;h2 id="keydb-特有功能的處理">KeyDB 特有功能的處理&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>KeyDB 特有功能&lt;/th>
 &lt;th>Redis/Valkey 對應&lt;/th>
 &lt;th>遷移處理&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Multi-threading（&lt;code>server-threads&lt;/code>）&lt;/td>
 &lt;td>Redis I/O threads / Valkey 8 async I/O&lt;/td>
 &lt;td>回到 Redis 後吞吐量下降是預期，需要 benchmark 建立新 baseline&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Active-active replication&lt;/td>
 &lt;td>無原生等價。Redis 需要 application 層解衝突或用 CRDTs（社群方案）&lt;/td>
 &lt;td>遷移前確認業務是否仍需 multi-master。不需要則直接切 Sentinel/Cluster&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>FLASH storage（&lt;code>storage-provider flash&lt;/code>）&lt;/td>
 &lt;td>無原生等價。Redis 純記憶體&lt;/td>
 &lt;td>遷移前把 FLASH 資料回收到記憶體，或接受遷移後記憶體需求上升。調整 &lt;code>maxmemory&lt;/code>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Subkey expires&lt;/td>
 &lt;td>Redis 無 subkey expire（只有 top-level key TTL）&lt;/td>
 &lt;td>檢查 application 是否依賴 subkey expire；若有需要改寫為 top-level key 或用 sorted set 模擬&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>EXPIREMEMBER&lt;/code> 命令&lt;/td>
 &lt;td>Redis 無此命令&lt;/td>
 &lt;td>grep application code 確認未使用；若有需改寫&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>FLASH storage 的處理取決於冷資料比例。如果多數資料在 FLASH 上（用 &lt;code>OBJECT FREQ&lt;/code> 確認），遷移後的 Redis 記憶體需求會大幅上升 — 要提前計算純記憶體所需容量，調整 instance 規格或改用更積極的 eviction policy。Subkey expires 和 &lt;code>EXPIREMEMBER&lt;/code> 的影響範圍通常較小，但一旦 application 依賴就需要重構資料結構（用 top-level key + TTL 或 sorted set 模擬過期）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（KeyDB 是 Redis fork、RESP 相容、RDB/AOF 相容），但 active-active replication 跟 multi-threading 特性回退需要額外處理。</p></blockquote>
<h2 id="為什麼從-keydb-遷回">為什麼從 KeyDB 遷回</h2>
<p>KeyDB 是 Snap 維護的 Redis fork，主要差異化在多線程和 active-active replication。遷回的 driver：</p>
<ul>
<li><strong>維護活躍度疑慮</strong>：KeyDB 的 release cadence 跟 Redis/Valkey 主線比較慢，部分組織擔心長期維護與安全 patch 的及時性</li>
<li><strong>Valkey 生態收斂</strong>：Valkey 在 Linux Foundation 治理下快速演進（8.x 多線程改進），KeyDB 的多線程優勢逐漸縮小</li>
<li><strong>Active-active 不再需要</strong>：業務不再需要跨 region active-active、或改用 application 層處理衝突解析</li>
<li><strong>社群與工具生態</strong>：Redis/Valkey 的 client library、monitoring exporter、Operator 支援度更廣</li>
</ul>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>完全相容（fork 自 Redis 6.x）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>active-active → Sentinel/Cluster；multi-thread config 移除</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>相近（1 primary + N replica + HA）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client config 微調</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB/AOF 完全相容</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Type B drop-in，工作重心在 active-active replication 拆除和效能 baseline 對齊。</p>
<h2 id="keydb-特有功能的處理">KeyDB 特有功能的處理</h2>
<table>
  <thead>
      <tr>
          <th>KeyDB 特有功能</th>
          <th>Redis/Valkey 對應</th>
          <th>遷移處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Multi-threading（<code>server-threads</code>）</td>
          <td>Redis I/O threads / Valkey 8 async I/O</td>
          <td>回到 Redis 後吞吐量下降是預期，需要 benchmark 建立新 baseline</td>
      </tr>
      <tr>
          <td>Active-active replication</td>
          <td>無原生等價。Redis 需要 application 層解衝突或用 CRDTs（社群方案）</td>
          <td>遷移前確認業務是否仍需 multi-master。不需要則直接切 Sentinel/Cluster</td>
      </tr>
      <tr>
          <td>FLASH storage（<code>storage-provider flash</code>）</td>
          <td>無原生等價。Redis 純記憶體</td>
          <td>遷移前把 FLASH 資料回收到記憶體，或接受遷移後記憶體需求上升。調整 <code>maxmemory</code></td>
      </tr>
      <tr>
          <td>Subkey expires</td>
          <td>Redis 無 subkey expire（只有 top-level key TTL）</td>
          <td>檢查 application 是否依賴 subkey expire；若有需要改寫為 top-level key 或用 sorted set 模擬</td>
      </tr>
      <tr>
          <td><code>EXPIREMEMBER</code> 命令</td>
          <td>Redis 無此命令</td>
          <td>grep application code 確認未使用；若有需改寫</td>
      </tr>
  </tbody>
</table>
<p>FLASH storage 的處理取決於冷資料比例。如果多數資料在 FLASH 上（用 <code>OBJECT FREQ</code> 確認），遷移後的 Redis 記憶體需求會大幅上升 — 要提前計算純記憶體所需容量，調整 instance 規格或改用更積極的 eviction policy。Subkey expires 和 <code>EXPIREMEMBER</code> 的影響範圍通常較小，但一旦 application 依賴就需要重構資料結構（用 top-level key + TTL 或 sorted set 模擬過期）。</p>
<h3 id="active-active-拆除">Active-active 拆除</h3>
<p>若 KeyDB 的 active-active replication 正在使用，遷移前需要先收斂為單主寫入：</p>
<ol>
<li>選定一個 region 的 KeyDB 為 primary，其他 region 停止寫入</li>
<li>等資料同步完成（replica 追上 primary offset）</li>
<li>從 primary 做 RDB export</li>
<li>用 RDB 建立 Redis/Valkey instance</li>
<li>各 region 的 application 切到新的 Redis/Valkey（Sentinel 或 Cluster）</li>
</ol>
<h2 id="資料搬遷">資料搬遷</h2>
<p>KeyDB 的 RDB 和 AOF 與 Redis 格式相容，搬遷流程跟 DragonflyDB 回退類似：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># KeyDB 端觸發 BGSAVE</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli -h keydb-host BGSAVE
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 複製 RDB 到 Redis/Valkey 資料目錄</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">scp keydb-host:/data/dump.rdb redis-host:/data/dump.rdb
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Redis/Valkey 載入</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">redis-server --dbfilename dump.rdb --dir /data</span></span></code></pre></div><p>如果使用了 FLASH storage，RDB 只包含記憶體中的資料。FLASH 上的冷資料需要先用 <code>OBJECT FREQ</code> 確認存取頻率，決定是要 warm up 到記憶體再 export，還是接受遷移後冷資料 cache miss 回源。</p>
<h2 id="效能差異預期">效能差異預期</h2>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>KeyDB → Redis 變化</th>
          <th>應對</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>吞吐量</td>
          <td>下降（KeyDB multi-thread → Redis single-thread）</td>
          <td>評估是否需要 Cluster 分片補償。Valkey 8 的 async I/O 可部分彌補</td>
      </tr>
      <tr>
          <td>記憶體</td>
          <td>上升（若使用了 FLASH storage 被移除）</td>
          <td>提前計算純記憶體所需容量，調整 instance 規格</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>BGSAVE fork spike 可能出現</td>
          <td>KeyDB 的多線程降低了 fork 影響，回到 Redis 需要關注 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a></td>
      </tr>
      <tr>
          <td>Active-active latency</td>
          <td>不適用（已拆除）</td>
          <td>N/A</td>
      </tr>
  </tbody>
</table>
<h2 id="回退路徑">回退路徑</h2>
<p>Cache 資料可重建，回退方式：</p>
<ol>
<li>Application endpoint 改回 KeyDB</li>
<li>若 KeyDB 已下線，重啟 KeyDB 載入 Redis 的 RDB（格式相容）</li>
<li>Cache miss 回源到 DB 自然 warm up</li>
</ol>
<p>KeyDB 保留 7 天再下線。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a>、<a href="/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/" data-link-title="KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入" data-link-desc="KeyDB 的 active-active 讓兩個 master 都能寫、互相同步，聽起來解決了跨區寫入的所有問題——直到兩邊同時寫同一個 key，last-write-wins 默默丟掉其中一筆。本文展開 active-active 的複製機制與衝突語意、實機驗證雙向同步、5 個把多主複製寫成資料遺失與迴圈的 production 踩坑，以及哪些資料能放 active-active、哪些不能的邊界">KeyDB Active-Active Replication</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>HA 重建：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a></li>
<li>效能參考：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Persistence Fork Latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Connection Pipeline Latency</a></li>
</ul>
]]></content:encoded></item><item><title>2.11 Redis data types 實作</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/redis-data-types/</guid><description>&lt;p>Redis data types 的核心責任是把服務語意映射到適合的內建結構，讓讀寫操作的複雜度、原子性與記憶體成本由結構本身保證。選對型別，排行榜更新是一次 O(log N) 操作；選錯型別，同一個需求要拉回整包資料在應用端重算再寫回。本章承接 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape&lt;/a> 的形狀選型，往下談每個型別的實作判讀與容量行為。&lt;/p>
&lt;h2 id="與-28-的分工">與 2.8 的分工&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8&lt;/a> 回答「這份資料是單 key、集合、排序還是計數」這層形狀選型，本章回答「選定形狀後，這個型別的操作語意、原子性與記憶體曲線是什麼」。形狀選型決定方向，型別實作決定它在真實流量下的成本與正確性邊界。兩章分工互補：2.8 判斷形狀，本章確認該型別能不能撐住預期的存取節奏。本章涵蓋 sorted set、bitmap、HyperLogLog、counter 與 hash 這五個快取場景最常用的型別；list 與 stream 的責任偏向佇列與事件流，由 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue&lt;/a> 涵蓋，geo 這類空間型別不在本章範圍。&lt;/p>
&lt;h2 id="sorted-set排行榜與時間線">sorted set：排行榜與時間線&lt;/h2>
&lt;p>sorted set 的責任是維護一組帶 score 的成員，並讓「依 score 排序取範圍」成為一次操作。它適合排行榜、時間線、優先佇列這類「要排序、要取 top-N、要查排名」的場景。&lt;/p>
&lt;p>排行榜是最直接的應用。&lt;code>ZADD leaderboard 5000 player:42&lt;/code> 寫入或更新分數，&lt;code>ZREVRANGE leaderboard 0 9 WITHSCORES&lt;/code> 取前十名，&lt;code>ZREVRANK leaderboard player:42&lt;/code> 查某玩家的排名。每個操作都是 O(log N)，不需要把整個排行榜拉到應用端排序。分數變動用 &lt;code>ZINCRBY&lt;/code> 原子遞增，避免「讀分數、加分、寫回」的競態。&lt;/p>
&lt;p>時間線是第二類應用。把訊息或事件的時間戳當 score，&lt;code>ZADD timeline &amp;lt;timestamp&amp;gt; &amp;lt;event-id&amp;gt;&lt;/code>，就能用 &lt;code>ZRANGEBYSCORE&lt;/code> 取某個時間窗口的事件，或用 &lt;code>ZREVRANGE&lt;/code> 取最新 N 則。這個用法要注意容量：時間線會持續增長，需要搭配 &lt;code>ZREMRANGEBYRANK&lt;/code> 或 &lt;code>ZREMRANGEBYSCORE&lt;/code> 定期裁剪舊資料，否則 key 會無限膨脹。&lt;/p>
&lt;p>sorted set 的判讀重點是 score 語意的正確性。score 是排序的唯一依據，score 設計錯誤會造成排序漂移：用浮點數當 score 時要注意精度，相同 score 的成員按字典序排列，需要穩定排序時要把 tie-break 維度編進 score 或成員名。容量上，sorted set 內部同時維護一個支援 O(1) 查找的 hash 與一個支援 O(log N) 排序的跳躍表（skiplist），兩份索引讓查找與排序都快，但每個成員要在兩個結構各存一份，記憶體成本高於單純的 set，成員數很大的排行榜要評估記憶體佔用。&lt;/p>
&lt;h2 id="bitmap布林狀態的省記憶體表示">bitmap：布林狀態的省記憶體表示&lt;/h2>
&lt;p>bitmap 的責任是用單一 bit 表示每個實體的布林狀態，讓「大量實體的是否」用極小記憶體承載。它建構在 string 上、以 bit 操作存取，適合日活躍標記、功能開關位、簽到記錄這類「每個 id 對應一個是否」的場景。&lt;/p>
&lt;p>日活躍使用者追蹤是典型應用。用日期當 key、使用者 id 當 offset，&lt;code>SETBIT active:20260616 &amp;lt;user-id&amp;gt; 1&lt;/code> 標記某使用者當天活躍，&lt;code>BITCOUNT active:20260616&lt;/code> 算當天活躍總數。一千萬個使用者只需要約 1.2 MB（一千萬 bit），相比為每個使用者存一筆記錄，記憶體成本低一到兩個數量級。多天的留存分析用 &lt;code>BITOP AND&lt;/code> 把多天的 bitmap 做交集，算出連續活躍的使用者。&lt;/p>
&lt;p>bitmap 的判讀重點是 offset 的密度。bitmap 的記憶體取決於最大 offset 而非實際設置的 bit 數：如果 user id 是稀疏的大整數（例如雪花 id），直接當 offset 會撐爆記憶體，需要先把 id 映射成稠密的連續整數。offset 稠密時 bitmap 極省空間，稀疏時反而浪費，這條判讀決定 bitmap 能不能用。&lt;/p>
&lt;h2 id="hyperloglog基數估計">HyperLogLog：基數估計&lt;/h2>
&lt;p>HyperLogLog 的責任是用固定的小記憶體估算一個集合的不重複元素數量，代價是放棄精確值換取近乎常數的空間。它適合 UV 統計、不重複事件計數這類「只要不重複的數量、不需要知道具體是誰」的場景。&lt;/p>
&lt;p>獨立訪客（UV）統計是典型應用。&lt;code>PFADD uv:20260616 &amp;lt;user-id&amp;gt;&lt;/code> 把訪客加入估計，&lt;code>PFCOUNT uv:20260616&lt;/code> 取得不重複訪客數的估計值。HyperLogLog 每個 key 的記憶體在 dense 表示下固定在約 12 KB，無論加入一千還是一億個元素都不增長，標準誤差約 0.81%；元素數少時 Redis 用 sparse 編碼、記憶體遠低於 12 KB，超過可配置的閾值（&lt;code>hll-sparse-max-bytes&lt;/code>，預設 3000 bytes）後才切換成 dense 表示。多天 UV 合併用 &lt;code>PFMERGE&lt;/code> 把多個 HLL 合成一個再 count，算出跨天的不重複訪客。&lt;/p></description><content:encoded><![CDATA[<p>Redis data types 的核心責任是把服務語意映射到適合的內建結構，讓讀寫操作的複雜度、原子性與記憶體成本由結構本身保證。選對型別，排行榜更新是一次 O(log N) 操作；選錯型別，同一個需求要拉回整包資料在應用端重算再寫回。本章承接 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape</a> 的形狀選型，往下談每個型別的實作判讀與容量行為。</p>
<h2 id="與-28-的分工">與 2.8 的分工</h2>
<p><a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8</a> 回答「這份資料是單 key、集合、排序還是計數」這層形狀選型，本章回答「選定形狀後，這個型別的操作語意、原子性與記憶體曲線是什麼」。形狀選型決定方向，型別實作決定它在真實流量下的成本與正確性邊界。兩章分工互補：2.8 判斷形狀，本章確認該型別能不能撐住預期的存取節奏。本章涵蓋 sorted set、bitmap、HyperLogLog、counter 與 hash 這五個快取場景最常用的型別；list 與 stream 的責任偏向佇列與事件流，由 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a> 涵蓋，geo 這類空間型別不在本章範圍。</p>
<h2 id="sorted-set排行榜與時間線">sorted set：排行榜與時間線</h2>
<p>sorted set 的責任是維護一組帶 score 的成員，並讓「依 score 排序取範圍」成為一次操作。它適合排行榜、時間線、優先佇列這類「要排序、要取 top-N、要查排名」的場景。</p>
<p>排行榜是最直接的應用。<code>ZADD leaderboard 5000 player:42</code> 寫入或更新分數，<code>ZREVRANGE leaderboard 0 9 WITHSCORES</code> 取前十名，<code>ZREVRANK leaderboard player:42</code> 查某玩家的排名。每個操作都是 O(log N)，不需要把整個排行榜拉到應用端排序。分數變動用 <code>ZINCRBY</code> 原子遞增，避免「讀分數、加分、寫回」的競態。</p>
<p>時間線是第二類應用。把訊息或事件的時間戳當 score，<code>ZADD timeline &lt;timestamp&gt; &lt;event-id&gt;</code>，就能用 <code>ZRANGEBYSCORE</code> 取某個時間窗口的事件，或用 <code>ZREVRANGE</code> 取最新 N 則。這個用法要注意容量：時間線會持續增長，需要搭配 <code>ZREMRANGEBYRANK</code> 或 <code>ZREMRANGEBYSCORE</code> 定期裁剪舊資料，否則 key 會無限膨脹。</p>
<p>sorted set 的判讀重點是 score 語意的正確性。score 是排序的唯一依據，score 設計錯誤會造成排序漂移：用浮點數當 score 時要注意精度，相同 score 的成員按字典序排列，需要穩定排序時要把 tie-break 維度編進 score 或成員名。容量上，sorted set 內部同時維護一個支援 O(1) 查找的 hash 與一個支援 O(log N) 排序的跳躍表（skiplist），兩份索引讓查找與排序都快，但每個成員要在兩個結構各存一份，記憶體成本高於單純的 set，成員數很大的排行榜要評估記憶體佔用。</p>
<h2 id="bitmap布林狀態的省記憶體表示">bitmap：布林狀態的省記憶體表示</h2>
<p>bitmap 的責任是用單一 bit 表示每個實體的布林狀態，讓「大量實體的是否」用極小記憶體承載。它建構在 string 上、以 bit 操作存取，適合日活躍標記、功能開關位、簽到記錄這類「每個 id 對應一個是否」的場景。</p>
<p>日活躍使用者追蹤是典型應用。用日期當 key、使用者 id 當 offset，<code>SETBIT active:20260616 &lt;user-id&gt; 1</code> 標記某使用者當天活躍，<code>BITCOUNT active:20260616</code> 算當天活躍總數。一千萬個使用者只需要約 1.2 MB（一千萬 bit），相比為每個使用者存一筆記錄，記憶體成本低一到兩個數量級。多天的留存分析用 <code>BITOP AND</code> 把多天的 bitmap 做交集，算出連續活躍的使用者。</p>
<p>bitmap 的判讀重點是 offset 的密度。bitmap 的記憶體取決於最大 offset 而非實際設置的 bit 數：如果 user id 是稀疏的大整數（例如雪花 id），直接當 offset 會撐爆記憶體，需要先把 id 映射成稠密的連續整數。offset 稠密時 bitmap 極省空間，稀疏時反而浪費，這條判讀決定 bitmap 能不能用。</p>
<h2 id="hyperloglog基數估計">HyperLogLog：基數估計</h2>
<p>HyperLogLog 的責任是用固定的小記憶體估算一個集合的不重複元素數量，代價是放棄精確值換取近乎常數的空間。它適合 UV 統計、不重複事件計數這類「只要不重複的數量、不需要知道具體是誰」的場景。</p>
<p>獨立訪客（UV）統計是典型應用。<code>PFADD uv:20260616 &lt;user-id&gt;</code> 把訪客加入估計，<code>PFCOUNT uv:20260616</code> 取得不重複訪客數的估計值。HyperLogLog 每個 key 的記憶體在 dense 表示下固定在約 12 KB，無論加入一千還是一億個元素都不增長，標準誤差約 0.81%；元素數少時 Redis 用 sparse 編碼、記憶體遠低於 12 KB，超過可配置的閾值（<code>hll-sparse-max-bytes</code>，預設 3000 bytes）後才切換成 dense 表示。多天 UV 合併用 <code>PFMERGE</code> 把多個 HLL 合成一個再 count，算出跨天的不重複訪客。</p>
<p>HyperLogLog 的判讀重點是「估計值能不能接受」。它回答的是「大約多少不重複」，不能回答「某個特定元素在不在集合裡」，也不能取出集合成員。需要精確去重、或需要判斷成員存在性時，用 set 或 bitmap；只要量級且能容忍百分之一以內的誤差時，HyperLogLog 用固定小記憶體換取巨大的空間節省。把 HLL 的估計值當精確值報給財務或計費，是越界用法。</p>
<h2 id="原子計數器counter">原子計數器：counter</h2>
<p>counter 的責任是提供一個原子遞增的整數，讓並發場景下的計數不需要鎖。它建構在 string 上，<code>INCR</code>、<code>INCRBY</code>、<code>DECR</code> 都是原子操作，適合限流、配額、瀏覽計數這類高並發累加。</p>
<p>限流計數是典型應用，也跟 <a href="/blog/backend/knowledge-cards/rate-limit/" data-link-title="Rate Limit" data-link-desc="說明限流如何保護服務入口、下游依賴與租戶公平性">rate limit</a> 卡片直接相關。固定窗口限流用 <code>INCR rate:&lt;user&gt;:&lt;minute&gt;</code> 累加當前窗口的請求數，第一次寫入時 <code>EXPIRE</code> 設定窗口長度，超過閾值就拒絕。原子性讓多個並發請求的計數不會互相覆蓋，這是用一般 <code>GET</code>/<code>SET</code> 做計數會踩到的競態。</p>
<p>counter 的判讀重點是原子性與過期窗口的對齊。<code>INCR</code> 本身原子，但「INCR 後再 EXPIRE」是兩個操作，若第一次 INCR 成功、EXPIRE 失敗，這個 key 會永不過期變成髒計數。最穩健的做法是用 Lua script 把 INCR 與 EXPIRE 包成一個原子單元；<code>SET key 1 EX &lt;ttl&gt; NX</code> 配合後續 INCR 能減少 EXPIRE 漏掉的機率（窗口第一次寫入時就帶上過期），但這個組合的兩步之間仍非原子，不視為與 Lua script 等效。這條對齊跟 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 counter 形狀</a> 提到的「原子性與過期窗口要對齊」是同一件事，本章補上具體實作。</p>
<h2 id="hash結構化欄位的局部更新">hash：結構化欄位的局部更新</h2>
<p>hash 的責任是把一個實體的多個欄位存在同一個 key 下，並讓單一欄位可以獨立讀寫。它適合使用者摘要、商品局部欄位這類「整體是一個實體、但欄位會分別更新」的場景。</p>
<p>相比把整個實體序列化成一個 JSON blob，hash 的優勢是局部更新：<code>HSET user:42 last_seen &lt;ts&gt;</code> 只改一個欄位，不需要讀出整包、改一個值、再寫回。這在欄位更新頻繁的場景省下大量序列化成本與競態風險。<code>HGET</code> 取單一欄位、<code>HGETALL</code> 取全部、<code>HINCRBY</code> 對數值欄位原子遞增。</p>
<p>hash 的判讀重點是欄位責任要清楚。hash 讓欄位能獨立更新，但這也讓它容易滑向「半正式狀態」：當不同欄位由不同來源在不同時間更新，整個 hash 的一致性就變得模糊，某些欄位新、某些欄位舊。判讀條件是這些欄位是否真的能獨立成立；如果它們必須一起更新才有意義，blob 的整體替換反而比 hash 的局部更新更安全。</p>
<p>容量上 hash 有一個要注意的轉折：欄位數與欄位值在閾值內時（<code>hash-max-listpack-entries</code> 預設 128 個欄位、<code>hash-max-listpack-value</code> 預設 64 bytes）用緊湊的 listpack 編碼、記憶體很省，超過任一閾值就轉成 hashtable 編碼，記憶體成本明顯上升。設計大 hash 時要確認欄位數落在閾值內，否則會在某個規模點遇到非線性的記憶體增長。</p>
<h2 id="型別選型的容量與原子性判讀">型別選型的容量與原子性判讀</h2>
<p>選型前要把存取語意、原子性需求與記憶體曲線一起考慮，而不是只看「能不能存」。</p>
<table>
  <thead>
      <tr>
          <th>型別</th>
          <th>承擔語意</th>
          <th>原子操作</th>
          <th>記憶體行為</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>sorted set</td>
          <td>排序、排名、時間線</td>
          <td><code>ZINCRBY</code>、範圍操作</td>
          <td>隨成員數線性增長，單成員成本偏高</td>
      </tr>
      <tr>
          <td>bitmap</td>
          <td>大量實體的布林狀態</td>
          <td><code>SETBIT</code>、<code>BITOP</code></td>
          <td>取決於最大 offset，稠密時極省</td>
      </tr>
      <tr>
          <td>HyperLogLog</td>
          <td>不重複數量估計</td>
          <td><code>PFADD</code>、<code>PFMERGE</code></td>
          <td>固定約 12 KB，與元素數無關</td>
      </tr>
      <tr>
          <td>counter</td>
          <td>並發累加計數</td>
          <td><code>INCR</code>、<code>INCRBY</code></td>
          <td>單一整數，極小</td>
      </tr>
      <tr>
          <td>hash</td>
          <td>實體的可獨立更新欄位</td>
          <td><code>HINCRBY</code>、<code>HSET</code> 單欄位</td>
          <td>隨欄位數增長，小 hash 有編碼優化</td>
      </tr>
  </tbody>
</table>
<p>sorted set 與 bitmap 都能做「統計」，但語意不同：sorted set 保留每個成員與其分數、可取明細，bitmap 只保留是否、取不出成員但極省空間。需要明細與排名用 sorted set，只需要聚合數量用 bitmap 或 HLL。</p>
<p>HyperLogLog 與 set 的分界是「要不要精確、要不要成員」。set 精確且可列舉，記憶體隨成員數增長；HLL 估計且不可列舉，記憶體固定。同一個 UV 需求，用 set 在大流量下記憶體會失控，用 HLL 換取固定成本但放棄精確值，選擇取決於誤差容忍度。</p>
<h2 id="常見誤區">常見誤區</h2>
<p>把 sorted set 當成「能排序的 set」而忽略 score 設計，會造成排序漂移。score 是排序的唯一依據，相同 score 按字典序，需要穩定且可預測的排序時要把 tie-break 維度設計進 score。</p>
<p>把 bitmap 用在稀疏 id 上，會讓記憶體被最大 offset 撐爆。bitmap 省記憶體的前提是 offset 稠密，稀疏 id 要先映射成連續整數，或改用其他結構。</p>
<p>把 HyperLogLog 的估計值當精確計數，會在計費、財務這類要求精確的場景出錯。HLL 是有誤差的估計，它的價值在用固定小記憶體換量級判斷，不是替代精確計數。</p>
<p>把多步操作當成原子，會在並發下產生競態。<code>INCR</code> 加 <code>EXPIRE</code>、<code>ZADD</code> 加裁剪都是多個命令，需要原子保證時用 Lua script 或 <code>MULTI</code>/<code>EXEC</code> 包起來。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>排行榜在應用端拉全量排序</td>
          <td>沒用 sorted set 的範圍操作</td>
          <td>改 <code>ZREVRANGE</code> / <code>ZREVRANK</code> 在 Redis 排序</td>
      </tr>
      <tr>
          <td>bitmap key 記憶體異常膨脹</td>
          <td>offset 稀疏、被最大 id 撐大</td>
          <td>把 id 映射成稠密整數，或換結構</td>
      </tr>
      <tr>
          <td>UV 統計記憶體隨流量無上限增長</td>
          <td>用 set 做大基數去重</td>
          <td>容忍誤差時改 HyperLogLog 固定成本</td>
      </tr>
      <tr>
          <td>限流計數出現永不過期的髒 key</td>
          <td>INCR 與 EXPIRE 未原子化</td>
          <td>Lua script 包成原子單元</td>
      </tr>
      <tr>
          <td>hash 欄位新舊不一致、難判讀</td>
          <td>欄位責任不清、滑向半正式狀態</td>
          <td>重新判斷欄位能否獨立，必要時改 blob 整體替換</td>
      </tr>
  </tbody>
</table>
<p>排行榜在應用端拉全量排序是最常見的浪費：明明 sorted set 能 O(log N) 取 top-N，卻把整個集合讀回應用端用程式排序，在成員數大時造成不必要的網路與 CPU 成本。判讀方法是看排序邏輯在哪裡發生，把它推回 Redis 的範圍操作。</p>
<p>limit 計數的髒 key 不產生任何錯誤訊息，因此特別容易被忽略：INCR 成功但 EXPIRE 漏掉，這個 key 不會報錯，只是悄悄永不過期，問題要等到記憶體監控異常或限流誤判時才間接浮現。把 INCR 與 EXPIRE 原子化是最可靠的修法。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>要回到資料形狀的選型判斷，回到 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.8 cache data shape 與 access pattern</a>。要看這些型別在高並發下的讀寫邊界與連線管理，接著讀 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.1 高併發下的 Redis 讀寫邊界</a>。要看 stream 型別承擔的事件流責任，接著讀 <a href="/blog/backend/02-cache-redis/pub-sub/" data-link-title="2.10 Pub/Sub 與即時 fan-out" data-link-desc="說明 Redis Pub/Sub 的即時廣播責任、at-most-once 邊界，以及何時升級到 Streams 或正式 message queue">2.10 Pub/Sub 與即時 fan-out</a> 與 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">模組三 message queue</a>。</p>
]]></content:encoded></item><item><title>AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 &lt;a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件&lt;/a>、&lt;a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價&lt;/a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。&lt;/p>&lt;/blockquote>
&lt;h2 id="managed-不等於-hands-off">managed 不等於 hands-off&lt;/h2>
&lt;p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。&lt;/p>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 &lt;a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog&lt;/a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。&lt;/p>
&lt;p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。&lt;/p>
&lt;h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側&lt;/h2>
&lt;p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>面向&lt;/th>
 &lt;th>AWS 的責任（managed）&lt;/th>
 &lt;th>你的責任（仍要自己做）&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>硬體 / OS / patching&lt;/td>
 &lt;td>全包&lt;/td>
 &lt;td>—&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>failover&lt;/td>
 &lt;td>自動偵測 + replica 晉升&lt;/td>
 &lt;td>client 要有 reconnect 邏輯&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>跨 AZ 複製&lt;/td>
 &lt;td>Multi-AZ 自動複製&lt;/td>
 &lt;td>接受非同步複製的 stale window&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>snapshot / backup&lt;/td>
 &lt;td>自動 + 手動 snapshot&lt;/td>
 &lt;td>決定保留策略、驗證能還原&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>eviction&lt;/td>
 &lt;td>提供 maxmemory-policy 參數&lt;/td>
 &lt;td>選對 policy、設對 TTL&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>cache stampede&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>client-side jitter / singleflight 自己做&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>key 設計 / hot key&lt;/td>
 &lt;td>不管&lt;/td>
 &lt;td>key 分布、hot key 兩層 cache 自己處理&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>連線管理&lt;/td>
 &lt;td>提供 endpoint&lt;/td>
 &lt;td>連線池、socket timeout 自己設&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede&lt;/a> 的雪崩、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯&lt;/a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a> overview 的 implementation-layer deep article。選型層（為何用 managed、engine 選擇、跟自管取捨）見 overview；本文只處理「決定用 ElastiCache 後，哪些是 AWS 的責任、哪些仍是你的」。CLI 與計費以 <a href="https://docs.aws.amazon.com/elasticache/">AWS ElastiCache 官方文件</a>、<a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價</a> 為準、最後檢查日 2026-06-16（managed 服務的引數與價格會變、以官方為準）。</p></blockquote>
<h2 id="managed-不等於-hands-off">managed 不等於 hands-off</h2>
<p>把 cache 換成 ElastiCache 之後，最危險的心態是「現在 AWS 全包了」。AWS 確實接走了一大塊運維——它幫你做 failover、patching、snapshot、跨 AZ 複製，你不用再自己部署 Sentinel、不用半夜起來手動切 master。但有一類問題 ElastiCache 一個都沒幫你解，而且因為「以為 AWS 會處理」，這些問題在 managed 環境反而更容易被忽略到上線才爆。</p>
<p><a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>跑在 ElastiCache for Valkey 上、4700 萬月活、sub-millisecond 延遲——這證明 managed 撐得起極大規模，但 Tinder 仍要自己設計 key、處理 cache miss、控制 client 行為。ElastiCache for Redis 7.1 在 r7g.4xlarge 上單 node 可達約 100 萬 RPS、單 cluster 約 5 億 RPS（引自 <a href="https://aws.amazon.com/blogs/database/achieve-over-500-million-requests-per-second-per-cluster-with-amazon-elasticache-for-redis-7-1/">AWS Database Blog</a>）——這個吞吐是 AWS 給的，但用不用得好取決於你的 key 分布與 client 設計。</p>
<p>理解 ElastiCache 就是劃清這條責任邊界。本文按 shared responsibility 展開：AWS 管什麼、你管什麼、邊界上的踩坑在哪。</p>
<h2 id="核心概念shared-responsibility-的兩側">核心概念：shared responsibility 的兩側</h2>
<p>ElastiCache 的責任劃分可以列成一張清楚的表，這張表是判讀所有 ElastiCache 事故的起點：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>AWS 的責任（managed）</th>
          <th>你的責任（仍要自己做）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>硬體 / OS / patching</td>
          <td>全包</td>
          <td>—</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自動偵測 + replica 晉升</td>
          <td>client 要有 reconnect 邏輯</td>
      </tr>
      <tr>
          <td>跨 AZ 複製</td>
          <td>Multi-AZ 自動複製</td>
          <td>接受非同步複製的 stale window</td>
      </tr>
      <tr>
          <td>snapshot / backup</td>
          <td>自動 + 手動 snapshot</td>
          <td>決定保留策略、驗證能還原</td>
      </tr>
      <tr>
          <td>eviction</td>
          <td>提供 maxmemory-policy 參數</td>
          <td>選對 policy、設對 TTL</td>
      </tr>
      <tr>
          <td>cache stampede</td>
          <td>不管</td>
          <td>client-side jitter / singleflight 自己做</td>
      </tr>
      <tr>
          <td>key 設計 / hot key</td>
          <td>不管</td>
          <td>key 分布、hot key 兩層 cache 自己處理</td>
      </tr>
      <tr>
          <td>連線管理</td>
          <td>提供 endpoint</td>
          <td>連線池、socket timeout 自己設</td>
      </tr>
  </tbody>
</table>
<p>左欄是用 managed 換到的，右欄是用 managed 換不掉的。<a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9 cache stampede</a> 的雪崩、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線風暴</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 選錯</a> 在 ElastiCache 上跟自管 Redis 一模一樣會發生——因為這些是 cache 使用方式的問題，不是運維的問題。</p>
<h3 id="engine-選擇與-cluster-mode">engine 選擇與 cluster mode</h3>
<p>ElastiCache 的兩個結構性決策：</p>
<p><strong>engine</strong>：2024 起 default 是 Valkey（成本約低 20%、OSI 開源、Redis 7.2.4 fork、API 相容）；Redis OSS 仍可選但 AWS 不推；Memcached 是另一條線（純 KV、無 cluster mode 概念）。新部署或既有 Redis 遷移都走 Valkey（相容、便宜），純 cache 才考慮 Memcached。</p>
<p><strong>cluster mode</strong>：disabled 是 1 primary + 最多 5 replica、單 shard、上限約 340GB；enabled 是多 shard（最多 500）、自動 sharding、橫向擴展。判讀：dataset &lt; 300GB 且不需 sharding 用 disabled（簡單），&gt; 300GB 或要橫向擴展用 enabled（但 client 要 cluster-aware）。</p>
<h2 id="配置建立與治理的設定路徑">配置：建立與治理的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 建立 Valkey replication group（Multi-AZ、auto failover、cluster mode disabled）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">aws elasticache create-replication-group <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --replication-group-id prod-cache <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --replication-group-description <span class="s2">&#34;prod cache&#34;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --engine valkey <span class="se">\
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="se"></span>  --cache-node-type cache.r7g.large <span class="se">\
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="se"></span>  --num-cache-clusters <span class="m">3</span> <span class="se">\ </span>          <span class="c1"># 1 primary + 2 replica</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  --automatic-failover-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --multi-az-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --snapshot-retention-limit <span class="m">7</span> <span class="se">\ </span>    <span class="c1"># 自動 snapshot 保留 7 天</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">  --at-rest-encryption-enabled <span class="se">\
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="se"></span>  --transit-encryption-enabled
</span></span><span class="line"><span class="ln">13</span><span class="cl">
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1"># 自訂 parameter group（maxmemory-policy 等仍是你的責任）</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">aws elasticache create-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="se"></span>  --cache-parameter-group-family valkey8 <span class="se">\
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="se"></span>  --description <span class="s2">&#34;prod cache params&#34;</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">aws elasticache modify-cache-parameter-group <span class="se">\
</span></span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="se"></span>  --cache-parameter-group-name prod-params <span class="se">\
</span></span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="se"></span>  --parameter-name-values <span class="s2">&#34;ParameterName=maxmemory-policy,ParameterValue=allkeys-lru&#34;</span></span></span></code></pre></div><p>配置判讀：</p>
<ul>
<li><code>--automatic-failover-enabled</code> + <code>--multi-az-enabled</code> 是 HA 的核心，把 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 那條 failover 時序鏈</a>託管掉</li>
<li><code>maxmemory-policy</code> 透過 parameter group 設定——AWS 給旋鈕、選哪個是你的責任（見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">eviction 調校</a>）</li>
<li><code>--transit-encryption-enabled</code> 加 TLS，但 TLS 增加 client 建連成本，連線池更重要</li>
<li>IAM authentication（Redis 7+）取代 AUTH password，對應 <a href="/blog/backend/07-security-data-protection/" data-link-title="模組七：資安與資料保護" data-link-desc="以問題驅動方式擴充資安知識網：先定義服務環節問題，再以案例作為觸發式參考">security 模組</a></li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1failover-期間-client-持續-error">Case 1：failover 期間 client 持續 error</h3>
<p><strong>徵兆</strong>：ElastiCache 觸發 failover（看 <code>describe-events</code>），AWS 端 replica 晉升完成，但 application 持續 30 秒到幾分鐘大量連線 error。</p>
<p><strong>根因</strong>：failover 時 primary endpoint 的 DNS 切到新 primary，但 client 的連線池還握著舊 primary 的連線、DNS 也可能有快取。AWS 完成了 failover，但 client 重連是你的責任——ElastiCache 不會幫你的 application 重連。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 用支援自動重連的 library，設合理的 socket timeout 與 retry（見 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線調校</a>）</li>
<li>連到 primary endpoint（會跟著 failover 更新 DNS），不要連到特定 node 的 endpoint</li>
<li>縮短 client 的 DNS 快取 TTL，讓 failover 後的 DNS 切換更快被看到</li>
<li>failover 期間的寫入中斷無法完全避免（非同步複製 + 重連時間），latency-sensitive 服務要設計降級</li>
</ol>
<h3 id="case-2跨-az-replication-lag-造成-stale-read">Case 2：跨 AZ replication lag 造成 stale read</h3>
<p><strong>徵兆</strong>：寫入 primary 後立刻從 replica 讀，偶爾讀到舊值；CloudWatch 的 <code>ReplicationLag</code> 在高寫入時段上升。</p>
<p><strong>根因</strong>：ElastiCache 的跨 AZ 複製是非同步的，replica 有 lag。AWS 保證複製會發生，但不保證即時——read-from-replica 在寫後立即讀的場景會看到 stale window。這跟自管 Redis 的 replica 行為一致，managed 沒有消除它。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>寫後需要立即一致讀的路徑，強制 read from primary</li>
<li>監控 CloudWatch <code>ReplicationLag</code>，持續高代表寫入超過複製能力，要 scale up node 或降寫入</li>
<li>接受 cache 的最終一致性——這是 cache copy 的本質，不是 bug（見 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a>）</li>
<li>需要強一致 + durability 走 MemoryDB（見本文 Capacity / cost 邊界段）</li>
</ol>
<h3 id="case-3serverless-計費超出預期">Case 3：Serverless 計費超出預期</h3>
<p><strong>徵兆</strong>：用了 ElastiCache Serverless 想省容量規劃，月底帳單遠超預期。</p>
<p><strong>根因</strong>：Serverless 按 ECPU（運算）+ storage 計費，流量尖峰或低效的 access pattern（大量小命令、大 value）會推高 ECPU 消耗。Serverless 解的是「不想規劃容量」，不是「一定更便宜」——可預測的穩態流量用 node-based + Reserved Instance 通常更省。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>流量可預測、穩態高的 workload 用 node-based + Reserved Instance（1/3 年承諾、折扣約 30-60%）</li>
<li>流量不可預測、有大量閒置時段的才適合 Serverless</li>
<li>監控 ECPU 消耗，找出推高成本的 access pattern（用 pipeline 合併小命令降 ECPU）</li>
<li>成本模型對比要算實際 workload，不要假設 Serverless 一定划算</li>
</ol>
<h3 id="case-4cluster-mode-enabled-但-client-不是-cluster-aware">Case 4：cluster mode enabled 但 client 不是 cluster-aware</h3>
<p><strong>徵兆</strong>：建了 cluster mode enabled 的 cluster，application 連線報 <code>MOVED</code> redirect 或連不上某些 key。</p>
<p><strong>根因</strong>：cluster mode enabled 把 keyspace 分到多 shard，client 必須 cluster-aware（懂 <code>CLUSTER SLOTS</code>、處理 <code>MOVED</code>/<code>ASK</code> redirect）才能正確路由。普通 standalone client 連 cluster mode enabled 會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cluster mode enabled 一律用 cluster-aware client（連 configuration endpoint 不是單一 node）</li>
<li>確認 application 的多 key 操作用 hash tag 把相關 key co-locate 同 slot（見 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding</a>）</li>
<li>dataset &lt; 300GB 且不需 sharding，用 cluster mode disabled 省掉這層複雜度</li>
<li>從 disabled 升 enabled 是有成本的架構變更，初期規劃就要決定</li>
</ol>
<h3 id="case-5snapshot-期間記憶體尖峰node-不穩">Case 5：snapshot 期間記憶體尖峰、node 不穩</h3>
<p><strong>徵兆</strong>：自動 snapshot 時段 node 延遲上升、<code>DatabaseMemoryUsagePercentage</code> 衝高，偶爾 snapshot 失敗。</p>
<p><strong>根因</strong>：Redis engine 的 snapshot 靠 fork（見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a>），fork 期間 copy-on-write 推高記憶體。如果 node 記憶體已吃緊，snapshot 的 fork 把它推爆。AWS 託管了 snapshot 排程，但 fork 的記憶體成本仍在 engine 層存在。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>node 記憶體留 headroom（不要長期 &gt; 80%），給 snapshot 的 fork copy-on-write 空間</li>
<li>snapshot window 設在低流量時段，減少 fork 期間被改的 page</li>
<li>監控 CloudWatch <code>DatabaseMemoryUsagePercentage</code>，&gt; 80% 考慮 scale up node type</li>
<li>Valkey engine 繼承 Redis 的 fork 模型，這個成本換 engine 到 Valkey 也還在（fork-less 要 DragonflyDB、但 ElastiCache 不提供）</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>ElastiCache 的容量判讀，混合了 AWS 的 metric 與 engine 層的行為：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>DatabaseMemoryUsagePercentage</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 80% → scale up node 或調 maxmemory-policy</td>
      </tr>
      <tr>
          <td><code>ReplicationLag</code></td>
          <td>&lt; 1 秒</td>
          <td>持續高 → 寫入超過複製能力</td>
      </tr>
      <tr>
          <td><code>CurrConnections</code></td>
          <td>遠低於 node 上限</td>
          <td>接近上限 → client 連線池問題</td>
      </tr>
      <tr>
          <td><code>CacheHitRate</code></td>
          <td>&gt; 90%（多數 cache）</td>
          <td>下滑 → TTL / eviction / key 設計問題</td>
      </tr>
      <tr>
          <td>Serverless ECPU</td>
          <td>對齊預算</td>
          <td>暴衝 → access pattern 低效、用 pipeline 合併</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 source-of-truth 的 Redis API（不是 cache）</strong>：ElastiCache 是 cache 語意（資料可重建）。需要 durability 走 <strong>AWS MemoryDB</strong>——Redis-compatible 但有 multi-AZ transaction log、提供 source-of-truth 語意，成本約 ElastiCache 的 2-3 倍。判讀：<a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 把 feature store 從 ScyllaDB 遷到 ElastiCache</a> 的前提是「feature 可重新計算」——可重建選 ElastiCache，不可重建選 MemoryDB 或 database。</li>
<li><strong>跨雲 / 不在 AWS 生態</strong>：ElastiCache 綁 AWS，跨雲走自管 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> 或 GCP Memorystore / Azure Cache。</li>
<li><strong>極端單機 throughput</strong>：要榨單機多核走自管 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（ElastiCache 不提供 Dragonfly engine）。</li>
<li><strong>跨 region active-passive DR</strong>：ElastiCache 的 Global Datastore（1 primary region + 多 secondary read replica、跨 region lag &lt; 1 秒），不支援 active-active multi-master。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>ElastiCache 的 deep article 本質是「劃清 managed 邊界」，它跟 engine 層的調校知識緊密相連：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis 全系列 deep article</a></strong>：eviction、persistence/fork、連線的調校在 ElastiCache 上仍適用（engine 是 Redis/Valkey），AWS 託管的是 failover/patching/snapshot 排程，不是這些 engine 行為。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></strong>：ElastiCache 的 default engine 就是 Valkey，相容性與 io-threads 的判讀直接適用。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 是 Netflix 自管的 Memcached-based 全域 cache，對照 ElastiCache for Memcached + Global Datastore——展示了自管跨區 vs managed 跨區的取捨。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi</a></strong>：兩個 ElastiCache 規模化案例，一個是 sub-ms 配對引擎、一個是 ML feature store p99&lt;10ms，都展示了「AWS 給吞吐、你給設計」的邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>engine 層 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 與 failover 時序</a>、<a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性</a></li>
<li>上游能力：<a href="/blog/backend/00-service-selection/cost-risk-tradeoffs/" data-link-title="0.6 成本、風險與選型取捨" data-link-desc="用人力成本、雲端成本、操作成本與失敗代價判斷後端能力投入順序">0.6 成本取捨</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Caffeine + Redis 兩層 cache：搭起來很容易，跨實例失效才是全部的問題</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/two-tier-cache-invalidation/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine&lt;/a> overview 的 implementation-layer deep article。選型層（Caffeine vs Redis、process-local 的定位）見 overview；本文只處理「決定用 L1 Caffeine + L2 Redis 後，跨實例一致性怎麼處理」。API 以 &lt;a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="兩層-cache-搭起來容易難的在後面">兩層 cache 搭起來容易，難的在後面&lt;/h2>
&lt;p>L1 Caffeine + L2 Redis 的兩層 cache，讀寫路徑三十行 Java 就寫完：讀的時候先查 L1（process-local、奈秒級），miss 再查 L2（Redis、毫秒級），再 miss 才回源。它擋掉了大部分 Redis 的網路往返，對「每個請求重複讀同一份小資料」的場景效果立竿見影。&lt;/p>
&lt;p>真正的難度不在搭兩層，在「每個 JVM 實例有自己的 L1 副本」這個事實。假設有 10 個 application 實例，就有 10 份獨立的 Caffeine cache。實例 A 更新了某個 user 的資料、寫進 L2 Redis，但實例 B、C、D&amp;hellip; 的 L1 還握著舊值——它們不知道資料變了。下一個打到實例 B 的請求，L1 命中，回的是舊值。Redis 是對的，但讀不到 Redis，因為 L1 先攔截了。&lt;/p>
&lt;p>這就是兩層 cache 的核心問題：L1 的速度來自「不跟任何人協調」，而一致性恰恰需要協調。本文聚焦這個矛盾——兩層讀寫路徑只是背景，跨實例 invalidation 才是全部的工程量。&lt;/p>
&lt;h2 id="核心概念l1-的-stale-從哪裡來">核心概念：L1 的 stale 從哪裡來&lt;/h2>
&lt;p>兩層 cache 的一致性問題，根源是 L1 的三個特性：&lt;/p>
&lt;p>&lt;strong>L1 是 per-instance 的私有副本&lt;/strong>。Caffeine 活在 JVM heap 內，每個實例一份。這是它快的原因（無網路、無序列化），也是它難一致的原因（無法被其他實例直接更新或清除）。L2 Redis 是共享的，所以 L2 一致相對容易；L1 才是 stale 的來源。&lt;/p>
&lt;p>&lt;strong>寫入只更新本地 L1 + 共享 L2&lt;/strong>。實例 A 處理一個更新：寫 L2 Redis（所有實例可見）+ 更新或清除自己的 L1。但 A 沒有辦法直接碰 B 的 L1——B 的 L1 還是舊的，直到它自己過期或被通知。&lt;/p>
&lt;p>&lt;strong>沒有通知機制，L1 只能靠 TTL 自然過期&lt;/strong>。如果不做任何跨實例協調，L1 的 stale window 就等於 L1 的 TTL。把 L1 TTL 設短（幾秒到幾十秒）是最簡單的「容忍 stale」策略——犧牲一點新鮮度換掉協調的複雜度。需要更快失效就得主動廣播。&lt;/p>
&lt;p>跨實例失效的標準解法是用 L2 Redis 的 pub/sub 當廣播通道：任一實例更新資料時，往一個 channel 發一條「key X 失效了」的訊息，所有實例訂閱這個 channel、收到就清掉自己 L1 對應的 entry。這把「各自為政的 L1」連成一個能協同失效的網。&lt;/p>
&lt;h2 id="配置兩層讀寫--pubsub-失效的程式碼">配置：兩層讀寫 + pub/sub 失效的程式碼&lt;/h2>
&lt;p>兩層讀取路徑（L1 → L2 → origin）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-java" data-lang="java">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="kd">public&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="nf">getUser&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1：Caffeine、奈秒級、命中就回&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">User&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">getIfPresent&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1 miss → L2 Redis、毫秒級&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">String&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">redis&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">get&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">if&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">!=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="kc">null&lt;/span>&lt;span class="p">)&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="p">{&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">deserialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">json&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// 回填 L1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="p">}&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">14&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="c1">// L2 miss → 回源 + 雙層回填&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">15&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">=&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">userRepository&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">findById&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">16&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">redis&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">setex&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="s">&amp;#34;user:&amp;#34;&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="o">+&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">300&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">serialize&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">));&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// L2 TTL 5 分鐘&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">17&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="n">l1&lt;/span>&lt;span class="p">.&lt;/span>&lt;span class="na">put&lt;/span>&lt;span class="p">(&lt;/span>&lt;span class="n">id&lt;/span>&lt;span class="p">,&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">);&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="c1">// L1&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">18&lt;/span>&lt;span class="cl">&lt;span class="w"> &lt;/span>&lt;span class="k">return&lt;/span>&lt;span class="w"> &lt;/span>&lt;span class="n">u&lt;/span>&lt;span class="p">;&lt;/span>&lt;span class="w">
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">19&lt;/span>&lt;span class="cl">&lt;span class="w">&lt;/span>&lt;span class="p">}&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>跨實例失效（寫入時往 Redis pub/sub 廣播、所有實例清 L1）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a> overview 的 implementation-layer deep article。選型層（Caffeine vs Redis、process-local 的定位）見 overview；本文只處理「決定用 L1 Caffeine + L2 Redis 後，跨實例一致性怎麼處理」。API 以 <a href="https://github.com/ben-manes/caffeine/wiki">Caffeine wiki</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="兩層-cache-搭起來容易難的在後面">兩層 cache 搭起來容易，難的在後面</h2>
<p>L1 Caffeine + L2 Redis 的兩層 cache，讀寫路徑三十行 Java 就寫完：讀的時候先查 L1（process-local、奈秒級），miss 再查 L2（Redis、毫秒級），再 miss 才回源。它擋掉了大部分 Redis 的網路往返，對「每個請求重複讀同一份小資料」的場景效果立竿見影。</p>
<p>真正的難度不在搭兩層，在「每個 JVM 實例有自己的 L1 副本」這個事實。假設有 10 個 application 實例，就有 10 份獨立的 Caffeine cache。實例 A 更新了某個 user 的資料、寫進 L2 Redis，但實例 B、C、D&hellip; 的 L1 還握著舊值——它們不知道資料變了。下一個打到實例 B 的請求，L1 命中，回的是舊值。Redis 是對的，但讀不到 Redis，因為 L1 先攔截了。</p>
<p>這就是兩層 cache 的核心問題：L1 的速度來自「不跟任何人協調」，而一致性恰恰需要協調。本文聚焦這個矛盾——兩層讀寫路徑只是背景，跨實例 invalidation 才是全部的工程量。</p>
<h2 id="核心概念l1-的-stale-從哪裡來">核心概念：L1 的 stale 從哪裡來</h2>
<p>兩層 cache 的一致性問題，根源是 L1 的三個特性：</p>
<p><strong>L1 是 per-instance 的私有副本</strong>。Caffeine 活在 JVM heap 內，每個實例一份。這是它快的原因（無網路、無序列化），也是它難一致的原因（無法被其他實例直接更新或清除）。L2 Redis 是共享的，所以 L2 一致相對容易；L1 才是 stale 的來源。</p>
<p><strong>寫入只更新本地 L1 + 共享 L2</strong>。實例 A 處理一個更新：寫 L2 Redis（所有實例可見）+ 更新或清除自己的 L1。但 A 沒有辦法直接碰 B 的 L1——B 的 L1 還是舊的，直到它自己過期或被通知。</p>
<p><strong>沒有通知機制，L1 只能靠 TTL 自然過期</strong>。如果不做任何跨實例協調，L1 的 stale window 就等於 L1 的 TTL。把 L1 TTL 設短（幾秒到幾十秒）是最簡單的「容忍 stale」策略——犧牲一點新鮮度換掉協調的複雜度。需要更快失效就得主動廣播。</p>
<p>跨實例失效的標準解法是用 L2 Redis 的 pub/sub 當廣播通道：任一實例更新資料時，往一個 channel 發一條「key X 失效了」的訊息，所有實例訂閱這個 channel、收到就清掉自己 L1 對應的 entry。這把「各自為政的 L1」連成一個能協同失效的網。</p>
<h2 id="配置兩層讀寫--pubsub-失效的程式碼">配置：兩層讀寫 + pub/sub 失效的程式碼</h2>
<p>兩層讀取路徑（L1 → L2 → origin）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kd">public</span><span class="w"> </span><span class="n">User</span><span class="w"> </span><span class="nf">getUser</span><span class="p">(</span><span class="n">String</span><span class="w"> </span><span class="n">id</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w">    </span><span class="c1">// L1：Caffeine、奈秒級、命中就回</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">l1</span><span class="p">.</span><span class="na">getIfPresent</span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">u</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">    </span><span class="c1">// L1 miss → L2 Redis、毫秒級</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w">    </span><span class="n">String</span><span class="w"> </span><span class="n">json</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">redis</span><span class="p">.</span><span class="na">get</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">    </span><span class="k">if</span><span class="w"> </span><span class="p">(</span><span class="n">json</span><span class="w"> </span><span class="o">!=</span><span class="w"> </span><span class="kc">null</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">        </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">deserialize</span><span class="p">(</span><span class="n">json</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">        </span><span class="n">l1</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">);</span><span class="w">                 </span><span class="c1">// 回填 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">        </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">    </span><span class="c1">// L2 miss → 回源 + 雙層回填</span><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w">    </span><span class="n">u</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">userRepository</span><span class="p">.</span><span class="na">findById</span><span class="p">(</span><span class="n">id</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">setex</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">300</span><span class="p">,</span><span class="w"> </span><span class="n">serialize</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w">  </span><span class="c1">// L2 TTL 5 分鐘</span><span class="w">
</span></span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="w">    </span><span class="n">l1</span><span class="p">.</span><span class="na">put</span><span class="p">(</span><span class="n">id</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">);</span><span class="w">                     </span><span class="c1">// L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="w">    </span><span class="k">return</span><span class="w"> </span><span class="n">u</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">19</span><span class="cl"><span class="w"></span><span class="p">}</span></span></span></code></pre></div><p>跨實例失效（寫入時往 Redis pub/sub 廣播、所有實例清 L1）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-java" data-lang="java"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1">// L1 設短 TTL 當保險（廣播漏掉時的上界）</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">Cache</span><span class="o">&lt;</span><span class="n">String</span><span class="p">,</span><span class="w"> </span><span class="n">User</span><span class="o">&gt;</span><span class="w"> </span><span class="n">l1</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">Caffeine</span><span class="p">.</span><span class="na">newBuilder</span><span class="p">()</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">maximumSize</span><span class="p">(</span><span class="n">10_000</span><span class="p">)</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">expireAfterWrite</span><span class="p">(</span><span class="n">Duration</span><span class="p">.</span><span class="na">ofSeconds</span><span class="p">(</span><span class="n">30</span><span class="p">))</span><span class="w">  </span><span class="c1">// 廣播失效之外的兜底</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="w">    </span><span class="p">.</span><span class="na">build</span><span class="p">();</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="c1">// 寫入：更新 L2 + 廣播失效</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w"></span><span class="kd">public</span><span class="w"> </span><span class="kt">void</span><span class="w"> </span><span class="nf">updateUser</span><span class="p">(</span><span class="n">User</span><span class="w"> </span><span class="n">u</span><span class="p">)</span><span class="w"> </span><span class="p">{</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w">    </span><span class="n">userRepository</span><span class="p">.</span><span class="na">save</span><span class="p">(</span><span class="n">u</span><span class="p">);</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">setex</span><span class="p">(</span><span class="s">&#34;user:&#34;</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">(),</span><span class="w"> </span><span class="n">300</span><span class="p">,</span><span class="w"> </span><span class="n">serialize</span><span class="p">(</span><span class="n">u</span><span class="p">));</span><span class="w">  </span><span class="c1">// 更新 L2（TTL 對齊讀路徑的 300s）</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">    </span><span class="n">redis</span><span class="p">.</span><span class="na">publish</span><span class="p">(</span><span class="s">&#34;cache:invalidate&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">());</span><span class="w">   </span><span class="c1">// 廣播給所有實例</span><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w">    </span><span class="n">l1</span><span class="p">.</span><span class="na">invalidate</span><span class="p">(</span><span class="n">u</span><span class="p">.</span><span class="na">id</span><span class="p">());</span><span class="w">                        </span><span class="c1">// 清自己的 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="w"></span><span class="p">}</span><span class="w">
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="w"></span><span class="c1">// 每個實例啟動時訂閱、收到就清本地 L1</span><span class="w">
</span></span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="w"></span><span class="n">redis</span><span class="p">.</span><span class="na">subscribe</span><span class="p">(</span><span class="s">&#34;cache:invalidate&#34;</span><span class="p">,</span><span class="w"> </span><span class="n">message</span><span class="w"> </span><span class="o">-&gt;</span><span class="w"> </span><span class="n">l1</span><span class="p">.</span><span class="na">invalidate</span><span class="p">(</span><span class="n">message</span><span class="p">));</span></span></span></code></pre></div><p>關鍵：L1 的短 TTL 是廣播機制的兜底——即使某個實例漏掉一條 pub/sub 訊息（pub/sub 是 fire-and-forget、訂閱者離線會錯過），L1 最多 stale 到 TTL 過期。廣播負責「快」，TTL 負責「最終」。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1更新後其他實例持續回舊值">Case 1：更新後其他實例持續回舊值</h3>
<p><strong>徵兆</strong>：使用者改了資料、自己刷新看到新值（打到處理寫入的實例），但同事看到的還是舊值（打到別的實例），且持續好幾分鐘。</p>
<p><strong>根因</strong>：只更新了寫入實例的 L1 與 L2，沒有跨實例廣播。其他實例的 L1 還握著舊值、攔截了讀取、根本沒查到已更新的 L2。stale window 等於 L1 TTL（如果 TTL 設很長就是好幾分鐘）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>加 Redis pub/sub 廣播失效，寫入時通知所有實例清 L1</li>
<li>廣播之外把 L1 TTL 設短當兜底（幾秒到幾十秒），縮短漏訊息時的 stale 上界</li>
<li>強一致需求的資料根本不該進 L1——L1 的本質就是「容忍一個 stale window 換速度」</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a> 的新鮮度邊界判斷</li>
</ol>
<h3 id="case-2pubsub-漏訊息個別實例-l1-卡舊值">Case 2：pub/sub 漏訊息、個別實例 L1 卡舊值</h3>
<p><strong>徵兆</strong>：多數實例更新後正常，但偶爾某個實例持續回舊值，直到重啟或 TTL 過期。</p>
<p><strong>根因</strong>：Redis pub/sub 是 fire-and-forget——訂閱者在訊息發出的瞬間若斷線（網路抖動、GC pause、重連中），就永久錯過那條失效訊息。沒有兜底的話，那個實例的 L1 會一直 stale 到 TTL。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>L1 TTL 設短是必要兜底，不要依賴 pub/sub 100% 送達（它不保證）</li>
<li>需要可靠失效用 Redis Streams（有 consumer group + 重放）取代 pub/sub，代價是複雜度</li>
<li>監控各實例的 L1 命中率與 stale 投訴，個別實例異常代表漏訊息</li>
<li>接受 pub/sub 的 at-most-once 語意，用 TTL 補足最終一致</li>
</ol>
<h3 id="case-3l1-太大撐爆-heapfull-gc-風暴">Case 3：L1 太大撐爆 heap、Full GC 風暴</h3>
<p><strong>徵兆</strong>：加了 L1 後 application 的 GC 時間變長、偶發 Full GC 導致請求暫停（STW），延遲尖刺。</p>
<p><strong>根因</strong>：Caffeine 預設 on-heap，L1 的 <code>maximumSize</code> 設太大、cache 的物件佔據大量 heap，增加 GC 掃描與回收壓力。大物件 + 大容量直接推高 old gen 佔用。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>maximumSize</code> 對齊 heap 預算，用 <code>recordStats()</code> 看實際記憶體佔用</li>
<li>用 <code>maximumWeight</code> + weigher 按物件實際大小限制（不只筆數），避免大物件撐爆</li>
<li>L1 只放「小、熱、重複讀」的資料，大物件留 L2 Redis（off-heap 視角）</li>
<li>監控 GC 時間與 old gen 佔用，L1 容量是可調的 GC 旋鈕</li>
</ol>
<h3 id="case-4l1-快取了不該快取的-per-user-大物件">Case 4：L1 快取了不該快取的 per-user 大物件</h3>
<p><strong>徵兆</strong>：L1 命中率偏低、heap 壓力大、效果不如預期。</p>
<p><strong>根因</strong>：把 per-user 的大物件或低重複率的資料放 L1。L1 的價值在「少量資料被大量重複讀」（如設定檔、熱門商品、權限表），per-user 資料每個 user 一份、重複率低、塞滿 L1 又命中率低。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>L1 只放高重複率的共享熱資料（config、feature flag、熱門 item、權限）</li>
<li>per-user 低重複資料放 L2 Redis 就好，不要進 L1</li>
<li>用 <code>recordStats()</code> 的 hit rate 驗證——L1 命中率低代表放錯資料</li>
<li>對應 <a href="/blog/backend/02-cache-redis/cache-data-shape-access-pattern/" data-link-title="2.8 Cache Data Shape 與 Access Pattern" data-link-desc="說明 cache value、key space、資料結構與存取型態如何反映服務語意。">2.4 cache data shape</a> 的存取形狀判斷</li>
</ol>
<h3 id="case-5refreshafterwrite-與-expireafterwrite-混淆行為不如預期">Case 5：refreshAfterWrite 與 expireAfterWrite 混淆、行為不如預期</h3>
<p><strong>徵兆</strong>：以為設了自動刷新、結果到期還是 miss 阻塞回源；或以為會過期、結果一直回舊值。</p>
<p><strong>根因</strong>：<code>expireAfterWrite</code>（到期 entry 失效、下次讀 miss + 阻塞載入）跟 <code>refreshAfterWrite</code>（到期後第一個讀觸發背景刷新、舊值立即回、不阻塞）語意不同，混用導致行為不符預期。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>要「到期就不可用」用 <code>expireAfterWrite</code>；要「到期背景刷新、舊值先頂」用 <code>refreshAfterWrite</code></li>
<li>兩者可組合：<code>refreshAfterWrite</code> 短 + <code>expireAfterWrite</code> 長，得到「背景刷新 + 最終過期」</li>
<li><code>refreshAfterWrite</code> 避免 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">stampede</a>（舊值先服務、單一背景刷新），適合熱 key</li>
<li>用 <code>LoadingCache</code> 的 <code>build(key -&gt; load)</code> 配 refresh，行為以官方 wiki 為準</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>兩層 cache 的容量判讀，核心在 L1 命中率、stale window 與 GC：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>L1 hit rate</td>
          <td>高（放對高重複資料）</td>
          <td>低 → 放錯資料（per-user 大物件）、改放 L2</td>
      </tr>
      <tr>
          <td>L1 stale window</td>
          <td>≤ L1 TTL（廣播正常更短）</td>
          <td>過長 → TTL 太長或廣播沒做</td>
      </tr>
      <tr>
          <td>GC 時間 / old gen 佔用</td>
          <td>穩定、無 Full GC 風暴</td>
          <td>升高 → L1 太大、降 maximumSize / maximumWeight</td>
      </tr>
      <tr>
          <td>pub/sub 失效送達率</td>
          <td>高（但不保證 100%）</td>
          <td>漏訊息 → TTL 兜底、或改 Streams</td>
      </tr>
      <tr>
          <td>L1 vs L2 命中分層</td>
          <td>L1 擋大部分、L2 擋 L1 miss</td>
          <td>L1 命中低 → 兩層沒分工好</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要強一致 / 不能容忍任何 stale</strong>：L1 process-local 本質有 stale window，不該放這類資料。強一致只用 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a> 共享層（甚至直接回源）。</li>
<li><strong>L1 容量需求超過 heap</strong>：on-heap Caffeine 撐不住，用 off-heap 方案（Ehcache off-heap tier）或把資料留 L2 Redis。</li>
<li><strong>可靠失效（不能漏）</strong>：pub/sub 是 at-most-once，要可靠用 Redis Streams 的 consumer group，代價是複雜度。</li>
<li><strong>非 JVM 服務</strong>：Caffeine 綁 JVM，其他語言用對應的 process-local cache（Go ristretto、Rust moka），兩層架構的思路相同。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>兩層 cache 的工程量集中在跨實例一致性，它跟多個議題交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine overview</a></strong>：overview 點到「跨實例 invalidation 是固有限制」、本文展開 pub/sub 廣播 + TTL 兜底的具體解法。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis connection / pipeline</a></strong>：L1 的價值正是消除 L2 Redis 的 RTT 稅，兩層 cache 是 RTT 優化的極致（L1 命中連網路都省）。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></strong>：hot key 的兩層解法（local cache + Redis）就是這個架構，L1 擋掉打在單一熱 key 的洪峰。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder</a></strong>：每次互動查多個 cache 的服務，L1 Caffeine 可擋掉重複讀、降低 L2（ElastiCache）的壓力與 RTT——但 per-user 配對資料重複率低、要判斷哪些放得進 L1。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/caffeine/" data-link-title="Caffeine" data-link-desc="JVM process-local cache、Window TinyLFU、Guava Cache 後繼">Caffeine</a></li>
<li>L2 對照：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>、<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a> overview 的 implementation-layer deep article。選型層（為何選 DragonflyDB、BSL 授權、相容度）見 overview；本文只處理「決定用 DragonflyDB 後，多核架構怎麼用、相容邊界在哪」。命令實機驗證於 dragonfly df-v1.39.0（&lt;code>redis_version:7.4.0&lt;/code>）、最後檢查日 2026-06-16；效能數字以 &lt;a href="https://www.dragonflydb.io/">DragonflyDB 官方 benchmark&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="scale-up-還是-scale-out一個架構賭注">scale-up 還是 scale-out：一個架構賭注&lt;/h2>
&lt;p>把一台 32 核機器交給 Redis，Redis 的主執行緒只用得到其中一核處理命令——要榨乾這台機器，你得在同一台上跑好幾個 Redis 進程、組成 Cluster、用 hash slot 把 key 分片。多核利用變成了一個分散式系統問題（&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding&lt;/a>、cross-slot transaction、hash tag 治理全都來了）。&lt;/p>
&lt;p>DragonflyDB 賭的是相反方向：一個進程、thread-per-core、shared-nothing，讓單機在不分片的情況下用滿所有核。它的論點是——多數「需要 Redis Cluster」的場景，真正的需求是吞吐與記憶體，不是跨機器分散；如果單機就能撐到那個規模，Cluster 的複雜度就不必付。實機可以看到這個架構：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">redis-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;thread_count|redis_version|dragonfly_version&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># thread_count:8 ← 自動對齊 CPU 核數&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.4.0 ← 對 client 裝成 Redis 7.4&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># dragonfly_version:df-v1.39.0&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>thread_count:8&lt;/code> 在一個進程內，不是 8 個 Redis 進程組 Cluster。這就是賭注的核心：把 Redis Cluster 的水平分片，收進單一進程的垂直多核。理解 DragonflyDB 就是理解這個賭注成立的條件與它撞牆的地方。&lt;/p>
&lt;p>對高吞吐單機 workload，這個賭注有現成的對照。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 用 KeyDB&lt;/a>（Redis 的 multi-threaded fork、單實例吞吐提升 5-10x）撐超高吞吐 cache，正是「不想為了多核去組 Cluster」的同一類需求；DragonflyDB 是這條路線更激進的版本（從零用 C++ 重寫、不是在 Redis 上加 thread）。&lt;/p>
&lt;h2 id="核心概念thread-per-core-與-shared-nothing">核心概念：thread-per-core 與 shared-nothing&lt;/h2>
&lt;p>DragonflyDB 的多核不是「多個執行緒搶同一份資料」，而是把資料切給各個執行緒、彼此不共享——這是它能線性擴展到多核的關鍵。&lt;/p>
&lt;p>&lt;strong>thread-per-core + 資料分區&lt;/strong>。每個 thread 綁一個核，keyspace 被 hash 切成多個 slice，每個 slice 只由一個 thread 擁有。一個命令進來，被路由到擁有該 key 的 thread 處理。因為一個 key 只有一個 thread 碰，單 key 操作不需要鎖——這消除了 Redis 多執行緒方案最大的開銷（lock contention）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> overview 的 implementation-layer deep article。選型層（為何選 DragonflyDB、BSL 授權、相容度）見 overview；本文只處理「決定用 DragonflyDB 後，多核架構怎麼用、相容邊界在哪」。命令實機驗證於 dragonfly df-v1.39.0（<code>redis_version:7.4.0</code>）、最後檢查日 2026-06-16；效能數字以 <a href="https://www.dragonflydb.io/">DragonflyDB 官方 benchmark</a> 為準。</p></blockquote>
<h2 id="scale-up-還是-scale-out一個架構賭注">scale-up 還是 scale-out：一個架構賭注</h2>
<p>把一台 32 核機器交給 Redis，Redis 的主執行緒只用得到其中一核處理命令——要榨乾這台機器，你得在同一台上跑好幾個 Redis 進程、組成 Cluster、用 hash slot 把 key 分片。多核利用變成了一個分散式系統問題（<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">cluster re-sharding</a>、cross-slot transaction、hash tag 治理全都來了）。</p>
<p>DragonflyDB 賭的是相反方向：一個進程、thread-per-core、shared-nothing，讓單機在不分片的情況下用滿所有核。它的論點是——多數「需要 Redis Cluster」的場景，真正的需求是吞吐與記憶體，不是跨機器分散；如果單機就能撐到那個規模，Cluster 的複雜度就不必付。實機可以看到這個架構：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;thread_count|redis_version|dragonfly_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># thread_count:8               ← 自動對齊 CPU 核數</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># redis_version:7.4.0          ← 對 client 裝成 Redis 7.4</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># dragonfly_version:df-v1.39.0</span></span></span></code></pre></div><p><code>thread_count:8</code> 在一個進程內，不是 8 個 Redis 進程組 Cluster。這就是賭注的核心：把 Redis Cluster 的水平分片，收進單一進程的垂直多核。理解 DragonflyDB 就是理解這個賭注成立的條件與它撞牆的地方。</p>
<p>對高吞吐單機 workload，這個賭注有現成的對照。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 用 KeyDB</a>（Redis 的 multi-threaded fork、單實例吞吐提升 5-10x）撐超高吞吐 cache，正是「不想為了多核去組 Cluster」的同一類需求；DragonflyDB 是這條路線更激進的版本（從零用 C++ 重寫、不是在 Redis 上加 thread）。</p>
<h2 id="核心概念thread-per-core-與-shared-nothing">核心概念：thread-per-core 與 shared-nothing</h2>
<p>DragonflyDB 的多核不是「多個執行緒搶同一份資料」，而是把資料切給各個執行緒、彼此不共享——這是它能線性擴展到多核的關鍵。</p>
<p><strong>thread-per-core + 資料分區</strong>。每個 thread 綁一個核，keyspace 被 hash 切成多個 slice，每個 slice 只由一個 thread 擁有。一個命令進來，被路由到擁有該 key 的 thread 處理。因為一個 key 只有一個 thread 碰，單 key 操作不需要鎖——這消除了 Redis 多執行緒方案最大的開銷（lock contention）。</p>
<p><strong>dashtable 取代 Redis 的 dict</strong>。DragonflyDB 用自製的 dashtable（一種 hash table）取代 Redis 的 dictionary，記憶體佈局更緊湊、resize 時不需要像 Redis 那樣漸進式 rehash 全表，同樣的 dataset 通常比 Redis 省 20-40% 記憶體（依資料形狀，以官方 benchmark 為準）。</p>
<p><strong>fork-less snapshot</strong>。Redis 的持久化靠 <code>fork()</code>，大記憶體下會凍結主執行緒並讓記憶體接近翻倍（見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence deep article</a>）。DragonflyDB 不用 fork——它用自己的快照演算法在不複製整個進程的前提下做一致性快照，大記憶體場景不付 fork 的延遲尖峰與記憶體翻倍代價。這是它對「fork 是 Redis 結構性瓶頸」這個痛點的直接回答。</p>
<p><strong>多執行緒的代價：沒有 Redis Cluster mode</strong>。資料分區在單進程內，DragonflyDB 不提供 Redis Cluster mode（它的哲學是單機撐大、不跨機器分片）。這個取捨決定了它的相容邊界與容量天花板，是後面踩坑的根源。</p>
<h2 id="配置多核與持久化的設定路徑">配置：多核與持久化的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker run -d --name dragonfly -p 6379:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>    --threads <span class="m">8</span> <span class="se">\ </span>             <span class="c1"># thread 數、預設等於 CPU 核數（一般不需手動設）</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    --maxmemory 4gb <span class="se">\ </span>         <span class="c1"># 記憶體上限、行為類似 Redis maxmemory</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    --cache_mode <span class="nb">true</span> <span class="se">\ </span>       <span class="c1"># 純 cache 模式：記憶體滿時自動 evict（類似 allkeys-lru）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    --snapshot_cron <span class="s2">&#34;0 3 * * *&#34;</span> <span class="c1"># fork-less snapshot 排程（cron 格式、這裡每天 3 點）</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>--threads</code> 預設對齊 CPU 核數，多數情況不需手動設；設小於核數會浪費核，設大於核數沒有意義</li>
<li><code>--cache_mode true</code> 讓 DragonflyDB 在記憶體滿時自動淘汰（純 cache 行為）；不開則記憶體滿時拒絕寫入（類似 Redis noeviction）</li>
<li><code>--maxmemory</code> 留 headroom，但因為 fork-less，headroom 不需要像 Redis 留那麼多給 fork copy-on-write</li>
<li>snapshot 用 <code>--snapshot_cron</code> 排程，fork-less 機制讓大記憶體快照不產生延遲尖峰</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1client-配-cluster-mode連不上">Case 1：client 配 Cluster mode、連不上</h3>
<p><strong>徵兆</strong>：從 Redis Cluster 遷來，application 的 client library 還配著 cluster mode，連 DragonflyDB 報錯或 hang，<code>CLUSTER</code> 相關命令行為不如預期。</p>
<p><strong>根因</strong>：DragonflyDB 不提供 Redis Cluster mode（單進程多核、不跨機器分片）。cluster-aware client 會嘗試 <code>CLUSTER SLOTS</code> 之類的拓樸發現，跟 standalone 的 DragonflyDB 對不上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 改回 standalone 配置（不要 cluster mode）</li>
<li>評估原本用 Cluster 的理由：若是為了多核吞吐，DragonflyDB 單進程多核已涵蓋，不需要 cluster mode</li>
<li>若原本用 Cluster 是為了超過單機的容量 / 跨機器分散，DragonflyDB 的 scale-up 模型撐不住，該留在 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a></li>
<li>確認 application 沒有依賴 cluster-specific 行為（hash tag 的跨 slot 語意等）</li>
</ol>
<h3 id="case-2某些-redis-命令--module-不支援">Case 2：某些 Redis 命令 / module 不支援</h3>
<p><strong>徵兆</strong>：核心 SET/GET/HASH 等正常，但某個命令報 <code>unknown command</code> 或行為跟 Redis 不同，特別是 module 命令（RedisJSON / RedisSearch）與部分冷門命令。</p>
<p><strong>根因</strong>：DragonflyDB 相容大多數 Redis 命令但不是 100%；它宣稱相容 <code>redis_version:7.4.0</code>，但部分 module、部分冷門命令、部分 Lua 行為有差異。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>遷移前盤點 application 用到的命令，對照 DragonflyDB 的 API 相容清單（官方 docs）</li>
<li>module 重度依賴（RedisJSON / RedisSearch）要特別確認——DragonflyDB 的 module 生態比 Redis 淺</li>
<li>Lua script 行為差異要實測，不要假設跟 Redis 完全一致</li>
<li>相容性是遷移的主要風險，跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 的相容性驗證</a>同理但 DragonflyDB 邊界更寬（重寫而非 fork）</li>
</ol>
<h3 id="case-3thread-沒對齊核數多核優勢沒發揮">Case 3：thread 沒對齊核數、多核優勢沒發揮</h3>
<p><strong>徵兆</strong>：吞吐沒有達到預期、CPU 使用率不均（部分核閒置），<code>thread_count</code> 跟機器核數對不上。</p>
<p><strong>根因</strong>：<code>--threads</code> 被手動設成小於 CPU 核數，或容器的 CPU limit 限制了實際可用核數，DragonflyDB 沒能用滿所有核。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>redis-cli INFO server | grep thread_count</code> 確認 thread 數對齊實體核數</li>
<li>容器環境確認 CPU limit 沒有卡住 DragonflyDB 的核數（cgroup CPU quota）</li>
<li>不要手動把 <code>--threads</code> 設小，預設對齊核數就是最佳</li>
<li>吞吐沒到預期也可能是 workload 本身（大命令、網路 RTT），用 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線 / pipeline</a> 的 RTT 分析交叉判斷</li>
</ol>
<h3 id="case-4跨-partition-的多-key-操作有額外成本">Case 4：跨 partition 的多 key 操作有額外成本</h3>
<p><strong>徵兆</strong>：大量多 key 命令（MGET 跨很多 key、跨 key 的 Lua）的延遲比預期高，單 key 操作則很快。</p>
<p><strong>根因</strong>：shared-nothing 下 key 分散在不同 thread，多 key 操作要跨 thread 協調——單 key 免鎖的好處在多 key 跨 partition 時要付協調成本。這跟 Redis Cluster 的 cross-slot 是類似的本質（資料分散的代價），只是發生在單進程內。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>高頻的多 key 操作盡量讓 key 落在同 partition（DragonflyDB 的 key 分布規則）</li>
<li>評估能否用單 key 結構（hash）取代多個 key 的聚合</li>
<li>跨 partition 協調是分區架構的固有成本，不是 bug，量大時要設計繞過</li>
<li>對照 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis Cluster 的 cross-slot 限制</a>，兩者都是「資料分散換吞吐」的代價</li>
</ol>
<h3 id="case-5bsl-授權踩到商業使用限制">Case 5：BSL 授權踩到商業使用限制</h3>
<p><strong>徵兆</strong>：準備把 DragonflyDB 包成對外的 managed service 提供給客戶，法務 review 卡關。</p>
<p><strong>根因</strong>：DragonflyDB 用 BSL（Business Source License），商業使用受限——具體限制是不可把 DragonflyDB 當成 managed service 對外提供（4 年後該版本轉 Apache 2.0）。內部使用無限制，但 SaaS 對外提供 DragonflyDB 即服務受限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>內部使用（多數企業場景）無限制，直接用</li>
<li>要把 DragonflyDB 當 managed service 對外賣，聯絡 DragonflyDB 取得商業 license</li>
<li>開源合規敏感（公部門 / 企業 OSI 政策）走 OSI 認可的 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（BSD）</li>
<li>授權法律解讀諮詢法務，不要憑技術判斷</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>DragonflyDB 的容量判讀，核心在 scale-up 的天花板與多核效率：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>thread_count</code></td>
          <td>= CPU 實體核數</td>
          <td>&lt; 核數 → 沒用滿多核、查 &ndash;threads / cgroup</td>
      </tr>
      <tr>
          <td>單機吞吐</td>
          <td>遠高於單 Redis 進程</td>
          <td>撞單機網路 / CPU 上限 → scale-up 到頂</td>
      </tr>
      <tr>
          <td>記憶體效率</td>
          <td>比 Redis 省 20-40%（依形狀）</td>
          <td>以官方 benchmark + 自己量為準</td>
      </tr>
      <tr>
          <td>snapshot 延遲尖峰</td>
          <td>接近 0（fork-less）</td>
          <td>有尖峰 → 確認用的是 DragonflyDB 快照不是相容路徑</td>
      </tr>
      <tr>
          <td>單機容量 / 跨 AZ 需求</td>
          <td>單機 + replica 撐得住</td>
          <td>超單機 / 要跨機器分散 → DragonflyDB 撐不住</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>超過單機容量、需要跨機器分散</strong>：DragonflyDB 的 scale-up 賭注在這裡輸——它沒有 Cluster mode。要跨機器分片走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis / Valkey Cluster</a>。</li>
<li><strong>需要 OSI 認可開源授權</strong>：BSL 不是 OSI 認可，合規敏感走 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（BSD）。</li>
<li><strong>不想自管</strong>：DragonflyDB 目前沒有 fully managed offering（無 ElastiCache for Dragonfly），必須自管。要 managed 走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a>（Redis / Valkey / Memcached）。</li>
<li><strong>跨 AZ / 跨 region HA</strong>：DragonflyDB 有 replica 模式（primary-replica）跨 AZ 可行，但跨 region 需自建——大規模跨區走 managed 的 Global Datastore。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>DragonflyDB 的定位是「Redis 相容 + 激進多核」，它在 Redis 相容服務的光譜上有明確座標：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：兩者都打「Redis 相容 + 更好的多核」，但 Valkey 是 fork（同源、最高相容、漸進加 thread），DragonflyDB 是 C++ 重寫（相容核心但架構激進、多核更徹底）。相容度要極致選 Valkey，多核吞吐要極致選 DragonflyDB。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> / Garnet</strong>：KeyDB 是 Redis 的 multi-threaded fork（<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 採用</a>、Snap 收購後相對停滯）；Garnet 是 Microsoft 的研究型高吞吐 store（生態淺）。DragonflyDB 是這個「高吞吐 Redis 替代」群裡商業化最積極、生態最活躍的。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster re-sharding</a></strong>：如果你的 Redis Cluster re-sharding 頻繁觸發、運維負擔重，DragonflyDB 的 scale-up 模型可能用單機取代整個 Cluster——這是評估遷移的主要動機。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">Shopify write-through</a></strong>：write-through 在 DragonflyDB 上行為一致，但單進程多核能承接比單 Redis 進程更大的 throughput，是 read-heavy + write-through 場景的 scale-up 選項。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性與 io-threads</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence 與 fork latency</a>（fork-less 對照的痛點）</li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>KeyDB active-active 多主複製：last-write-wins 會默默吃掉哪一筆寫入</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/active-active-replication/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB&lt;/a> overview 的 implementation-layer deep article。選型層（KeyDB vs Redis / DragonflyDB / Valkey、為何選 fork）見 overview；本文只處理「決定用 KeyDB active-active 後，衝突與一致性怎麼判」。命令實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；複製機制以 &lt;a href="https://docs.keydb.dev/docs/active-rep/">KeyDB active-replication 文件&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="兩邊都能寫聽起來太美好">兩邊都能寫，聽起來太美好&lt;/h2>
&lt;p>Redis 的複製是單向的：一個 master 寫、replica 唯讀。要跨區讓兩邊都能就近寫入，Redis 本身做不到（得靠應用層分區或外部工具）。KeyDB 的 active-active 把這個限制拿掉——兩個（含以上）KeyDB 節點都是 master、都能接受寫入、互相把寫入同步給對方。對「兩個 region 都要低延遲寫入同一份 cache」的場景，這聽起來解決了所有問題。&lt;/p>
&lt;p>問題藏在「兩邊同時寫同一個 key」的那一刻。active-active 沒有全域協調者來仲裁誰對誰錯，它用 last-write-wins（LWW）：比較兩筆寫入的時間戳，留下較晚的、默默丟掉較早的。多數時候沒事，但當兩個 region 在幾毫秒內各自更新同一個 key，其中一筆寫入會無聲消失——沒有錯誤、沒有日誌、application 以為自己寫成功了。&lt;/p>
&lt;p>理解 KeyDB active-active 就是理解這個取捨：它用 LWW 換到了「兩邊都能寫」的可用性，代價是放棄了強一致與「不丟寫入」的保證。本文展開複製機制、衝突語意，以及哪些資料放得進這個模型、哪些放進去就是 bug。&lt;/p>
&lt;h2 id="核心概念active-active-的複製與衝突語意">核心概念：active-active 的複製與衝突語意&lt;/h2>
&lt;p>active-active 不是「分散式交易」，它是「雙向非同步複製 + LWW 衝突解決」。理解它要抓三個點：&lt;/p>
&lt;p>&lt;strong>每個節點都是 active-replica&lt;/strong>。一般 Redis replica 是唯讀的；KeyDB 的 active-replica 既接受本地寫入、又接收對方的複製流。兩個節點互相設定對方為 master，形成雙向複製環。實機看到的 role 就是 &lt;code>active-replica&lt;/code>（不是 master / slave）。&lt;/p>
&lt;p>&lt;strong>複製是非同步的&lt;/strong>。本地寫入立即回 OK 給 client，之後才非同步傳給對方節點。這意味著兩個節點之間永遠有一個複製延遲窗口——在這個窗口內，兩邊看到的資料可能不同。這是 active-active 是 AP（可用性 + 分區容忍）而非 CP 的根本原因。&lt;/p>
&lt;p>&lt;strong>衝突用 last-write-wins 解決&lt;/strong>。同一個 key 在兩個節點被並發修改時，KeyDB 比較版本，保留較晚的寫入、丟棄較早的。沒有 merge、沒有 vector clock、沒有 application callback——就是比誰較晚。KeyDB 用 hybrid logical clock（HLC）排序、不是純 wall-clock，但 HLC 仍綁節點實體時鐘——時鐘不同步（clock skew）會直接影響哪一筆被判定為「較晚」。同步的是 key 的「值」不是「操作」，這也是為什麼並發 INCR 會互相覆蓋而非累加（見故障演練 Case 1）。&lt;/p>
&lt;p>&lt;strong>每筆寫入帶來源標記避免無限迴圈&lt;/strong>。A 的寫入同步給 B 後，B 不會再把它當成新寫入傳回 A（否則會無限循環）。KeyDB 用來源標記處理這個，但複製拓樸設計錯（例如環狀多節點）仍可能放大流量。&lt;/p>
&lt;h2 id="配置兩節點-active-active-的設定路徑">配置：兩節點 active-active 的設定路徑&lt;/h2>
&lt;p>實機驗證的最小雙主設定（兩個節點互相複製）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 節點 A 與 B 都開 active-replica + multi-master&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">docker run -d --name kdb-a --network kdbnet -p 6401:6379 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> eqalpha/keydb keydb-server --active-replica yes --multi-master yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">docker run -d --name kdb-b --network kdbnet -p 6402:6379 &lt;span class="se">\
&lt;/span>&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="se">&lt;/span> eqalpha/keydb keydb-server --active-replica yes --multi-master yes
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl">&lt;span class="c1"># 互相指向對方（形成雙向複製）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> replicaof kdb-b &lt;span class="m">6379&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> replicaof kdb-a &lt;span class="m">6379&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>實機驗證雙向同步（最後檢查日 2026-06-16）：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">&lt;span class="c1"># 寫 A、讀 B&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> SET fromA hello &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> GET fromA &lt;span class="c1"># → hello （A 的寫入同步到 B）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">&lt;span class="c1"># 寫 B、讀 A（雙向）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6402&lt;/span> SET fromB world &lt;span class="c1"># → OK&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> GET fromB &lt;span class="c1"># → world （B 的寫入同步到 A）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">&lt;span class="c1"># 確認 role 與複製鏈路&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">keydb-cli -p &lt;span class="m">6401&lt;/span> INFO replication &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;role|master_link_status|connected_slaves&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">&lt;span class="c1"># role:active-replica&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">12&lt;/span>&lt;span class="cl">&lt;span class="c1"># master_link_status:up&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">13&lt;/span>&lt;span class="cl">&lt;span class="c1"># connected_slaves:1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>兩個節點都回報 &lt;code>role:active-replica&lt;/code>（不是傳統的 master / slave），&lt;code>master_link_status:up&lt;/code> 確認複製鏈路健康。寫入任一節點、另一節點都讀得到，這就是 active-active 的核心行為。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> overview 的 implementation-layer deep article。選型層（KeyDB vs Redis / DragonflyDB / Valkey、為何選 fork）見 overview；本文只處理「決定用 KeyDB active-active 後，衝突與一致性怎麼判」。命令實機驗證於 eqalpha/keydb image、最後檢查日 2026-06-16；複製機制以 <a href="https://docs.keydb.dev/docs/active-rep/">KeyDB active-replication 文件</a> 為準。</p></blockquote>
<h2 id="兩邊都能寫聽起來太美好">兩邊都能寫，聽起來太美好</h2>
<p>Redis 的複製是單向的：一個 master 寫、replica 唯讀。要跨區讓兩邊都能就近寫入，Redis 本身做不到（得靠應用層分區或外部工具）。KeyDB 的 active-active 把這個限制拿掉——兩個（含以上）KeyDB 節點都是 master、都能接受寫入、互相把寫入同步給對方。對「兩個 region 都要低延遲寫入同一份 cache」的場景，這聽起來解決了所有問題。</p>
<p>問題藏在「兩邊同時寫同一個 key」的那一刻。active-active 沒有全域協調者來仲裁誰對誰錯，它用 last-write-wins（LWW）：比較兩筆寫入的時間戳，留下較晚的、默默丟掉較早的。多數時候沒事，但當兩個 region 在幾毫秒內各自更新同一個 key，其中一筆寫入會無聲消失——沒有錯誤、沒有日誌、application 以為自己寫成功了。</p>
<p>理解 KeyDB active-active 就是理解這個取捨：它用 LWW 換到了「兩邊都能寫」的可用性，代價是放棄了強一致與「不丟寫入」的保證。本文展開複製機制、衝突語意，以及哪些資料放得進這個模型、哪些放進去就是 bug。</p>
<h2 id="核心概念active-active-的複製與衝突語意">核心概念：active-active 的複製與衝突語意</h2>
<p>active-active 不是「分散式交易」，它是「雙向非同步複製 + LWW 衝突解決」。理解它要抓三個點：</p>
<p><strong>每個節點都是 active-replica</strong>。一般 Redis replica 是唯讀的；KeyDB 的 active-replica 既接受本地寫入、又接收對方的複製流。兩個節點互相設定對方為 master，形成雙向複製環。實機看到的 role 就是 <code>active-replica</code>（不是 master / slave）。</p>
<p><strong>複製是非同步的</strong>。本地寫入立即回 OK 給 client，之後才非同步傳給對方節點。這意味著兩個節點之間永遠有一個複製延遲窗口——在這個窗口內，兩邊看到的資料可能不同。這是 active-active 是 AP（可用性 + 分區容忍）而非 CP 的根本原因。</p>
<p><strong>衝突用 last-write-wins 解決</strong>。同一個 key 在兩個節點被並發修改時，KeyDB 比較版本，保留較晚的寫入、丟棄較早的。沒有 merge、沒有 vector clock、沒有 application callback——就是比誰較晚。KeyDB 用 hybrid logical clock（HLC）排序、不是純 wall-clock，但 HLC 仍綁節點實體時鐘——時鐘不同步（clock skew）會直接影響哪一筆被判定為「較晚」。同步的是 key 的「值」不是「操作」，這也是為什麼並發 INCR 會互相覆蓋而非累加（見故障演練 Case 1）。</p>
<p><strong>每筆寫入帶來源標記避免無限迴圈</strong>。A 的寫入同步給 B 後，B 不會再把它當成新寫入傳回 A（否則會無限循環）。KeyDB 用來源標記處理這個，但複製拓樸設計錯（例如環狀多節點）仍可能放大流量。</p>
<h2 id="配置兩節點-active-active-的設定路徑">配置：兩節點 active-active 的設定路徑</h2>
<p>實機驗證的最小雙主設定（兩個節點互相複製）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 節點 A 與 B 都開 active-replica + multi-master</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name kdb-a --network kdbnet -p 6401:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --active-replica yes --multi-master yes
</span></span><span class="line"><span class="ln">4</span><span class="cl">docker run -d --name kdb-b --network kdbnet -p 6402:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  eqalpha/keydb keydb-server --active-replica yes --multi-master yes
</span></span><span class="line"><span class="ln">6</span><span class="cl">
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># 互相指向對方（形成雙向複製）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">keydb-cli -p <span class="m">6401</span> replicaof kdb-b <span class="m">6379</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl">keydb-cli -p <span class="m">6402</span> replicaof kdb-a <span class="m">6379</span></span></span></code></pre></div><p>實機驗證雙向同步（最後檢查日 2026-06-16）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 寫 A、讀 B</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">keydb-cli -p <span class="m">6401</span> SET fromA hello   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">keydb-cli -p <span class="m">6402</span> GET fromA         <span class="c1"># → hello   （A 的寫入同步到 B）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 寫 B、讀 A（雙向）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">keydb-cli -p <span class="m">6402</span> SET fromB world   <span class="c1"># → OK</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">keydb-cli -p <span class="m">6401</span> GET fromB         <span class="c1"># → world   （B 的寫入同步到 A）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 確認 role 與複製鏈路</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">keydb-cli -p <span class="m">6401</span> INFO replication <span class="p">|</span> grep -E <span class="s2">&#34;role|master_link_status|connected_slaves&#34;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># role:active-replica</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># master_link_status:up</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># connected_slaves:1</span></span></span></code></pre></div><p>兩個節點都回報 <code>role:active-replica</code>（不是傳統的 master / slave），<code>master_link_status:up</code> 確認複製鏈路健康。寫入任一節點、另一節點都讀得到，這就是 active-active 的核心行為。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1並發寫同一-key一筆寫入無聲消失">Case 1：並發寫同一 key、一筆寫入無聲消失</h3>
<p><strong>徵兆</strong>：兩個 region 的 application 各自更新同一個 user 的 cache（例如 profile），事後發現其中一個 region 的更新「沒生效」——但寫入時 application 收到的是 OK，沒有任何錯誤。</p>
<p><strong>根因</strong>：active-active 的 LWW。兩筆寫入在複製延遲窗口內並發發生，KeyDB 比較時間戳保留較晚的、默默丟棄較早的。application 兩邊都以為自己寫成功了（本地確實 OK），但同步後只有一筆存活。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>不要讓同一個 key 被多個 region 並發寫——按 key 分區（user X 的寫入永遠路由到 region A），把多主退化成「就近讀 + 單點寫」</li>
<li>真的需要多點寫的計數器類資料，用 CRDT 語意的結構（KeyDB 的 LWW 不適合 counter，並發 INCR 會互相覆蓋而非累加）</li>
<li>接受 LWW 是 cache 的取捨——可重建的 cache 副本丟一筆寫入可回源重算，不可重建的資料不該放 active-active</li>
<li>衝突無聲是最危險的——加應用層的寫入審計（不靠 KeyDB 告警）</li>
</ol>
<h3 id="case-2clock-skew-讓較晚的判定錯亂">Case 2：clock skew 讓「較晚」的判定錯亂</h3>
<p><strong>徵兆</strong>：明明 region B 後寫的值，最後存活的卻是 region A 先寫的值——LWW 的「後寫者勝」失效。</p>
<p><strong>根因</strong>：LWW 比較時間戳，但兩個節點的系統時鐘若沒同步（clock skew），「較晚」的判定就錯了。B 的時鐘慢了 200ms，B 後寫的值帶的時間戳反而比 A 早，被判定為「較舊」丟棄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>所有 KeyDB 節點強制 NTP 時鐘同步，把 skew 壓到毫秒級</li>
<li>監控節點間的時鐘偏差，skew 超過複製延遲就有 LWW 判定錯亂風險</li>
<li>對時間敏感的衝突，LWW 本質不可靠——時鐘永遠無法完美同步，這是 LWW 模型的固有弱點</li>
<li>需要正確衝突解決的場景，不要用 LWW 的 active-active，改強一致儲存</li>
</ol>
<h3 id="case-3複製延遲下的-stale-read">Case 3：複製延遲下的 stale read</h3>
<p><strong>徵兆</strong>：region A 寫入後，立刻有請求打到 region B 讀同一 key，讀到舊值；幾百毫秒後再讀才是新值。</p>
<p><strong>根因</strong>：active-active 是非同步複製，A 的寫入要經過網路傳到 B 才可見。在這個複製延遲窗口內，B 讀到的是 stale 值。跨 region 的延遲窗口比同 AZ 大得多。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>寫後需要立即一致讀的路徑，讀同一個寫入的節點（read-your-writes 綁定到寫入 region）</li>
<li>監控節點間複製延遲，跨 region 的延遲是 stale window 的下界</li>
<li>接受最終一致——這是 active-active 的本質，cache 場景多數可容忍短暫 stale</li>
<li>不可容忍 stale 的資料不適合 active-active，走單寫入點 + 跨區唯讀 replica</li>
</ol>
<h3 id="case-4複製拓樸設計錯流量放大或迴圈">Case 4：複製拓樸設計錯、流量放大或迴圈</h3>
<p><strong>徵兆</strong>：加了第三個 active 節點組成環狀後，節點間流量異常放大、CPU 升高，甚至同一筆寫入被反覆傳遞。</p>
<p><strong>根因</strong>：active-active 多節點（&gt; 2）的拓樸需要小心設計。全互連（full mesh）下每筆寫入要傳給所有其他節點、流量隨節點數平方成長；環狀拓樸若來源標記處理不當可能放大傳遞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>多節點 active-active 優先用 full mesh 但控制節點數（active-active 不適合大量節點）</li>
<li>監控節點間複製流量，異常放大代表拓樸或來源標記問題</li>
<li>大規模多區優先考慮「每區單寫入點 + 跨區唯讀」而非全 active-active</li>
<li>active-active 的甜蜜點是 2-3 個區的雙向就近寫，不是大規模 mesh</li>
</ol>
<h3 id="case-5節點重連後的全量重同步衝擊">Case 5：節點重連後的全量重同步衝擊</h3>
<p><strong>徵兆</strong>：一個節點短暫斷線後重連，重連瞬間 CPU / 網路尖峰，期間延遲升高。</p>
<p><strong>根因</strong>：節點斷線時間過長、超過複製 backlog 能覆蓋的範圍，重連時要做全量重同步（full resync）——對方節點要產生快照（fork、見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence 的 fork 成本</a>，KeyDB 繼承 Redis 的 fork 機制）並傳輸整個 dataset。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設足夠大的 <code>repl-backlog-size</code>，讓短暫斷線走部分同步（partial resync）而非全量</li>
<li>重同步的 fork 成本跟記憶體 headroom 相關，節點要留 fork 空間</li>
<li>監控 <code>master_link_status</code>，頻繁 down / up 代表網路不穩、要先修網路</li>
<li>跨 region 的 active-active 對網路穩定性敏感，不穩的鏈路會頻繁觸發重同步</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>active-active 的容量判讀，核心在衝突率與複製健康：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>同 key 跨節點並發寫入率</td>
          <td>接近 0（key 按區分區）</td>
          <td>高 → LWW 丟寫入風險、改 key 分區</td>
      </tr>
      <tr>
          <td>節點間 clock skew</td>
          <td>&lt; 複製延遲（毫秒級）</td>
          <td>大 → LWW 判定錯亂、強制 NTP</td>
      </tr>
      <tr>
          <td>節點間複製延遲</td>
          <td>跨 region 可接受的 stale 窗</td>
          <td>過大 → stale read 嚴重、檢查網路</td>
      </tr>
      <tr>
          <td><code>master_link_status</code></td>
          <td><code>up</code></td>
          <td>頻繁 down → 網路不穩、會觸發重同步</td>
      </tr>
      <tr>
          <td>active 節點數</td>
          <td>2-3（雙向就近寫）</td>
          <td>過多 → mesh 流量平方成長、改單寫入點拓樸</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要正確的衝突解決 / 不能丟寫入</strong>：LWW 不保證，走強一致儲存（<a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a> 的 multi-region 一致性方案）或單寫入點架構。</li>
<li><strong>需要 counter / 累加語意的多點寫</strong>：LWW 會讓並發 INCR 互相覆蓋，KeyDB active-active 不適合，改 CRDT 或單點 counter。</li>
<li><strong>跨 region 但可接受單寫入點</strong>：用 Redis / Valkey 的單向複製（一區寫、其他區唯讀），比 active-active 簡單且無衝突。</li>
<li><strong>大規模多區</strong>：active-active 的甜蜜點是 2-3 區，更大規模走 managed 的跨區方案（<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache Global Datastore</a> 的 active-passive）。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>active-active 是 KeyDB 區別於 Redis 的核心能力之一，但它的取捨跨多個子系統：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB overview</a></strong>：overview 點到 active-active 是 last-write-wins、本文展開它什麼時候默默丟資料。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis persistence / fork latency</a></strong>：KeyDB 繼承 Redis 的 fork 機制，節點重連的全量重同步付 fork 成本。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></strong>：active-active 的 stale window 與 LWW 丟寫入，本質是「cache 副本的新鮮度與一致性邊界」議題的多主版本。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap KeyDB cross-cloud case</a></strong>：Snap 用 KeyDB 的主因是 cross-cloud latency 治理（cache 與 application 共置），active-active 的雙向就近寫是這類 multi-cloud 場景的工具，但要按 key 分區避開 LWW 衝突。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/shared-nothing-multicore-architecture/" data-link-title="DragonflyDB shared-nothing 多核架構：用 scale-up 取代 Redis Cluster" data-link-desc="Redis 要靠 Cluster 分片才能用滿一台多核機器，DragonflyDB 賭的是相反方向——單一進程 thread-per-core、shared-nothing、把單機推到 Redis 要好幾個 shard 才達到的規模。本文展開 thread-per-core 與 dashtable 的架構、fork-less snapshot、5 個把架構假設寫成 production 事故的踩坑，以及 scale-up 撞牆該回 Cluster 的邊界">DragonflyDB 多核架構</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover</a>（單向複製的 HA）</li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Memcached slab allocator 與記憶體經濟學：明明有記憶體卻在 evict</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/slab-allocator-memory-economics/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 &lt;code>memcached:1.6&lt;/code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 &lt;a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict&lt;/h2>
&lt;p>Memcached 最違反直覺的故障是這樣：監控顯示 &lt;code>evictions&lt;/code> 持續上升、hit rate 在掉，但 &lt;code>stats&lt;/code> 算下來實際用掉的記憶體遠低於 &lt;code>-m&lt;/code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。&lt;/p>
&lt;p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。&lt;/p>
&lt;p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。&lt;/p>
&lt;h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型&lt;/h2>
&lt;p>Memcached 啟動時不會把 &lt;code>-m&lt;/code> 指定的記憶體一次配掉，而是按需求以 &lt;strong>page&lt;/strong>（預設 1MB）為單位分配給 &lt;strong>slab class&lt;/strong>，每個 class 存放某個大小區間的 item。&lt;/p>
&lt;p>&lt;strong>slab class 與 chunk size&lt;/strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 &lt;code>growth_factor&lt;/code> 等比成長——實機看預設值：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;stats settings\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep growth_factor
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT growth_factor 1.25&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="nb">printf&lt;/span> &lt;span class="s1">&amp;#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&amp;#39;&lt;/span> &lt;span class="p">|&lt;/span> nc localhost &lt;span class="m">11211&lt;/span> &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;chunk_size|active_slabs&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT 1:chunk_size 96 ← 最小的 slab class、chunk 96 bytes&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl">&lt;span class="c1"># STAT active_slabs 1&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>growth_factor 1.25&lt;/code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。&lt;/p>
&lt;p>&lt;strong>page 分配是單向的&lt;/strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。&lt;code>-o slab_automove&lt;/code> 與手動 &lt;code>slabs reassign&lt;/code> 可以把 page 在 class 間搬移，但預設行為偏保守。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a> overview 的 implementation-layer deep article。選型層（純 KV vs Redis data types、何時選 Memcached）見 overview；本文只處理「決定用 Memcached 後，slab 記憶體怎麼配才不會莫名淘汰」。命令實機驗證於 <code>memcached:1.6</code>（VERSION 1.6.42）、最後檢查日 2026-06-16；機制以 <a href="https://github.com/memcached/memcached/wiki/UserInternals">Memcached 官方 wiki</a> 為準。</p></blockquote>
<h2 id="明明有記憶體卻在-evict">明明有記憶體、卻在 evict</h2>
<p>Memcached 最違反直覺的故障是這樣：監控顯示 <code>evictions</code> 持續上升、hit rate 在掉，但 <code>stats</code> 算下來實際用掉的記憶體遠低於 <code>-m</code> 設的上限——機器明明還有空間，Memcached 卻在淘汰資料。換成 Redis 思維的人會卡住，因為 Redis 是一個共用的記憶體池，不會出現「有空間卻淘汰」。</p>
<p>這個現象叫 slab calcification，根因在 Memcached 的記憶體模型：它把記憶體預先切成許多固定大小的格子（slab class），每個 class 各自管自己那塊，跟 Redis 共用一個記憶體池的模型相反。記憶體一旦分配給某個 class，預設不會還回去給別的 class 用。如果你的 value 大小分布隨時間改變（早期都是小 value、後來都是大 value），早期被小 value 佔走的記憶體還鎖在小 class 裡，大 value 的 class 沒有足夠空間、開始淘汰——即使整體還有大量「屬於別人」的空閒記憶體。</p>
<p>理解 Memcached 就是理解這套 slab 經濟學。它用「放棄記憶體的靈活性」換到了「永不碎片化、O(1) 分配、可預測的多執行緒擴展」。這個取捨在純 cache 場景非常划算，但它的失敗模式跟 Redis 完全不同，要用 slab 的語言來判讀。</p>
<h2 id="核心概念slab-allocator-的會計模型">核心概念：slab allocator 的會計模型</h2>
<p>Memcached 啟動時不會把 <code>-m</code> 指定的記憶體一次配掉，而是按需求以 <strong>page</strong>（預設 1MB）為單位分配給 <strong>slab class</strong>，每個 class 存放某個大小區間的 item。</p>
<p><strong>slab class 與 chunk size</strong>。每個 slab class 對應一個固定的 chunk size，item 被放進「裝得下它的最小 class」。class 的 chunk size 按 <code>growth_factor</code> 等比成長——實機看預設值：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;stats settings\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep growth_factor
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># STAT growth_factor 1.25</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">printf</span> <span class="s1">&#39;set k1 0 0 5\r\nhello\r\nstats slabs\r\nquit\r\n&#39;</span> <span class="p">|</span> nc localhost <span class="m">11211</span> <span class="p">|</span> grep -E <span class="s2">&#34;chunk_size|active_slabs&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># STAT 1:chunk_size 96      ← 最小的 slab class、chunk 96 bytes</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># STAT active_slabs 1</span></span></span></code></pre></div><p><code>growth_factor 1.25</code> 表示每個 class 的 chunk size 是前一個的 1.25 倍：class 1 是 96 bytes、class 2 約 120、class 3 約 152……一路到 item 大小上限。一個 100 bytes 的 value 放不進 96 bytes 的 class 1，被放進 120 bytes 的 class 2——浪費 20 bytes。這個「向上取整到 chunk size」的浪費是 slab 模型的固有成本。</p>
<p><strong>page 分配是單向的</strong>。當某個 class 需要空間，Memcached 給它一個 1MB 的 page，切成該 class 的 chunk。這個 page 預設永久屬於這個 class——這就是 calcification 的來源。<code>-o slab_automove</code> 與手動 <code>slabs reassign</code> 可以把 page 在 class 間搬移，但預設行為偏保守。</p>
<p><strong>LRU 是 per-slab-class 的</strong>。淘汰不是全域的，是每個 slab class 維護自己的 LRU。所以「class 2 滿了開始淘汰、但 class 5 有空閒 page」是正常現象——淘汰看的是該 class 自己的空間，不是全域記憶體。</p>
<p>這三點合起來解釋了開頭的悖論：evict 發生在某個 class 內，跟全域剩餘記憶體無關。</p>
<h2 id="配置slab-與多執行緒的設定路徑">配置：slab 與多執行緒的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 啟動參數（Memcached 的調校多在啟動參數、不像 Redis 有大量 runtime CONFIG SET）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">docker run -d --name memcached -p 11211:11211 memcached:1.6 <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  memcached <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>    -m <span class="m">1024</span> <span class="se">\ </span>         <span class="c1"># 記憶體上限 1024 MB</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    -t <span class="m">4</span> <span class="se">\ </span>            <span class="c1"># worker thread 數（多執行緒、對齊 CPU 核數）</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    -f 1.25 <span class="se">\ </span>         <span class="c1"># slab growth factor（預設 1.25、調小→class 更密集→浪費更少但 class 更多）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    -I 2m <span class="se">\ </span>           <span class="c1"># 單一 item 大小上限（預設 1MB、超過要調大或拆 value）</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    -o <span class="nv">slab_automove</span><span class="o">=</span><span class="m">1</span> <span class="c1"># 自動把空閒 page 從一個 class 搬到吃緊的 class（緩解 calcification）</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>-m</code> 是給 item 資料的上限，Memcached 自身的 hash table、連線 buffer 等 overhead 在 <code>-m</code> 之外，機器要留 headroom</li>
<li><code>-t</code> 對齊 CPU 核數——Memcached 從早期就是 multi-threaded，這是它跟早期單執行緒 Redis 的核心差異</li>
<li><code>-f</code> 調小（例如 1.08）讓 slab class 更密集、向上取整浪費更少，代價是 class 數變多、管理開銷略增</li>
<li><code>-I</code> 是單 item 上限，超過會 store 失敗（見故障演練 Case 3）</li>
<li><code>slab_automove=1</code> 是緩解 calcification 的關鍵，預設視版本而定，明確開啟較穩</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1slab-calcificationvalue-大小漂移造成假性記憶體不足">Case 1：slab calcification——value 大小漂移造成假性記憶體不足</h3>
<p><strong>徵兆</strong>：<code>evictions</code> 上升、hit rate 下降，但 <code>stats</code> 顯示 <code>bytes</code> 遠低於 <code>limit_maxbytes</code>。<code>stats slabs</code> 看到某個 class 的 page 用滿在淘汰，另一個 class 有大量空閒 chunk。</p>
<p><strong>根因</strong>：value 大小分布隨時間漂移。早期 value 小、記憶體被分配給小 slab class；後來 value 變大、需要大 class，但 page 已被小 class 鎖住不還，大 class 空間不足開始淘汰。整體記憶體沒滿，但「對的 class」沒空間。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>開 <code>-o slab_automove=1</code>，讓 Memcached 自動把空閒 page 從冷 class 搬到吃緊的 class</li>
<li>手動觸發搬移：<code>slabs reassign &lt;src_class&gt; &lt;dst_class&gt;</code>（緊急救火用）</li>
<li>監控 <code>stats slabs</code> 各 class 的 <code>used_chunks</code> vs <code>total_chunks</code> 與 <code>stats items</code> 的 per-class evicted，找出失衡的 class</li>
<li>從源頭穩定 value 大小分布——序列化格式統一、避免同類資料時大時小</li>
</ol>
<h3 id="case-2chunk-向上取整浪費大量記憶體">Case 2：chunk 向上取整浪費大量記憶體</h3>
<p><strong>徵兆</strong>：存的 value 總大小算起來只有 600MB，但 Memcached 報用掉接近 1GB，記憶體效率異常低。</p>
<p><strong>根因</strong>：value 大小剛好落在 slab class chunk size 的「上緣之外」，被向上取整到下一個更大的 class，每個 item 浪費接近一個 growth step 的空間。例如大量 130 bytes 的 value 被放進 152 bytes 的 class，每個浪費 22 bytes，量大就顯著。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-f</code> 調小（1.25 → 1.08）讓 class 粒度更細，向上取整的浪費變小</li>
<li><code>stats slabs</code> 看主要 class 的 <code>chunk_size</code> 跟你的 value 實際大小差多少，量化浪費</li>
<li>value 設計上靠近 chunk 邊界（例如壓縮或裁剪 metadata 讓 value 剛好塞進較小的 class）</li>
<li>浪費是 slab 模型的固有成本，純 KV 的 trade-off——換到的是永不碎片化與 O(1) 分配</li>
</ol>
<h3 id="case-3value-超過-item-大小上限store-直接失敗">Case 3：value 超過 item 大小上限、store 直接失敗</h3>
<p><strong>徵兆</strong>：某些大 value 的寫入回 <code>SERVER_ERROR object too large for cache</code>，application 端 cache 寫入靜默失敗、之後一直 miss。</p>
<p><strong>根因</strong>：單一 item 超過 <code>-I</code> 設的上限（預設 1MB）。Memcached 設計上不適合存大 object，預設 1MB 是刻意的純 cache 邊界。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 value 大小分布，大 value 是否真該進 Memcached（純 KV cache 不適合大 blob）</li>
<li>必要時調大 <code>-I</code>（例如 <code>-I 2m</code>），但這會改變 slab class 結構、增加大 chunk 的記憶體佔用</li>
<li>大 object 考慮壓縮、或拆成多個小 key、或改放適合的儲存（物件儲存 / <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> 的 hash）</li>
<li>application 端要處理 store 失敗，不要假設 set 一定成功——失敗就走 origin</li>
</ol>
<h3 id="case-4thread-數設太高lock-contention-反而拖慢">Case 4：thread 數設太高、lock contention 反而拖慢</h3>
<p><strong>徵兆</strong>：把 <code>-t</code> 從 4 調到 32 想榨多核效能，throughput 沒升反降，CPU 在 system time 飆高。</p>
<p><strong>根因</strong>：Memcached 的多執行緒有 per-item lock（hash bucket lock），thread 數遠超核數時，執行緒互搶 lock 與 CPU、context switch 開銷超過平行收益。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>-t</code> 對齊實體核數，不要超配（多數場景 4-8 已足夠，極高核機器再往上調並壓測）</li>
<li>用實際 workload 壓測對比不同 <code>-t</code> 的 throughput，找拐點</li>
<li>hot key 集中時 lock contention 更明顯（同 bucket），這是資料分布問題不是 thread 數問題</li>
<li>跨機器水平擴展（client-side consistent hashing）比單機堆 thread 更能解規模，見本文整合段</li>
</ol>
<h3 id="case-5連線數打到上限新連線被拒">Case 5：連線數打到上限、新連線被拒</h3>
<p><strong>徵兆</strong>：高並發下新連線報錯或 hang，<code>stats</code> 的 <code>curr_connections</code> 接近 <code>max_connections</code>，<code>listen_disabled_num</code> 在增加。</p>
<p><strong>根因</strong>：每個 client 連線佔一個 connection slot，Memcached 預設 <code>-c 1024</code>。大量 client（尤其沒用連線池、每請求建連）會打滿 connection 上限。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 端用連線池重用連線，不要每請求建連</li>
<li>調高 <code>-c</code>（例如 <code>-c 4096</code>），但連線本身有記憶體 overhead（在 <code>-m</code> 之外），要算進機器容量</li>
<li>監控 <code>curr_connections</code> 與 <code>listen_disabled_num</code>，後者非零代表曾達上限拒絕連線</li>
<li>連線數爆炸常是 client fan-out 問題，跨多 Memcached node 分散（consistent hashing）能攤平單 node 連線壓力</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Memcached 的容量判讀，核心在 slab 效率與多執行緒擴展：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>evictions</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高但記憶體沒滿 → calcification、開 slab_automove</td>
      </tr>
      <tr>
          <td>各 class <code>used / total chunks</code></td>
          <td>各 class 均衡</td>
          <td>單 class 滿、其他空 → calcification</td>
      </tr>
      <tr>
          <td>chunk 向上取整浪費</td>
          <td>小（value 貼近 chunk size）</td>
          <td>大 → 調小 <code>-f</code> 或調整 value 大小</td>
      </tr>
      <tr>
          <td><code>curr_connections / -c</code></td>
          <td>&lt; 80%</td>
          <td>接近上限 → 用連線池或調高 <code>-c</code></td>
      </tr>
      <tr>
          <td>多執行緒 CPU</td>
          <td>核數內、system time 低</td>
          <td>system time 高 → <code>-t</code> 超配、lock contention</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>需要 data types / 持久化 / distributed lock</strong>：Memcached 是純 KV、刻意不做這些。需要這些走 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis / Valkey</a>，這是 capability 差異不是調校能補。</li>
<li><strong>單機容量 / throughput 不夠</strong>：Memcached 沒有 server-side cluster，靠 client-side consistent hashing（ketama）水平擴展到多 node，見整合。</li>
<li><strong>想要 Memcached 的多執行緒 + Redis 的 data types</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 兼具多核與 Redis 相容，是兩者的中間點。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Memcached 的單機很簡單，它的工程深度在「如何把多個 Memcached node 組成一個 cache 層」——而這發生在 client 端與代理層，不在 server：</p>
<ul>
<li><strong>client-side consistent hashing（ketama）</strong>：Memcached server 之間互不知道彼此，sharding 由 client library 用 consistent hashing 決定 key 去哪個 node，加減 node 時最小化 key 重新分布。這是 Memcached 水平擴展的基礎。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">Meta mcrouter</a></strong>：Meta 的 mcrouter 是 Memcached 專屬的 protocol-aware routing proxy，把跨叢集 / 跨區的流量收斂、失效隔離、pool 管理從 client 端移到代理層——這是 Memcached 大規模治理的標準答案。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">Netflix EVCache</a></strong>：EVCache 基於 Memcached，Netflix 在上面加跨 AZ replication 與 client-side smart routing，補足 Memcached 沒有的跨區 HA。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">Meta TAO</a></strong>：TAO 底層用 Memcached 作為 social graph 的 cache 層，上層加一致性與關聯查詢——展示了純 KV 之上如何疊加語意。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">Meta CacheLib + Kangaroo</a></strong>：當 DRAM 的記憶體經濟撞到極限，Meta 用 CacheLib 把 cache 分層到 flash——這是 slab 記憶體經濟學的下一個邊界。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>對照 vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a>（jemalloc 池 vs slab class 的差異）、<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type B drop-in&lt;/strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="同一份程式碼不同授權">同一份程式碼、不同授權&lt;/h2>
&lt;p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。&lt;/p>
&lt;p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是&lt;strong>授權&lt;/strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是&lt;strong>fork 後的分歧&lt;/strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。&lt;/p>
&lt;p>&lt;code>INFO server&lt;/code> 上看得到這個「同源但分歧」的事實：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.2.4 ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自己的演進線&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>redis_version:7.2.4&lt;/code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；&lt;code>valkey_version&lt;/code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B&lt;/h2>
&lt;p>跑 diff dimension audit，Redis → Valkey 全維度 Low：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>同 Redis 7.2.4（fork 同源）、RESP 協定一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>同 redis.conf、同監控指標、同 CLI 命令&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同一份 code base 演進）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1（單服務換單服務）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>零（所有 Redis client library 直接相容）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB / AOF 檔案相容、可直接拷資料目錄&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>全 Low → &lt;strong>Type B drop-in&lt;/strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &amp;#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB&lt;/a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type B drop-in</strong>（全維度 Low），結構走 6-section + 相容性 audit 前置。實機驗證於 valkey/valkey:8（valkey_version 8.1.8、redis_version 7.2.4）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="同一份程式碼不同授權">同一份程式碼、不同授權</h2>
<p>多數 migration 的工作量在「source 跟 target 不一樣」——schema 要翻譯、API 要改、資料要轉。Redis → Valkey 幾乎沒有這個問題：Valkey 是 2024 年從 Redis 7.2.4 直接 fork 出來的，那一刻它跟 Redis 是 bit-for-bit 同一份程式碼。RDB 與 AOF 檔案格式相同（可以直接把 Redis 的資料目錄拷給 Valkey 載入）、RESP 協定相同、所有 Redis client library 不改一行就能連。技術上，這是 cache 領域最容易的遷移。</p>
<p>那為什麼要寫一篇 playbook？因為這個遷移的工作量不在資料層，在兩個別的地方。第一是<strong>授權</strong>——Redis 2024 改成 RSALv2 / SSPL（非 OSI 認可），Valkey 是 BSD 3-clause（OSI、Linux Foundation 治理），這個遷移的整個 driver 是授權合規，而合規驗證有它自己的流程。第二是<strong>fork 後的分歧</strong>——fork 那一刻兩者相同，但之後各自演進：Redis 加了 7.4+ 的新功能、Valkey 加了自己的（如 8.x 多執行緒），用到 fork 之後 Redis 新功能的部署會有相容缺口。</p>
<p><code>INFO server</code> 上看得到這個「同源但分歧」的事實：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← fork 點、client 以此判斷相容性（裝成 Redis 7.2.4）</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自己的演進線</span></span></span></code></pre></div><p><code>redis_version:7.2.4</code> 是相容性的保證（client 看到就以 Redis 7.2.4 行為運作）；<code>valkey_version</code> 是分歧的證據。這篇 playbook 處理的就是「資料層幾乎零工作、工作在授權與分歧盤點」的 drop-in 遷移。</p>
<h2 id="6-維-diff-dimension-audit為什麼是-type-b">6 維 diff dimension audit：為什麼是 Type B</h2>
<p>跑 diff dimension audit，Redis → Valkey 全維度 Low：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis 7.2.4（fork 同源）、RESP 協定一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 redis.conf、同監控指標、同 CLI 命令</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同一份 code base 演進）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1（單服務換單服務）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>零（所有 Redis client library 直接相容）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB / AOF 檔案相容、可直接拷資料目錄</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>全 Low → <strong>Type B drop-in</strong>（6-section + 相容性 audit 前置、週期 1-4 週）。跟同模組的 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對照：DragonflyDB 是 C++ 重寫（drop-in 但 Lua / encoding / module 有差異），Valkey 是 fork（同源、連 RDB 檔都相容）——Valkey 的相容度比 DragonflyDB 更高，是 Type B 裡最純粹的一端。</p>
<p>這個遷移的特殊之處是 driver 在資料層之外：它是<strong>授權 / 合規驅動</strong>。依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的漏類處理，政策 / 合規驅動的遷移資料層仍走 Type B，但 audit 重點多一塊<strong>授權驗證與證據收集</strong>。</p>
<h2 id="相容性-auditcutover-前要確認的清單">相容性 audit：cutover 前要確認的清單</h2>
<p>Valkey 號稱 100% 相容 Redis 7.2.4，但「100%」的邊界在 fork 之後的分歧。Pre-migration 必跑的 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>Valkey 相容程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Core data types / commands / RESP</td>
          <td>完全相容（fork 自 7.2.4）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB / AOF 檔案格式</td>
          <td>完全相容（可直接拷資料目錄）</td>
          <td>無需轉檔</td>
      </tr>
      <tr>
          <td>Eviction / persistence / pub-sub</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Client libraries</td>
          <td>完全相容（透過 redis_version 協商）</td>
          <td>無需改 code</td>
      </tr>
      <tr>
          <td>Cluster / Sentinel</td>
          <td>完全相容（同 Redis 模型）</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>Redis 7.4+ 新功能（fork 後新增）</td>
          <td>Valkey 不一定跟進</td>
          <td>盤點是否用到、確認 Valkey 對應</td>
      </tr>
      <tr>
          <td>Redis Stack 商業 module（JSON/Search）</td>
          <td>不相容（Valkey 有 valkey-search / valkey-bloom）</td>
          <td>盤點 module 使用、確認替代或改寫</td>
      </tr>
      <tr>
          <td>RedisInsight 等 Redis Inc 監控工具</td>
          <td>部分 vendor-specific 命令缺</td>
          <td>改通用工具（valkey-cli / redis_exporter）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：兩份清單——(1) 用到的 Redis 7.4+ 功能（fork 後新增、Valkey 可能沒有）、(2) 載入的 Redis Stack module。這兩塊是僅有的相容風險，其餘資料層零工作。盤點方法：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 盤點載入的 module（最大相容風險）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli MODULE LIST
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 盤點是否用到 7.4+ 功能（抓 production traffic 對照 Redis 7.4 changelog）</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">redis-cli MONITOR    <span class="c1"># 限時抓樣、grep 可疑的新命令</span></span></span></code></pre></div><h2 id="step-by-step-cutover">Step-by-step cutover</h2>
<p>因為 RDB 檔案相容，cutover 比 DragonflyDB 更簡單（無版本轉換風險）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 部署 Valkey（同 Redis 配置、可直接沿用 redis.conf）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name valkey -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/valkey:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  valkey/valkey:8 valkey-server /etc/valkey/valkey.conf
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 2. Redis 端 BGSAVE 產生 RDB</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 3. 把 dump.rdb 拷給 Valkey（檔案格式相容、無需轉換）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb valkey-host:/data/valkey/
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 4. 重啟 Valkey 載入 RDB</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">docker restart valkey
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl"><span class="c1"># 5. 驗證資料一致 + 版本</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> DBSIZE          <span class="c1"># 對齊 Redis DBSIZE</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">valkey-cli -h valkey-host -p <span class="m">6380</span> INFO server <span class="p">|</span> grep redis_version  <span class="c1"># 7.2.4</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl"><span class="c1"># 6. 替代方案（零停機）：用 replicaof 讓 Valkey 當 Redis 的 replica、即時同步後 promote</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1">#    valkey-cli -h valkey-host REPLICAOF redis-primary 6379</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl"><span class="c1">#    重要邊界：此路徑只在 source 是 Redis 7.2 或更早版本時成立。</span>
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1">#    Redis 7.4+（Community Edition）改了複製格式、Valkey 無法當其 replica</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1">#    → source 為 7.4+ 時改走上面的 RDB 拷貝路徑（步驟 2-4）。</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl">
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 7. Cutover：client 配置切到 Valkey endpoint、Redis 留 standby</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>RDB 拷貝 + load</strong>：100GB 約 5-15 分鐘（無版本轉換、比 DragonflyDB 少一道風險）</li>
<li><strong>replicaof 路徑</strong>：要零停機可讓 Valkey 當 Redis replica 即時同步、確認 lag 趨零後 promote + 切 client（僅限 source 為 Redis 7.2 或更早；7.4+ 複製格式已分歧、不適用、改走 RDB 拷貝）</li>
<li><strong>Cutover</strong>：client 配置切換（單次完成、硬邊界）、Redis 留 standby 1-2 週</li>
<li><strong>Decom</strong>：無相容問題後關閉 Redis</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1用到-redis-74-功能valkey-沒有">Case 1：用到 Redis 7.4+ 功能、Valkey 沒有</h3>
<p><strong>徵兆</strong>：cutover 後某功能報 <code>unknown command</code> 或行為不同，命令是 Redis 在 7.4 之後（fork 點之後）才加的。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，Redis 7.4+ 新增的功能 Valkey 不一定跟進。pre-migration audit 漏掉了這些 fork 後的新功能。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 對照 Redis 7.4+ changelog 盤點用到的新功能（audit 清單第一項）</li>
<li>Valkey 有對應就確認版本、沒有就評估改寫或留在 Redis 商業版</li>
<li>多數標準 cache 用法不碰 7.4+ 新功能，這個風險集中在用了較新進階功能的部署</li>
<li>Valkey 自己的 roadmap（valkey.io）會逐步補上 Redis 新功能，可追蹤</li>
</ol>
<h3 id="case-2載入了-redis-stack-商業-module">Case 2：載入了 Redis Stack 商業 module</h3>
<p><strong>徵兆</strong>：cutover 後 <code>JSON.SET</code> / <code>FT.SEARCH</code> 報 <code>unknown command</code>，application 部分功能失效。</p>
<p><strong>根因</strong>：用了 Redis Stack 的商業 module（RedisJSON / RedisSearch），這些不在 fork 範圍。Valkey 有自己的 valkey-search / valkey-bloom，但不是同一套命令、要另外安裝。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration <code>MODULE LIST</code> 盤點所有載入的 module（audit 清單第二項）</li>
<li>確認 Valkey 對應替代（valkey-search 對 RedisSearch）、確認命令相容度</li>
<li>沒有對應的評估改 module-free 設計（JSON 操作拉回 application 層）或留在 Redis Inc 商業版</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性 deep article</a> 的三層相容邊界</li>
</ol>
<h3 id="case-3以為換-valkey-解決了記憶體--fork-問題">Case 3：以為換 Valkey 解決了記憶體 / fork 問題</h3>
<p><strong>徵兆</strong>：因為 Redis 的 OOM 或 fork 延遲尖峰而遷 Valkey，遷完發現同樣問題還在。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，繼承了完全相同的記憶體模型、eviction 演算法、AOF/RDB fork 機制。這些行為在 Valkey 上一模一樣——遷移沒有改變它們。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / fork 調校在 Valkey 上跟 Redis 完全相同，直接套用 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體調校</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></li>
<li>遷 Valkey 的理由應是授權合規 / 多執行緒吞吐 / managed 成本，不是記憶體問題</li>
<li>fork 尖峰要根治走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 fork-less，不是換 Valkey</li>
<li>遷移前釐清痛點是授權（Valkey 解）還是架構（Valkey 不解）</li>
</ol>
<h3 id="case-4授權合規驗證沒做完整合規卡關">Case 4：授權合規驗證沒做完整、合規卡關</h3>
<p><strong>徵兆</strong>：技術遷移完成、但法務 / 合規 review 要求證明「不再使用 RSALv2 / SSPL 授權的軟體」，缺少證據。</p>
<p><strong>根因</strong>：這個遷移的 driver 是授權合規，但團隊只做了技術 cutover、沒收集合規證據。Redis 的 binary / image / 相依套件若還殘留在某些環境，合規目標沒真正達成。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>盤點所有環境（dev / staging / prod / CI）的 Redis binary / image / 相依，確認全部換成 Valkey</li>
<li>收集合規證據：image SBOM、套件清單、部署 manifest 顯示 Valkey BSD 授權</li>
<li>把「不再使用非 OSI 授權 cache」寫成可驗證的 CI 檢查（掃 image / 依賴）</li>
<li>依 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的合規驅動漏類，audit 重點就是 evidence collection</li>
</ol>
<h3 id="case-5監控-dashboard-部分指標斷掉">Case 5：監控 dashboard 部分指標斷掉</h3>
<p><strong>徵兆</strong>：cutover 後 RedisInsight 或某監控 dashboard 部分面板空白、vendor-specific 命令回錯。</p>
<p><strong>根因</strong>：RedisInsight 等 Redis Inc 工具有部分偏商業版的命令，Valkey 不一定實作。核心指標通用，但進階面板可能缺。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>監控改用通用工具：valkey-cli INFO、Prometheus + redis_exporter（相容 Valkey）、Grafana</li>
<li>核心指標（used_memory / keyspace_hits / connected_clients）在 Valkey 完全相容、覆蓋不受影響</li>
<li>把監控相容性納入 cutover 前驗證、不要遷完才發現面板空白</li>
<li>RedisInsight 連 Valkey 多數仍可用、只是部分 vendor 進階面板缺</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>Valkey（self-managed）</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>授權</td>
          <td>RSALv2 / SSPL（非 OSI）</td>
          <td>BSD 3-clause（OSI、Linux Foundation）</td>
          <td>Valkey 對合規敏感場景是決定性優勢</td>
      </tr>
      <tr>
          <td>核心效能</td>
          <td>baseline</td>
          <td>同 Redis 7.2.4 + 8.x 多執行緒選項</td>
          <td>Valkey 多核 workload 可更高（依 workload）</td>
      </tr>
      <tr>
          <td>相容度</td>
          <td>原生</td>
          <td>100%（fork、檔案相容）</td>
          <td>平手（同源）</td>
      </tr>
      <tr>
          <td>記憶體 / fork</td>
          <td>baseline</td>
          <td>完全相同（同源）</td>
          <td>平手（遷移不改變這層）</td>
      </tr>
      <tr>
          <td>7.4+ 新功能</td>
          <td>有</td>
          <td>不一定跟進</td>
          <td>Redis 領先（用到才在意）</td>
      </tr>
      <tr>
          <td>Redis Stack module</td>
          <td>RedisJSON / Search / Graph</td>
          <td>valkey-search / valkey-bloom（不同套）</td>
          <td>Redis 商業 module 較全</td>
      </tr>
      <tr>
          <td>managed 選項</td>
          <td>ElastiCache for Redis（legacy）</td>
          <td>ElastiCache for Valkey（AWS default、約低 20%）</td>
          <td>Valkey 在 AWS 生態成本優勢</td>
      </tr>
      <tr>
          <td>遷移成本</td>
          <td>—</td>
          <td>極低（drop-in + 檔案相容）</td>
          <td>Valkey 是最容易的遷移目標</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：合規敏感（公部門 / 企業 OSI 政策）或想降 managed 成本 → 遷 Valkey（drop-in、風險集中在 module / 7.4+ 盤點）；重度依賴 Redis Stack 商業 module → 留 Redis Inc 商業版。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-elasticache-for-valkey-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a> 對位</h3>
<p>AWS 已把 ElastiCache default engine 設為 Valkey（約低 Redis 20%）。自管 Redis → ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位，但要同時處理 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a>（failover / cluster mode / client 重連）。</p>
<h3 id="跟-client--監控整合">跟 client / 監控整合</h3>
<p>client library 零改（透過 redis_version 協商）；監控把 exporter 指向 Valkey 即可（redis_exporter 相容）、RedisInsight 部分面板需換通用工具。</p>
<h3 id="跟-valkey-8-多執行緒對位">跟 Valkey 8 多執行緒對位</h3>
<p>遷移後可評估開 Valkey 8 的 io-threads 榨多核吞吐（Redis 7.2.4 沒有的能力），見 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey 相容性與 io-threads deep article</a>。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>反向遷移</strong>（Valkey → Redis）：僅在重度依賴 Redis 7.4+ 功能或 Stack 商業 module 時需要、同樣 drop-in</li>
<li><strong>跨雲 managed Valkey</strong>：GCP Memorystore / Azure Cache 的 Valkey 支援陸續推出、評估 vendor boundary</li>
<li><strong>授權合規 CI 化</strong>：把「不使用非 OSI 授權 cache」寫成持續檢查</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（重寫型 drop-in）、<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type B drop-in + 合規驅動漏類）</li>
</ul>
]]></content:encoded></item><item><title>Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a> overview 的 implementation-layer deep article。選型層（為何 fork、授權治理、何時選 Valkey）見 overview；本文只處理「決定用 Valkey 後，相容性怎麼驗、執行緒怎麼調」。命令實機驗證於 &lt;code>valkey/valkey:8&lt;/code> image（valkey_version 8.1.8）、最後檢查日 2026-06-16；效能數字以 &lt;a href="https://valkey.io/blog/">valkey.io 官方 benchmark&lt;/a> 為準。&lt;/p>&lt;/blockquote>
&lt;h2 id="100-相容要能驗證才敢切">「100% 相容」要能驗證才敢切&lt;/h2>
&lt;p>Valkey 從 Redis 7.2.4 fork、宣稱 100% API 相容、drop-in 替換——這對選型是好消息，對上線前的工程師卻是一個需要證據的斷言。把 production 的 Redis 換成 Valkey，最怕的不是「大部分指令能跑」，而是某個邊角行為、某個 client library 的版本協商、某個 module 沒有對應 fork，在切換後才浮現。相容性不能靠信任，要靠驗證。&lt;/p>
&lt;p>驗證的起點是一個容易被忽略的細節：Valkey 的 &lt;code>INFO server&lt;/code> 同時回報兩個版本號。&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-bash" data-lang="bash">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">docker &lt;span class="nb">exec&lt;/span> valkey valkey-cli INFO server &lt;span class="p">|&lt;/span> grep -E &lt;span class="s2">&amp;#34;redis_version|valkey_version|server_name&amp;#34;&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl">&lt;span class="c1"># redis_version:7.2.4 ← client library 以此協商相容行為&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl">&lt;span class="c1"># server_name:valkey&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">&lt;span class="c1"># valkey_version:8.1.8 ← Valkey 自身的演進線&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>這個雙版本回報就是相容性的機制本身：client library 看到 &lt;code>redis_version:7.2.4&lt;/code>，就以 Redis 7.2.4 的協定與行為運作，完全不知道背後是 Valkey；&lt;code>valkey_version&lt;/code> 才是 Valkey 自己的版本，記錄它在 fork 之後加了什麼（例如 8.x 的多執行緒）。理解這條雙線——「對外裝成 Redis 7.2.4、對內持續演進」——是判斷相容性邊界的鑰匙。&lt;/p>
&lt;p>對大規模生產驗證，&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>是現成的證據：4700 萬月活、每次滑動讀多個 cache、sub-millisecond 延遲，跑在 Amazon ElastiCache for Valkey 上——這個規模的服務跑在 Valkey 上，本身就是相容性的背書。另一個訊號是 AWS 在 2024 把 ElastiCache 的 default engine 從 Redis 改成 Valkey（AWS 宣稱成本較 Redis OSS 低約 20%、以 &lt;a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價&lt;/a> 為準、最後檢查日 2026-06-16）。這些都是外部背書，但各服務有自己的 client library、module 與邊角用法，仍需自行驗證。&lt;/p>
&lt;h2 id="核心概念相容性的三層邊界">核心概念：相容性的三層邊界&lt;/h2>
&lt;p>「100% 相容」在不同層次有不同的精確度，驗證要分三層做。&lt;/p>
&lt;p>&lt;strong>協定與核心指令層：完全相容&lt;/strong>。string / hash / list / set / sorted set / stream / hyperloglog / geo 的所有指令、TTL / eviction / persistence / pub-sub / transaction、RESP 協定——這層是 fork 自 Redis 7.2.4 的部分，行為一致。所有標準 Redis client library 透過 &lt;code>redis_version&lt;/code> 協商，直接連、不改 code。&lt;/p>
&lt;p>&lt;strong>檔案格式層：相容&lt;/strong>。RDB 與 AOF 的檔案格式跟 Redis 7.2.4 一致，可以直接把 Redis 的資料目錄拷給 Valkey 載入——這是 drop-in 遷移的基礎，不需要 dump / reload。&lt;/p>
&lt;p>&lt;strong>生態與新功能層：要逐項確認&lt;/strong>。Redis 7.4+ 在 fork 之後新增的功能（Valkey 不一定跟進）、Redis Stack 的商業 module（RedisJSON / RedisSearch，Valkey 有自己的 valkey-search / valkey-bloom 但不是同一套）、偏 Redis Inc 的監控工具（RedisInsight 部分 vendor-specific 命令）——這層是相容性的真實風險所在，驗證要集中在這裡。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a> overview 的 implementation-layer deep article。選型層（為何 fork、授權治理、何時選 Valkey）見 overview；本文只處理「決定用 Valkey 後，相容性怎麼驗、執行緒怎麼調」。命令實機驗證於 <code>valkey/valkey:8</code> image（valkey_version 8.1.8）、最後檢查日 2026-06-16；效能數字以 <a href="https://valkey.io/blog/">valkey.io 官方 benchmark</a> 為準。</p></blockquote>
<h2 id="100-相容要能驗證才敢切">「100% 相容」要能驗證才敢切</h2>
<p>Valkey 從 Redis 7.2.4 fork、宣稱 100% API 相容、drop-in 替換——這對選型是好消息，對上線前的工程師卻是一個需要證據的斷言。把 production 的 Redis 換成 Valkey，最怕的不是「大部分指令能跑」，而是某個邊角行為、某個 client library 的版本協商、某個 module 沒有對應 fork，在切換後才浮現。相容性不能靠信任，要靠驗證。</p>
<p>驗證的起點是一個容易被忽略的細節：Valkey 的 <code>INFO server</code> 同時回報兩個版本號。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">docker <span class="nb">exec</span> valkey valkey-cli INFO server <span class="p">|</span> grep -E <span class="s2">&#34;redis_version|valkey_version|server_name&#34;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># redis_version:7.2.4    ← client library 以此協商相容行為</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># server_name:valkey</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># valkey_version:8.1.8   ← Valkey 自身的演進線</span></span></span></code></pre></div><p>這個雙版本回報就是相容性的機制本身：client library 看到 <code>redis_version:7.2.4</code>，就以 Redis 7.2.4 的協定與行為運作，完全不知道背後是 Valkey；<code>valkey_version</code> 才是 Valkey 自己的版本，記錄它在 fork 之後加了什麼（例如 8.x 的多執行緒）。理解這條雙線——「對外裝成 Redis 7.2.4、對內持續演進」——是判斷相容性邊界的鑰匙。</p>
<p>對大規模生產驗證，<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>是現成的證據：4700 萬月活、每次滑動讀多個 cache、sub-millisecond 延遲，跑在 Amazon ElastiCache for Valkey 上——這個規模的服務跑在 Valkey 上，本身就是相容性的背書。另一個訊號是 AWS 在 2024 把 ElastiCache 的 default engine 從 Redis 改成 Valkey（AWS 宣稱成本較 Redis OSS 低約 20%、以 <a href="https://aws.amazon.com/elasticache/pricing/">ElastiCache 定價</a> 為準、最後檢查日 2026-06-16）。這些都是外部背書，但各服務有自己的 client library、module 與邊角用法，仍需自行驗證。</p>
<h2 id="核心概念相容性的三層邊界">核心概念：相容性的三層邊界</h2>
<p>「100% 相容」在不同層次有不同的精確度，驗證要分三層做。</p>
<p><strong>協定與核心指令層：完全相容</strong>。string / hash / list / set / sorted set / stream / hyperloglog / geo 的所有指令、TTL / eviction / persistence / pub-sub / transaction、RESP 協定——這層是 fork 自 Redis 7.2.4 的部分，行為一致。所有標準 Redis client library 透過 <code>redis_version</code> 協商，直接連、不改 code。</p>
<p><strong>檔案格式層：相容</strong>。RDB 與 AOF 的檔案格式跟 Redis 7.2.4 一致，可以直接把 Redis 的資料目錄拷給 Valkey 載入——這是 drop-in 遷移的基礎，不需要 dump / reload。</p>
<p><strong>生態與新功能層：要逐項確認</strong>。Redis 7.4+ 在 fork 之後新增的功能（Valkey 不一定跟進）、Redis Stack 的商業 module（RedisJSON / RedisSearch，Valkey 有自己的 valkey-search / valkey-bloom 但不是同一套）、偏 Redis Inc 的監控工具（RedisInsight 部分 vendor-specific 命令）——這層是相容性的真實風險所在，驗證要集中在這裡。</p>
<p>驗證的操作順序：先確認 client library 連得上且核心指令正常（第一層），再確認資料能載入（第二層），最後盤點你實際用到的 module 與 7.4+ 功能（第三層）。前兩層幾乎必過，工夫花在第三層。</p>
<h2 id="配置io-threads-多執行緒調校">配置：io-threads 多執行緒調校</h2>
<p>Valkey 跟 Redis 7.2.4 拉開的第一個實質技術差異是執行緒模型。Redis 的命令處理是單執行緒（I/O threads 只分擔 socket 讀寫，命令仍在主執行緒），Valkey 8.x 把更多 I/O 路徑非同步化，在多核機器上能讓單實例吞吐明顯高於 Redis——具體倍數依 workload 與核數而定，以 <a href="https://valkey.io/blog/">valkey.io 官方 benchmark</a> 為準，這裡不複述未經自己壓測的數字。</p>
<p>執行緒由 <code>io-threads</code> 控制，預設 1（單執行緒，跟 Redis 行為一致）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 確認目前執行緒數（預設 1）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">valkey-cli CONFIG GET io-threads
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 1) &#34;io-threads&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2) &#34;1&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># 調高 I/O 執行緒數（建議不超過機器實體核數、留核給其他進程）</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># redis.conf / valkey.conf:</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="c1">#   io-threads 4</span></span></span></code></pre></div><p>調校判讀：</p>
<ul>
<li><code>io-threads</code> 是啟動參數，多數版本需要重啟生效（不是所有 CONFIG SET 都能熱套），改 conf 後 rolling restart</li>
<li>設定值對齊機器核數但留 headroom，例如 8 核機器設 4-6，不要設滿</li>
<li>單核或低核機器設 1（預設）即可，多執行緒在核數不足時沒有收益反而增加切換開銷</li>
<li>I/O 密集（大量小命令、高連線數）的 workload 收益最明顯；CPU 密集的重命令（大 Lua、大 collection 操作）收益有限</li>
</ul>
<p>調完用實際 workload 壓測驗證，不要假設「開了就快」——執行緒配置的收益高度依賴 workload 形狀。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1切換後-module-指令報-unknown-command">Case 1：切換後 module 指令報 unknown command</h3>
<p><strong>徵兆</strong>：drop-in 換成 Valkey 後核心功能正常，但某些路徑報 <code>ERR unknown command 'JSON.SET'</code> 或 <code>FT.SEARCH</code>，application 部分功能失效。</p>
<p><strong>根因</strong>：用到了 Redis Stack 的商業 module（RedisJSON / RedisSearch）。這些 module 不在 fork 範圍內，Valkey 有自己的 valkey-search / valkey-bloom，但不是同一套指令、需要另外安裝。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>切換前用 <code>MODULE LIST</code> 在原 Redis 上盤點所有載入的 module</li>
<li>逐個確認 Valkey 是否有對應替代（valkey-search 對 RedisSearch 等），確認指令相容度</li>
<li>沒有對應的 module，評估改用 module-free 設計（例如把 JSON 操作拉回 application 層）</li>
<li>重度依賴 Redis Stack 商業 module 的場景，相容性邊界在這裡，可能該留在 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> Inc 商業版</li>
</ol>
<h3 id="case-2client-library-太舊協商失敗">Case 2：client library 太舊、協商失敗</h3>
<p><strong>徵兆</strong>：絕大多數 client 正常，但某個老服務的 client library 連 Valkey 報協定錯誤或行為異常。</p>
<p><strong>根因</strong>：Valkey 回報 <code>redis_version:7.2.4</code>，client library 若太舊（不支援 Redis 7.2 對應的協定特性，例如 RESP3）會協商失敗。這不是 Valkey 的問題，是 client 本來就跟不上 Redis 7.2。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>valkey-cli INFO server</code> 確認回報的 <code>redis_version</code>，對照 client library 支援到哪個 Redis 版本</li>
<li>升級過舊的 client library 到支援 Redis 7.2 的版本</li>
<li>必要時 client 端強制用 RESP2（多數 library 可配置），避開 RESP3 協商</li>
<li>這類問題在升級 Redis 7.2 時也會遇到，不是 Valkey 特有</li>
</ol>
<h3 id="case-3監控工具部分指標消失">Case 3：監控工具部分指標消失</h3>
<p><strong>徵兆</strong>：切換後 RedisInsight 或某監控 dashboard 部分面板空白、某些 vendor-specific 命令回錯。</p>
<p><strong>根因</strong>：RedisInsight 等 Redis Inc 工具有部分偏 Redis 商業版的命令，Valkey 不一定實作。核心指標（memory / hit rate / connections）通用，但 vendor-specific 的進階面板可能缺。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>監控改用通用工具：<code>valkey-cli INFO</code>、Prometheus + redis_exporter（相容 Valkey）、Grafana</li>
<li>核心指標（<code>used_memory</code> / <code>keyspace_hits</code> / <code>connected_clients</code>）在 Valkey 完全相容，監控覆蓋不受影響</li>
<li>把監控的相容性納入切換前驗證清單，不要切換後才發現面板空白</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">連線</a> 調校用到的 INFO 指標，這些在 Valkey 都通用</li>
</ol>
<h3 id="case-4io-threads-開太多效能反而下降">Case 4：io-threads 開太多、效能反而下降</h3>
<p><strong>徵兆</strong>：把 <code>io-threads</code> 從 1 調到 16 想榨效能，結果延遲不降反升、CPU 使用率異常。</p>
<p><strong>根因</strong>：<code>io-threads</code> 設成超過機器實體核數，執行緒互搶 CPU、context switch 開銷超過平行收益。或 workload 是 CPU 密集（重命令），I/O 多執行緒對它沒幫助。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>io-threads</code> 不超過實體核數，留 headroom 給 OS 與其他進程（8 核設 4-6）</li>
<li>用實際 workload 壓測對比不同 io-threads 值的延遲與吞吐，不要憑感覺調滿</li>
<li>CPU 密集 workload 收益有限，問題可能在命令本身太重（大 collection / 大 Lua），先優化命令</li>
<li>多執行緒解的是 I/O 平行度，不是單命令執行速度，分清楚瓶頸在哪</li>
</ol>
<h3 id="case-5以為換-valkey-就解決了-redis-的記憶體--fork-問題">Case 5：以為換 Valkey 就解決了 Redis 的記憶體 / fork 問題</h3>
<p><strong>徵兆</strong>：因為 Redis 的 fork 延遲尖峰或記憶體 OOM 而切到 Valkey，切完發現同樣的尖峰與 OOM 還在。</p>
<p><strong>根因</strong>：Valkey fork 自 Redis 7.2.4，繼承了 Redis 的記憶體模型、eviction 演算法、AOF/RDB fork 機制。這些行為在 Valkey 上完全一致——Valkey 的差異在執行緒與授權，不在記憶體與持久化架構。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / 淘汰 / fork 的調校在 Valkey 上跟 Redis 完全一樣，直接套用 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體調校</a> 與 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></li>
<li>fork 尖峰是 Redis 系列的共同架構限制，要根治走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 fork-less 機制，不是換 Valkey</li>
<li>切換 Valkey 的理由應該是授權合規、多執行緒吞吐或 managed 成本，不是記憶體問題</li>
<li>切換前釐清痛點：是授權 / 成本（Valkey 解）還是記憶體 / fork 架構（Valkey 不解）</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Valkey 的容量判讀，多數沿用 Redis（同源），差異集中在執行緒與授權成本：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Valkey 的情況</th>
          <th>判讀</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心指標（記憶體 / hit rate）</td>
          <td>跟 Redis 完全一致</td>
          <td>直接套用 Redis 的容量判讀</td>
      </tr>
      <tr>
          <td><code>io-threads</code></td>
          <td>預設 1、可調至接近核數</td>
          <td>多核 + I/O 密集才有收益、需壓測驗證</td>
      </tr>
      <tr>
          <td>單實例吞吐</td>
          <td>多執行緒下高於 Redis（依 workload）</td>
          <td>以 valkey.io benchmark 為準、自己壓測</td>
      </tr>
      <tr>
          <td>授權成本</td>
          <td>BSD 3-clause、商業使用無限制</td>
          <td>合規敏感場景的決定性優勢</td>
      </tr>
      <tr>
          <td>managed 成本</td>
          <td>ElastiCache for Valkey 約低 Redis 20%</td>
          <td>AWS 生態的成本優化路徑</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>記憶體 / fork 是瓶頸</strong>：Valkey 同源、不解這層，走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（fork-less + 更省記憶體）或 Redis 系列的 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>。</li>
<li><strong>需要 Redis Stack 商業 module</strong>：Valkey 的 valkey-search / valkey-bloom 覆蓋不到全部，重度依賴走 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> Inc 商業版。</li>
<li><strong>不想自管</strong>：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a> 是 AWS 的 default engine，managed failover / snapshot / patching 全託管，成本比 ElastiCache for Redis 低約 20%。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Valkey 的 deep article 大量複用 Redis 的調校知識（同源），它自己的獨特性在相容性驗證、執行緒與授權：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis 全系列 deep article</a></strong>：記憶體、持久化、Sentinel、連線的調校在 Valkey 上完全一致，Valkey 不重寫這些，直接套用。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache for Valkey</a></strong>：managed Valkey 把執行緒與 failover 託管，省掉自管的調校與演練。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的 ElastiCache for Valkey 案例</a></strong>：4700 萬月活的 sub-millisecond 配對引擎是相容性與規模化的生產證據，但 module / client 的相容性仍需逐案驗證。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></strong>：兩者都打「Redis 相容 + 更好的執行緒」，但 Valkey 是 fork（同源、最高相容），DragonflyDB 是 C++ 重寫（相容核心但架構不同），選型差異在相容度 vs 架構激進度。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>同源 deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（default engine 即 Valkey）、<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>ElastiCache → 自管 Redis / Valkey：脫離 managed 的遷移路徑</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/migrate-to-self-managed/</link><pubDate>Mon, 22 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/migrate-to-self-managed/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（target）。跑 6 維 diff dimension audit 後判定為 &lt;strong>Type C operational redesign hybrid&lt;/strong>：engine 層相容（Low）但 operational model 差異大（IAM auth → password/ACL、CloudWatch → 自管監控、auto failover → Sentinel/自建 HA）。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼從-managed-遷出">為什麼從 managed 遷出&lt;/h2>
&lt;p>ElastiCache 遷出的 driver 通常不是 engine 層問題 — 它跑的就是 Redis 或 Valkey。常見遷出原因：&lt;/p>
&lt;ul>
&lt;li>&lt;strong>成本&lt;/strong>：managed premium 在大規模（數百 GB、多叢集）下比自管 + 運維人力更貴，尤其跨帳戶大量叢集時&lt;/li>
&lt;li>&lt;strong>跨雲或混合雲&lt;/strong>：業務需要在 GCP、Azure 或 on-prem 同時運行 cache 層，ElastiCache 只在 AWS&lt;/li>
&lt;li>&lt;strong>功能限制&lt;/strong>：ElastiCache 不支援所有 Redis module（RediSearch、RedisJSON 等），或 Valkey 8.x 新功能 ElastiCache 尚未上線&lt;/li>
&lt;li>&lt;strong>控制權&lt;/strong>：自管可以自訂 redis.conf、自選 kernel 參數、自決 upgrade 時機&lt;/li>
&lt;/ul>
&lt;p>資料搬遷用 RDB export + import 就完成，真正的工程量在 operational model 重建 — ElastiCache 幫你管的 HA、monitoring、backup、security，遷出後全要自建。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>同 Redis/Valkey engine、RESP 相容&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>IAM auth → ACL/password、CloudWatch → 自管監控、auto failover → Sentinel 或手動&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>相同（key-value cache）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>ElastiCache 1 → Redis/Valkey + Sentinel/HA + 監控 + backup 多元件&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、認證方式換、少量 client config 修改&lt;/td>
 &lt;td>Low-Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>RDB 相容、cluster mode 對應 Redis Cluster&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>Operational model 是 High — 這是 Type C 的判定依據。遷移重心在重建 ElastiCache 幫你做的那些事。&lt;/p>
&lt;h2 id="階段一盤點-elasticache-依賴">階段一：盤點 ElastiCache 依賴&lt;/h2>
&lt;p>在動手之前，先列出 ElastiCache 幫你管的所有東西，每一項都要在自管環境重建或決定不要。&lt;/p>
&lt;h3 id="認證與網路">認證與網路&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>IAM auth&lt;/strong>：ElastiCache 支援 IAM auth token（短效 token），自管 Redis 改用 &lt;code>requirepass&lt;/code> 或 Redis 6+ ACL&lt;/li>
&lt;li>&lt;strong>VPC / Security Group&lt;/strong>：自管 Redis 仍需 VPC 隔離，但 security group 規則要自己維護&lt;/li>
&lt;li>&lt;strong>TLS&lt;/strong>：ElastiCache 原生 in-transit encryption，自管要自己配 redis TLS 憑證&lt;/li>
&lt;/ul>
&lt;h3 id="高可用">高可用&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Auto failover&lt;/strong>：ElastiCache 自動偵測 primary failure 並 promote replica。自管用 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA failover&lt;/a> 或 Redis Cluster 內建 failover&lt;/li>
&lt;li>&lt;strong>Cross-AZ replication&lt;/strong>：ElastiCache 自動跨 AZ。自管要自己在不同 AZ 部署 replica&lt;/li>
&lt;/ul>
&lt;h3 id="監控與備份">監控與備份&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>CloudWatch metrics&lt;/strong>：ElastiCache 自動發 &lt;code>CurrConnections&lt;/code>、&lt;code>CacheHitRate&lt;/code>、&lt;code>ReplicationLag&lt;/code> 等。自管用 &lt;code>INFO&lt;/code> 指令 + Prometheus redis_exporter&lt;/li>
&lt;li>&lt;strong>Snapshot&lt;/strong>：ElastiCache 自動 daily snapshot + 手動 snapshot。自管用 &lt;code>BGSAVE&lt;/code> + cron + 外部 storage&lt;/li>
&lt;/ul>
&lt;h3 id="跨-region-replication">跨 region replication&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Global Datastore&lt;/strong>：ElastiCache 支援跨 region active-passive replication。自管 Redis 沒有原生跨 region replication — 若目前使用 Global Datastore，遷出前需要決定是用 application-level replication、第三方工具（Redis Enterprise Active-Active）還是放棄跨 region cache 同步&lt;/li>
&lt;/ul>
&lt;h3 id="升級與維護">升級與維護&lt;/h3>
&lt;ul>
&lt;li>&lt;strong>Engine 升級&lt;/strong>：ElastiCache 在維護窗口自動或手動升級。自管要自己做 rolling upgrade&lt;/li>
&lt;li>&lt;strong>Patch&lt;/strong>：安全 patch 由 AWS 負責。自管要自己追蹤 CVE&lt;/li>
&lt;/ul>
&lt;h2 id="階段二建立自管環境">階段二：建立自管環境&lt;/h2>
&lt;h3 id="部署架構">部署架構&lt;/h3>
&lt;p>最小 production 架構：1 primary + 1 replica + 3 Sentinel（或 Redis Cluster 3 primary + 3 replica）。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（target）。跑 6 維 diff dimension audit 後判定為 <strong>Type C operational redesign hybrid</strong>：engine 層相容（Low）但 operational model 差異大（IAM auth → password/ACL、CloudWatch → 自管監控、auto failover → Sentinel/自建 HA）。</p></blockquote>
<h2 id="為什麼從-managed-遷出">為什麼從 managed 遷出</h2>
<p>ElastiCache 遷出的 driver 通常不是 engine 層問題 — 它跑的就是 Redis 或 Valkey。常見遷出原因：</p>
<ul>
<li><strong>成本</strong>：managed premium 在大規模（數百 GB、多叢集）下比自管 + 運維人力更貴，尤其跨帳戶大量叢集時</li>
<li><strong>跨雲或混合雲</strong>：業務需要在 GCP、Azure 或 on-prem 同時運行 cache 層，ElastiCache 只在 AWS</li>
<li><strong>功能限制</strong>：ElastiCache 不支援所有 Redis module（RediSearch、RedisJSON 等），或 Valkey 8.x 新功能 ElastiCache 尚未上線</li>
<li><strong>控制權</strong>：自管可以自訂 redis.conf、自選 kernel 參數、自決 upgrade 時機</li>
</ul>
<p>資料搬遷用 RDB export + import 就完成，真正的工程量在 operational model 重建 — ElastiCache 幫你管的 HA、monitoring、backup、security，遷出後全要自建。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis/Valkey engine、RESP 相容</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>IAM auth → ACL/password、CloudWatch → 自管監控、auto failover → Sentinel 或手動</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>相同（key-value cache）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>ElastiCache 1 → Redis/Valkey + Sentinel/HA + 監控 + backup 多元件</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、認證方式換、少量 client config 修改</td>
          <td>Low-Medium</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>RDB 相容、cluster mode 對應 Redis Cluster</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>Operational model 是 High — 這是 Type C 的判定依據。遷移重心在重建 ElastiCache 幫你做的那些事。</p>
<h2 id="階段一盤點-elasticache-依賴">階段一：盤點 ElastiCache 依賴</h2>
<p>在動手之前，先列出 ElastiCache 幫你管的所有東西，每一項都要在自管環境重建或決定不要。</p>
<h3 id="認證與網路">認證與網路</h3>
<ul>
<li><strong>IAM auth</strong>：ElastiCache 支援 IAM auth token（短效 token），自管 Redis 改用 <code>requirepass</code> 或 Redis 6+ ACL</li>
<li><strong>VPC / Security Group</strong>：自管 Redis 仍需 VPC 隔離，但 security group 規則要自己維護</li>
<li><strong>TLS</strong>：ElastiCache 原生 in-transit encryption，自管要自己配 redis TLS 憑證</li>
</ul>
<h3 id="高可用">高可用</h3>
<ul>
<li><strong>Auto failover</strong>：ElastiCache 自動偵測 primary failure 並 promote replica。自管用 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA failover</a> 或 Redis Cluster 內建 failover</li>
<li><strong>Cross-AZ replication</strong>：ElastiCache 自動跨 AZ。自管要自己在不同 AZ 部署 replica</li>
</ul>
<h3 id="監控與備份">監控與備份</h3>
<ul>
<li><strong>CloudWatch metrics</strong>：ElastiCache 自動發 <code>CurrConnections</code>、<code>CacheHitRate</code>、<code>ReplicationLag</code> 等。自管用 <code>INFO</code> 指令 + Prometheus redis_exporter</li>
<li><strong>Snapshot</strong>：ElastiCache 自動 daily snapshot + 手動 snapshot。自管用 <code>BGSAVE</code> + cron + 外部 storage</li>
</ul>
<h3 id="跨-region-replication">跨 region replication</h3>
<ul>
<li><strong>Global Datastore</strong>：ElastiCache 支援跨 region active-passive replication。自管 Redis 沒有原生跨 region replication — 若目前使用 Global Datastore，遷出前需要決定是用 application-level replication、第三方工具（Redis Enterprise Active-Active）還是放棄跨 region cache 同步</li>
</ul>
<h3 id="升級與維護">升級與維護</h3>
<ul>
<li><strong>Engine 升級</strong>：ElastiCache 在維護窗口自動或手動升級。自管要自己做 rolling upgrade</li>
<li><strong>Patch</strong>：安全 patch 由 AWS 負責。自管要自己追蹤 CVE</li>
</ul>
<h2 id="階段二建立自管環境">階段二：建立自管環境</h2>
<h3 id="部署架構">部署架構</h3>
<p>最小 production 架構：1 primary + 1 replica + 3 Sentinel（或 Redis Cluster 3 primary + 3 replica）。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Docker Compose 驗證用（production 用 VM 或 K8s）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># Primary</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">docker run -d --name redis-primary -p 6379:6379 redis:7 <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  redis-server --requirepass <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> --appendonly yes
</span></span><span class="line"><span class="ln">5</span><span class="cl">
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Replica</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">docker run -d --name redis-replica -p 6380:6379 redis:7 <span class="se">\
</span></span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="se"></span>  redis-server --replicaof redis-primary <span class="m">6379</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="se"></span>  --masterauth <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> --requirepass <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span></span></span></code></pre></div><p>Sentinel 或 Redis Cluster 配置見 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel HA Failover</a>。</p>
<h3 id="監控重建">監控重建</h3>
<p>ElastiCache CloudWatch metrics 對應的自管替代：</p>
<table>
  <thead>
      <tr>
          <th>ElastiCache metric</th>
          <th>自管替代</th>
          <th>來源</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CurrConnections</td>
          <td><code>connected_clients</code></td>
          <td><code>INFO clients</code></td>
      </tr>
      <tr>
          <td>CacheHitRate</td>
          <td><code>keyspace_hits / (keyspace_hits + keyspace_misses)</code></td>
          <td><code>INFO stats</code></td>
      </tr>
      <tr>
          <td>ReplicationLag</td>
          <td><code>master_repl_offset - slave_repl_offset</code></td>
          <td><code>INFO replication</code></td>
      </tr>
      <tr>
          <td>EngineCPUUtilization</td>
          <td><code>used_cpu_sys + used_cpu_user</code></td>
          <td><code>INFO cpu</code></td>
      </tr>
      <tr>
          <td>DatabaseMemoryUsagePercentage</td>
          <td><code>used_memory / maxmemory</code></td>
          <td><code>INFO memory</code></td>
      </tr>
      <tr>
          <td>Evictions</td>
          <td><code>evicted_keys</code></td>
          <td><code>INFO stats</code></td>
      </tr>
  </tbody>
</table>
<p>用 <a href="https://github.com/oliver006/redis_exporter">Prometheus redis_exporter</a> 自動採集，接 Grafana dashboard。</p>
<h3 id="backup-重建">Backup 重建</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># cron job: 每日 BGSAVE + 等完成 + 上傳 S3</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># LASTSAVE 回傳 Unix timestamp，BGSAVE 完成後 LASTSAVE 會更新</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="m">0</span> <span class="m">3</span> * * * <span class="nv">BEFORE</span><span class="o">=</span><span class="k">$(</span>redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> LASTSAVE<span class="k">)</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> BGSAVE <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="se"></span>  <span class="k">while</span> <span class="o">[</span> <span class="s2">&#34;</span><span class="k">$(</span>redis-cli -a <span class="s2">&#34;</span><span class="nv">$REDIS_PASSWORD</span><span class="s2">&#34;</span> LASTSAVE<span class="k">)</span><span class="s2">&#34;</span> <span class="o">=</span> <span class="s2">&#34;</span><span class="nv">$BEFORE</span><span class="s2">&#34;</span> <span class="o">]</span><span class="p">;</span> <span class="k">do</span> sleep 5<span class="p">;</span> <span class="k">done</span> <span class="o">&amp;&amp;</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="se"></span>  aws s3 cp /data/dump.rdb s3://backup-bucket/redis/<span class="k">$(</span>date +<span class="se">\%</span>Y<span class="se">\%</span>m<span class="se">\%</span>d<span class="k">)</span>.rdb</span></span></code></pre></div><p>Production 建議搭配 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence fork latency</a> 的監控，確認 BGSAVE 的 fork 不會造成延遲 spike。</p>
<h2 id="階段三資料搬遷與切換">階段三：資料搬遷與切換</h2>
<h3 id="搬遷策略">搬遷策略</h3>
<p>ElastiCache 的資料搬遷有兩條路：</p>
<p><strong>RDB export + import（適合 downtime 可接受的場景）</strong>：</p>
<ol>
<li>ElastiCache 建立手動 snapshot</li>
<li>把 snapshot export 到 S3（ElastiCache console → Export snapshot）</li>
<li>下載 RDB 檔，放到自管 Redis 的資料目錄</li>
<li>重啟自管 Redis 載入 RDB</li>
</ol>
<p><strong>雙寫期間遷移（適合零停機需求）</strong>：</p>
<ol>
<li>Application 同時寫 ElastiCache 和自管 Redis（雙寫）</li>
<li>讀取仍走 ElastiCache</li>
<li>監控自管 Redis 的資料量與命中率追上後，切讀取到自管</li>
<li>移除 ElastiCache 寫入</li>
<li>下線 ElastiCache</li>
</ol>
<p>雙寫的複雜度高於 RDB export。Cache 資料可重建的特性讓第一種策略在多數場景夠用 — 短暫 cache miss 的代價是回源到 DB，通常可接受。</p>
<h3 id="endpoint-切換">Endpoint 切換</h3>
<p>Application 用 endpoint 連 ElastiCache。切換時：</p>
<ol>
<li>把 application config 的 Redis host 改為自管 Redis endpoint</li>
<li>確認 TLS 與認證方式對齊（IAM token → password/ACL）</li>
<li>Rolling restart application</li>
<li>監控 cache hit rate 與 latency 回到 baseline</li>
</ol>
<p>如果用 DNS CNAME 間接指向 ElastiCache endpoint，可以直接改 CNAME 指向自管 Redis，application 不用改 config。</p>
<h2 id="階段四驗證與回退">階段四：驗證與回退</h2>
<h3 id="驗證清單">驗證清單</h3>
<table>
  <thead>
      <tr>
          <th>驗證項目</th>
          <th>通過條件</th>
          <th>工具</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>連線正常</td>
          <td>application 能 PING、無 auth error</td>
          <td>redis-cli + application log</td>
      </tr>
      <tr>
          <td>資料完整</td>
          <td>key count 跟 ElastiCache 一致（容許 TTL 過期差異）</td>
          <td><code>DBSIZE</code> 比對</td>
      </tr>
      <tr>
          <td>效能 baseline</td>
          <td>latency p99 與 hit rate 跟遷移前一致</td>
          <td>Prometheus + Grafana</td>
      </tr>
      <tr>
          <td>HA 測試</td>
          <td>kill primary，Sentinel promote replica，application 自動重連</td>
          <td>手動 failover drill</td>
      </tr>
      <tr>
          <td>Backup 測試</td>
          <td>BGSAVE 產生 RDB、上傳成功、可還原</td>
          <td>還原到測試 instance 驗證</td>
      </tr>
  </tbody>
</table>
<h3 id="回退路徑">回退路徑</h3>
<p>Cache 遷移的回退比 DB 遷移簡單 — cache 資料可重建。回退步驟：</p>
<ol>
<li>Application config 改回 ElastiCache endpoint（或 CNAME 指回）</li>
<li>Rolling restart</li>
<li>Cache miss 回源到 DB，自然 warm up</li>
</ol>
<p>ElastiCache 在遷移期間不要下線，保留 7-14 天作為回退保險。確認自管 Redis 穩定運行後再刪除 ElastiCache cluster。</p>
<h2 id="成本對照">成本對照</h2>
<table>
  <thead>
      <tr>
          <th>項目</th>
          <th>ElastiCache</th>
          <th>自管 Redis</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Compute</td>
          <td>managed node pricing（含 premium）</td>
          <td>EC2 / K8s 原價</td>
      </tr>
      <tr>
          <td>HA</td>
          <td>auto failover 內建</td>
          <td>Sentinel 或 Cluster 自建</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>CloudWatch 內建</td>
          <td>redis_exporter + Prometheus 自建</td>
      </tr>
      <tr>
          <td>Backup</td>
          <td>自動 snapshot</td>
          <td>cron + S3 自建</td>
      </tr>
      <tr>
          <td>人力</td>
          <td>低（AWS 管）</td>
          <td>高（on-call + upgrade + patch）</td>
      </tr>
      <tr>
          <td>靈活度</td>
          <td>受限（engine version、module）</td>
          <td>完全自控</td>
      </tr>
  </tbody>
</table>
<p>小規模（&lt; 50 GB、&lt; 5 cluster）通常 ElastiCache 的 managed premium 比自管人力便宜。Compute 跟 HA 的差額在小規模可忽略，但監控跟 backup 的自建成本是固定開銷 — 即使只管一個 cluster，redis_exporter + Prometheus + cron backup 的設定跟維護都要做。大規模（數百 GB、多叢集）或跨雲場景下，managed premium 累積到 cluster 數 × node 數的倍數，自管的邊際成本反而更低，遷出 ROI 才成立。</p>
<h2 id="交接路由">交接路由</h2>
<ul>
<li>Source vendor overview：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>Target vendor 操作：<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel HA</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster Resharding</a></li>
<li>監控重建：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis Memory Eviction Tuning</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">Redis Persistence Fork Latency</a></li>
<li>反向路徑：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/" data-link-title="自管 Redis / Valkey → AWS ElastiCache：engine 不變、變的是誰運維" data-link-desc="自管 Redis/Valkey 遷到 ElastiCache 的特殊之處：engine 沒變（Redis 還是 Redis）、data model 沒變、API 沒變——變的只有運維責任歸屬。本文跑 6 維 diff audit 對映 Type C operational hybrid、展開 VPC/安全/cutover 的實際工作、以及『把 failover/patching 交出去、同時交出哪些控制權』的責任邊界，5 個 production 踩坑">Redis → ElastiCache</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → DragonflyDB：drop-in 相容下的容量躍升 + 5 個踩雷</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>（target）。跟前一篇 &lt;a href="https://tarrragon.github.io/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security&lt;/a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 &lt;em>drop-in 相容&lt;/em> 形態的 migration、結構更接近 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a> 的 6-section flow + 一段「相容性驗證」前置。&lt;/p>&lt;/blockquote>
&lt;h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Driver&lt;/th>
 &lt;th>觸發場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Memory cost&lt;/strong>&lt;/td>
 &lt;td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Single-thread bottleneck&lt;/strong>&lt;/td>
 &lt;td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Multi-tenancy&lt;/strong>&lt;/td>
 &lt;td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 &lt;em>Redis Modules 依賴&lt;/em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 &lt;em>Lua script 用了 redis.call 進階 API&lt;/em>。&lt;/p>
&lt;h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased&lt;/h2>
&lt;p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration &lt;em>結構接近 standard deep article&lt;/em>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a>（target）。跟前一篇 <a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a> 的 6-phase playbook 對照、Redis → DragonflyDB 是 <em>drop-in 相容</em> 形態的 migration、結構更接近 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a> 的 6-section flow + 一段「相容性驗證」前置。</p></blockquote>
<h2 id="為什麼遷cost--single-thread--multi-tenancy-三條-driver">為什麼遷：cost / single-thread / multi-tenancy 三條 driver</h2>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Memory cost</strong></td>
          <td>Redis 6.x cluster 跑 1-10 TB 時、機器成本爆；DragonflyDB 記憶體效率提升 ~30%、相同 dataset 少 30% RAM</td>
      </tr>
      <tr>
          <td><strong>Single-thread bottleneck</strong></td>
          <td>Redis 主執行緒在單一 hot key 寫入時是瓶頸、scale-up 受限；DragonflyDB 多執行緒 + shared-nothing 設計、單機 throughput 號稱 25x</td>
      </tr>
      <tr>
          <td><strong>Multi-tenancy</strong></td>
          <td>Redis Cluster 多 namespace 需要 cluster-per-tenant、運維成本爆；DragonflyDB 設計上 namespace 隔離成本低</td>
      </tr>
  </tbody>
</table>
<p>反向 driver（DragonflyDB → Redis）也存在 — 主要是 <em>Redis Modules 依賴</em>（RedisJSON / RedisSearch / RedisGraph）DragonflyDB 不支援、或 <em>Lua script 用了 redis.call 進階 API</em>。</p>
<h2 id="跟-phased-migration-的對照drop-in-不需要-phased">跟 phased migration 的對照：drop-in 不需要 phased</h2>
<p>跟前一篇 Splunk → Elastic 的 6-phase playbook 不同、Redis → DragonflyDB 的 migration <em>結構接近 standard deep article</em>：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Splunk → Elastic（phased）</th>
          <th>Redis → DragonflyDB（drop-in）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema 對位</td>
          <td>需要（SPL ↔ KQL / CIM ↔ ECS）</td>
          <td>不需要（RESP protocol 相容）</td>
      </tr>
      <tr>
          <td>Rule translation</td>
          <td>4-12 週 SOC engineering 工作</td>
          <td>不需要（command 直接相容）</td>
      </tr>
      <tr>
          <td>Parallel run</td>
          <td>4-8 週 dual-SIEM 跑</td>
          <td>1-7 天 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Cutover 邊界</td>
          <td>軟邊界（routing 切換、可逆 30 分鐘）</td>
          <td>硬邊界（client 配置切換、單次完成）</td>
      </tr>
      <tr>
          <td>不可逆 cleanup</td>
          <td>1 年後 archive</td>
          <td>立刻（DragonflyDB 接管後 Redis 可關）</td>
      </tr>
      <tr>
          <td>整體週期</td>
          <td>4-9 個月</td>
          <td>1-4 週</td>
      </tr>
  </tbody>
</table>
<p><strong>判斷依據</strong>：migration 結構由 <em>source 跟 target 的 schema / protocol 差異程度</em> 決定、不是 universal phased playbook。本批第 2 篇驗證 <em>deep article methodology 的 6-section 框架</em> 在 drop-in migration 仍適用（只需前置 <em>相容性驗證</em> 段、其他 6 段對位）。</p>
<h2 id="相容性驗證在-cutover-前要確認的清單">相容性驗證：在 cutover 前要確認的清單</h2>
<p>DragonflyDB 號稱 Redis drop-in、但「drop-in」涵蓋範圍依 Redis feature 使用程度而定。Pre-migration 必跑的相容性 audit：</p>
<table>
  <thead>
      <tr>
          <th>Redis feature</th>
          <th>DragonflyDB 支援程度</th>
          <th>Action</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Basic data types (String / Hash / List / Set / ZSet)</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RESP protocol v2 / v3</td>
          <td>完全相容</td>
          <td>無需處理</td>
      </tr>
      <tr>
          <td>RDB load</td>
          <td>Redis 6.x RDB 完全相容；7.x 部分 feature 待測</td>
          <td>用 BGSAVE → 切換 → load 驗證</td>
      </tr>
      <tr>
          <td>AOF</td>
          <td>DragonflyDB 不用 AOF、改 <em>snapshotting</em> 模式</td>
          <td>不直接 import AOF、需經 RDB 中介</td>
      </tr>
      <tr>
          <td>Lua scripts</td>
          <td>90% 相容、部分 redis.call API + EVAL 邊界 case 差異</td>
          <td>Lua script audit 必跑、不能假設全相容</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>相容、但 message fanout 行為差異（多 thread 處理）</td>
          <td>高 fanout pub/sub 場景需測 latency</td>
      </tr>
      <tr>
          <td>Cluster mode</td>
          <td>DragonflyDB <em>單機</em> 即可達 cluster throughput、不必 cluster；emulated cluster mode 部分相容</td>
          <td>評估是否仍需 cluster</td>
      </tr>
      <tr>
          <td>Sentinel HA</td>
          <td>不直接支援、用 DragonflyDB 自家 replication</td>
          <td>HA 架構重設計</td>
      </tr>
      <tr>
          <td>Redis Modules (RedisJSON / Search / Graph)</td>
          <td><strong>不支援</strong></td>
          <td>必須前置改寫 application</td>
      </tr>
      <tr>
          <td>Streams</td>
          <td>相容、但 consumer group 行為部分差異</td>
          <td>Stream consumer 跑 dual-write 觀察</td>
      </tr>
      <tr>
          <td>Keyspace notifications</td>
          <td>相容</td>
          <td>無需處理</td>
      </tr>
  </tbody>
</table>
<p><strong>Audit 的關鍵 output</strong>：列「不相容功能」清單 + 對應 application code 修改範圍；若 Modules 在 production 使用、migration <em>退役</em>。</p>
<h2 id="step-by-step-cutover">Step-by-step cutover</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 部署 DragonflyDB</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">docker run -d --name dragonfly -p 6380:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  -v /data/dragonfly:/data <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  docker.dragonflydb.io/dragonflydb/dragonfly:latest <span class="se">\
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="se"></span>  --logtostderr --requirepass<span class="o">=</span>&lt;your_password&gt;
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 2. Redis 端 BGSAVE</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">redis-cli -h redis-primary BGSAVE
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 等到 BGSAVE 完成</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli -h redis-primary INFO Persistence <span class="p">|</span> grep rdb_last_save_time
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># 3. 把 dump.rdb 拷到 DragonflyDB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">scp redis-primary:/var/lib/redis/dump.rdb dragonfly-host:/data/dragonfly/
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 4. 重啟 DragonflyDB 載入 RDB</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">docker restart dragonfly
</span></span><span class="line"><span class="ln">17</span><span class="cl">
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="c1"># 5. 驗證資料一致</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">redis-cli -h dragonfly-host -p <span class="m">6380</span> DBSIZE
</span></span><span class="line"><span class="ln">20</span><span class="cl">redis-cli -h redis-primary DBSIZE
</span></span><span class="line"><span class="ln">21</span><span class="cl"><span class="c1"># 兩端 key 數對齊</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl"><span class="c1"># 6. Dual-write 1-7 天（application 同時寫兩端）</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl"><span class="c1"># 7. Read 切換到 DragonflyDB、Redis 端只寫不讀</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="c1"># 8. Write 切換、Redis 端 standby</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="c1"># 9. 觀察 1-2 週、無異常後 Redis decommission</span></span></span></code></pre></div><p>關鍵時間點：</p>
<ul>
<li><strong>BGSAVE → load</strong>：100GB RDB 約 5-15 分鐘、跨網路 SCP 時間另算</li>
<li><strong>Dual-write window</strong>：1-7 天觀察、application 寫兩端、read 仍走 Redis</li>
<li><strong>Cutover</strong>：read switch → write switch、每步間隔 24 小時</li>
<li><strong>Decom</strong>：Redis 保留 standby 1-2 週、無異常後關閉</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1rdb-版本差dragonflydb-load-失敗">Case 1：RDB 版本差，DragonflyDB load 失敗</h3>
<p><strong>徵兆</strong>：Redis 7.2 端 BGSAVE 出的 <code>dump.rdb</code> 在 DragonflyDB load 時報 <code>Unsupported RDB version</code>、DragonflyDB 啟動失敗。</p>
<p><strong>根因</strong>：Redis 7.2 RDB version 11 含新 feature（function library / sharded pubsub）DragonflyDB 當前 release 沒支援；版本相容性需逐 release 確認。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration 版本相容矩陣 audit</strong>：DragonflyDB release note 對照 Redis version、確認 RDB version 支援</li>
<li><strong>降級 BGSAVE</strong>：Redis 端設 <code>rdb-version 9</code>（Redis 6.x 兼容版本）、犧牲 Redis 7.x 新 feature</li>
<li><strong>替代方案</strong>：用 <code>redis-cli --scan</code> + <code>MIGRATE</code> 命令 incremental 搬、不用 RDB；速度慢 100x 但相容性好</li>
</ol>
<h3 id="case-2lua-script-跑進-eval-不一致">Case 2：Lua script 跑進 EVAL 不一致</h3>
<p><strong>徵兆</strong>：dual-write 階段、發現某些 EVAL script 在 Redis 跟 DragonflyDB 結果不同；具體是某個 <code>redis.call(&quot;OBJECT&quot;, &quot;ENCODING&quot;, key)</code> 在 DragonflyDB 回不一樣的 encoding 字串。</p>
<p><strong>根因</strong>：DragonflyDB 內部不用 Redis 的 ziplist / listpack encoding（dashtable 不需要）、<code>OBJECT ENCODING</code> 返回值不對等；script 邏輯依賴 encoding 來決定行為、結果不同。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Audit Lua script</strong>：grep 所有 <code>redis.call(&quot;OBJECT&quot;</code>、列出依賴 encoding 的 script</li>
<li><strong>改寫 application</strong>：不依賴 encoding、改用 <code>MEMORY USAGE</code> 或 high-level check</li>
<li><strong>接受差異</strong>：DragonflyDB 不會回 encoding 但 functional 結果對等、SOC review 確認可接受</li>
</ol>
<h3 id="case-3pubsub-fanout-高負載-latency">Case 3：Pub/Sub fanout 高負載 latency</h3>
<p><strong>徵兆</strong>：production 切到 DragonflyDB 後、Pub/Sub 訂閱端 latency p99 從 5ms 漲到 20-50ms；topic fanout &gt;10K subscriber 場景。</p>
<p><strong>根因</strong>：DragonflyDB 多 thread 設計、Pub/Sub message 在 thread 間 dispatch 需要 routing；Redis single-thread 沒這個 overhead。高 fanout 是 DragonflyDB 設計取捨。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>架構</strong>：高 fanout Pub/Sub 不用 DragonflyDB、改 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> / Redis Streams + consumer group</li>
<li><strong>DragonflyDB 配置調整</strong>：<code>--proactor_threads</code> 對 Pub/Sub 影響大、調到符合 CPU 核心數</li>
<li><strong>接受 latency</strong>：&lt; 10K subscriber 差異可忽略、不必動</li>
</ol>
<h3 id="case-4cluster-mode-看似相容但-slot-routing-行為差">Case 4：Cluster mode 看似相容但 slot routing 行為差</h3>
<p><strong>徵兆</strong>：application 用 Redis Cluster client（lettuce / Jedis cluster mode）連 DragonflyDB emulated cluster、運行幾天後 <code>MOVED</code> redirect 異常、key 找不到。</p>
<p><strong>根因</strong>：DragonflyDB emulated cluster mode 是 <em>single node 模擬</em>、CLUSTER SLOTS 返回固定 mapping；某些 client 端 cluster topology cache 跟實際 routing 不對齊、發 redirect。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 改 standalone client</strong>：DragonflyDB single node 已能達 cluster 級 throughput、不必用 cluster client</li>
<li><strong>Client config</strong>：lettuce 端 <code>clusterTopologyRefreshOptions(...)</code> 設較長 refresh、減少 redirect 機會</li>
<li><strong>長期</strong>：等 DragonflyDB cluster 正式 GA 後再評估</li>
</ol>
<h3 id="case-5modules-用了沒注意migration-卡住">Case 5：Modules 用了沒注意，migration 卡住</h3>
<p><strong>徵兆</strong>：cutover 後幾天、application 某個功能完全壞、log 顯示 <code>ERR unknown command 'JSON.SET'</code>；DragonflyDB 不支援 RedisJSON。</p>
<p><strong>根因</strong>：Pre-migration audit 漏掉 application 用了 RedisJSON（透過某 client library 抽象）；DragonflyDB 不支援該 Module 命令、application 直接壞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-migration audit 必跑</strong>：<code>MONITOR</code> 抓 1 小時 production traffic、grep 非 standard command（<code>JSON.*</code> / <code>FT.*</code> / <code>GRAPH.*</code>）</li>
<li><strong>應急回退</strong>：Redis standby 還在、application client config 切回</li>
<li><strong>長期</strong>：JSON 改用 standard Hash + serialization、Search 改 Elasticsearch / Meilisearch、Graph 改 Neo4j</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis（self-managed）</th>
          <th>DragonflyDB</th>
          <th>取捨</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Single-node throughput</td>
          <td>~100K-200K ops/s</td>
          <td>~2-5M ops/s（號稱 25x）</td>
          <td>DragonflyDB 領先、實測依 workload 而定</td>
      </tr>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>-30% 平均、依資料分佈</td>
          <td>DragonflyDB 領先</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF 雙模式</td>
          <td>Snapshotting 為主、不用 AOF</td>
          <td>Redis 對 durability 要求高的 workload 仍領先</td>
      </tr>
      <tr>
          <td>HA / Replication</td>
          <td>Sentinel + Cluster 成熟</td>
          <td>自家 replication、HA 文件相對少</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Modules ecosystem</td>
          <td>RedisJSON / Search / Graph / TimeSeries</td>
          <td>不支援</td>
          <td>Redis 領先</td>
      </tr>
      <tr>
          <td>Cluster scaling</td>
          <td>Cluster mode 成熟</td>
          <td>單機效能高、cluster 仍 emerging</td>
          <td>Redis 領先、但 DragonflyDB 單機已能 cover 多數 use case</td>
      </tr>
      <tr>
          <td>Total cost (10TB cache)</td>
          <td>$8-15K USD / month</td>
          <td>$2-5K USD / month</td>
          <td>DragonflyDB 顯著便宜</td>
      </tr>
      <tr>
          <td>Operational maturity</td>
          <td>高（10+ 年 production）</td>
          <td>中（2022+、production 案例 1000+）</td>
          <td>Redis 領先</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：cache use case 簡單（pure cache / session store）走 DragonflyDB；複雜 use case（Modules / Pub/Sub fanout / strict durability）保留 Redis。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-client-library-整合">跟 client library 整合</h3>
<p>主流 Redis client（lettuce / Jedis / redis-py / node-redis / go-redis）都直接相容 DragonflyDB；唯一例外是 <em>cluster client</em> 模式行為差（見 Case 4）。</p>
<h3 id="跟-monitoring-整合">跟 monitoring 整合</h3>
<p>DragonflyDB exporter 提供 Prometheus metric、跟 Redis exporter 對應 metric 名稱 80% 相同；grafana dashboard 需小改：</p>
<ul>
<li><code>redis_memory_used_bytes</code> → <code>dragonfly_memory_used_bytes</code></li>
<li><code>redis_commands_processed_total</code> → <code>dragonfly_commands_processed_total</code></li>
</ul>
<h3 id="跟-redis-sentinel-ha-對位">跟 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">Redis Sentinel HA</a> 對位</h3>
<p>DragonflyDB 不直接支援 Sentinel、HA 走自家 <em>master-replica</em> + DNS-based failover：</p>
<ol>
<li>DragonflyDB primary + replica</li>
<li>K8s 用 StatefulSet + Service + readiness probe</li>
<li>失敗 failover 比 Sentinel 慢（30s-2min vs 5-15s）</li>
</ol>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>DragonflyDB Cluster GA</strong>：正式 cluster mode 出來後重評估</li>
<li><strong>Stream + consumer group 細節</strong>：dual-write 期間驗證每個 consumer pattern</li>
<li><strong>Modules 替代方案</strong>：JSON / Search / Graph 各自的 cloud-native 替代評估</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></li>
<li>平行 migration playbook：<a href="/blog/backend/07-security-data-protection/vendors/splunk/migrate-to-elastic-security/" data-link-title="Splunk → Elastic Security Detection Rule Migration：6 段 phased playbook 跟 5 大踩雷" data-link-desc="從 Splunk Enterprise Security 遷到 Elastic Security 的 detection rule translation playbook：SPL ↔ KQL/ES|QL schema 對位、AI-assisted translation pipeline、parallel run 比對、cutover routing、5 個 production 踩雷（macro 沒對應 / time zone 差異 / summary index 不對位 / alert dedup key 衝突 / 過早 decommission）、capacity / cost 對照">Splunk → Elastic Security</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 &lt;em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift&lt;/em>；本文是 &lt;em>paradigm reduction&lt;/em>（downgrade 方向）的 dogfood。&lt;/p>&lt;/blockquote>
&lt;h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm&lt;/h2>
&lt;p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>概念&lt;/th>
 &lt;th>Redis&lt;/th>
 &lt;th>Memcached&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>核心 paradigm&lt;/td>
 &lt;td>Multi-paradigm（KV + 資料結構 + pub/sub + script）&lt;/td>
 &lt;td>Pure cache（KV + TTL）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Value 類型&lt;/td>
 &lt;td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog&lt;/td>
 &lt;td>byte string only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Atomic operations&lt;/td>
 &lt;td>100+（INCR / LPUSH / ZADD / &amp;hellip;）&lt;/td>
 &lt;td>INCR / DECR / APPEND / CAS&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Server-side scripting&lt;/td>
 &lt;td>Lua scripts (&lt;code>EVAL&lt;/code>)&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Pub/Sub&lt;/td>
 &lt;td>Native&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Persistence&lt;/td>
 &lt;td>RDB / AOF&lt;/td>
 &lt;td>無（restart 全失）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replication&lt;/td>
 &lt;td>Async / sync replication&lt;/td>
 &lt;td>無&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Cluster&lt;/td>
 &lt;td>Redis Cluster + Sentinel HA&lt;/td>
 &lt;td>Memcached cluster（client-side sharding）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Eviction policy&lt;/td>
 &lt;td>8 種（LRU / LFU / random / &amp;hellip;）&lt;/td>
 &lt;td>LRU only&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Expiration accuracy&lt;/td>
 &lt;td>TTL 精確到 ms&lt;/td>
 &lt;td>TTL 精確到 second、lazy expiration&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。&lt;/strong> Redis 的 features（hash / sorted set / pub/sub）多數 &lt;em>不該移除&lt;/em>、是 &lt;em>重新分配到對應 specialized service&lt;/em>：&lt;/p>
&lt;ul>
&lt;li>Hash / sorted set → application 端用 JSON + 自管 index&lt;/li>
&lt;li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）&lt;/li>
&lt;li>Lua scripts → application code&lt;/li>
&lt;li>Persistence → 真正需要的 data 該存 DB、不是 cache&lt;/li>
&lt;li>Replication / cluster → Memcached 自己 cluster strategy&lt;/li>
&lt;/ul>
&lt;h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver&lt;/h2>
&lt;ul>
&lt;li>&lt;strong>Operational simplification&lt;/strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family&lt;/li>
&lt;li>&lt;strong>Cost&lt;/strong>：對 &lt;em>純 cache use case&lt;/em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）&lt;/li>
&lt;li>&lt;strong>Strict cache discipline&lt;/strong>：Memcached &lt;em>逼&lt;/em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 &lt;em>poor man&amp;rsquo;s database&lt;/em>&lt;/li>
&lt;/ul>
&lt;p>反向 driver（Memcached → Redis）：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> 跟 <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a>。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 <em>Paradigm = High（multi-paradigm → pure cache）→ Type E paradigm shift</em>；本文是 <em>paradigm reduction</em>（downgrade 方向）的 dogfood。</p></blockquote>
<h2 id="memcached-不是-simpler-redis是-cache-paradigm">Memcached 不是 simpler Redis、是 cache paradigm</h2>
<p>把 Redis → Memcached 當「移除 Redis 功能」是最常見的誤判：</p>
<table>
  <thead>
      <tr>
          <th>概念</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>核心 paradigm</td>
          <td>Multi-paradigm（KV + 資料結構 + pub/sub + script）</td>
          <td>Pure cache（KV + TTL）</td>
      </tr>
      <tr>
          <td>Value 類型</td>
          <td>String / Hash / List / Set / Sorted Set / Stream / Bitmap / HyperLogLog</td>
          <td>byte string only</td>
      </tr>
      <tr>
          <td>Atomic operations</td>
          <td>100+（INCR / LPUSH / ZADD / &hellip;）</td>
          <td>INCR / DECR / APPEND / CAS</td>
      </tr>
      <tr>
          <td>Server-side scripting</td>
          <td>Lua scripts (<code>EVAL</code>)</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Pub/Sub</td>
          <td>Native</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Persistence</td>
          <td>RDB / AOF</td>
          <td>無（restart 全失）</td>
      </tr>
      <tr>
          <td>Replication</td>
          <td>Async / sync replication</td>
          <td>無</td>
      </tr>
      <tr>
          <td>Cluster</td>
          <td>Redis Cluster + Sentinel HA</td>
          <td>Memcached cluster（client-side sharding）</td>
      </tr>
      <tr>
          <td>Eviction policy</td>
          <td>8 種（LRU / LFU / random / &hellip;）</td>
          <td>LRU only</td>
      </tr>
      <tr>
          <td>Expiration accuracy</td>
          <td>TTL 精確到 ms</td>
          <td>TTL 精確到 second、lazy expiration</td>
      </tr>
  </tbody>
</table>
<p><strong>核心差異不在「Memcached 少了 Redis 功能」、在「Memcached 是不同的 cache paradigm」。</strong> Redis 的 features（hash / sorted set / pub/sub）多數 <em>不該移除</em>、是 <em>重新分配到對應 specialized service</em>：</p>
<ul>
<li>Hash / sorted set → application 端用 JSON + 自管 index</li>
<li>Pub/Sub → message queue（NATS / Redis Streams / Kafka）</li>
<li>Lua scripts → application code</li>
<li>Persistence → 真正需要的 data 該存 DB、不是 cache</li>
<li>Replication / cluster → Memcached 自己 cluster strategy</li>
</ul>
<h2 id="為什麼遷simplification--cost--ops-三條-driver">為什麼遷：simplification / cost / ops 三條 driver</h2>
<ul>
<li><strong>Operational simplification</strong>：Memcached 沒 persistence / replication / cluster mode、ops surface 縮小、團隊不用懂 Redis 25+ command family</li>
<li><strong>Cost</strong>：對 <em>純 cache use case</em> 而言、Memcached 每 GB 比 Redis 便宜（memory efficiency 略勝 + 無 persistence overhead）</li>
<li><strong>Strict cache discipline</strong>：Memcached <em>逼</em> application code 把「真正的 cache」跟「半 persistent state」分開、避免 Redis 變 <em>poor man&rsquo;s database</em></li>
</ul>
<p>反向 driver（Memcached → Redis）：</p>
<ul>
<li>Application 寫到 Memcached 後發現需要 <em>atomic counter / leaderboard / queue / lock</em>、應該升 Redis（不是繼續 wrap Memcached）</li>
</ul>
<h2 id="跑-6-維-audit">跑 6 維 audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Redis 命令集 → Memcached 命令集、相容度 &lt; 20%</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>兩者都簡單、Memcached 略簡單</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Paradigm</td>
          <td>Multi-paradigm → pure cache</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Components</td>
          <td>同 1 個 cache service</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>必改（任何 hash / list / sorted set / pubsub 用法）</td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>同 single instance / cluster</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>3 維 High（Schema / Paradigm / Application change）多軸高、主導維度 = Paradigm → <strong>Type E paradigm shift</strong>；Schema + Application change 抽獨立段補充。</p>
<h2 id="結構類-type-e--paradigm-reduction-分配路線">結構：類 Type E + paradigm reduction 分配路線</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">1. Memcached 不是 simpler Redis（concept reverse 開頭）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 為什麼遷
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">3. 6 維 audit
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">4. Paradigm reduction 路線（Redis features 對應的 specialized service）
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">5. Schema 差段（Redis vs Memcached command set）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">6. Application 重設計（per-call-site refactor）
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">7. Migration 流程（漸進、部分 use case 切）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">8. Production 故障演練
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">9. Capacity / cost
</span></span><span class="line"><span class="ln">10</span><span class="cl">10. 整合 / 下一步</span></span></code></pre></div><p>10 章節、220-260 行。比 Type E（<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a>）多 <em>paradigm reduction 路線</em> 段。</p>
<h2 id="paradigm-reduction-路線">Paradigm reduction 路線</h2>
<p>Redis features 對應的 specialized service：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Redis Hash           → Application 端 JSON.stringify + Memcached SET
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">                       (or 直接存 DB + Memcached cache layer)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Redis List (queue)   → NATS / Kafka / RabbitMQ / SQS
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">Redis List (stack)   → Application 端用 array + 自管 LIFO
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">Redis Set            → Application 端用 array + dedup OR 用 DB unique index
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">Redis Sorted Set     → Application 端用 ordered list + comparator
</span></span><span class="line"><span class="ln">11</span><span class="cl">                       OR PostgreSQL + index
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl">Redis Stream         → Kafka / Redis Streams (保留) / NATS JetStream
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">Redis Pub/Sub        → NATS Core / Redis Streams / Kafka
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl">Redis Lua script     → Application code（避免 atomic 假設）
</span></span><span class="line"><span class="ln">18</span><span class="cl">
</span></span><span class="line"><span class="ln">19</span><span class="cl">Redis distributed lock → Consul / etcd / DB advisory lock / Redis (保留)
</span></span><span class="line"><span class="ln">20</span><span class="cl">
</span></span><span class="line"><span class="ln">21</span><span class="cl">Redis Bitmap         → DB bit column / 應用端 bitset
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">Redis HyperLogLog    → DB approx_count_distinct / 應用端 cardinality estimator</span></span></code></pre></div><p>Migration scope 包含 <em>每個 Redis-specific feature use case 對應的 service 評估</em>；不是「移除」、是「重新分配」。</p>
<h2 id="application-重設計">Application 重設計</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># Before: Redis hash</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;email&#39;</span><span class="p">,</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">hset</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">,</span> <span class="s1">&#39;Alice&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># After: Memcached + JSON</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="kn">import</span> <span class="nn">json</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">user_data</span> <span class="o">=</span> <span class="p">{</span><span class="s1">&#39;email&#39;</span><span class="p">:</span> <span class="s1">&#39;a@b.com&#39;</span><span class="p">,</span> <span class="s1">&#39;name&#39;</span><span class="p">:</span> <span class="s1">&#39;Alice&#39;</span><span class="p">}</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">user_data</span><span class="p">))</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">user</span> <span class="o">=</span> <span class="n">json</span><span class="o">.</span><span class="n">loads</span><span class="p">(</span><span class="n">mc</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s1">&#39;user:123&#39;</span><span class="p">)</span> <span class="ow">or</span> <span class="s1">&#39;</span><span class="si">{}</span><span class="s1">&#39;</span><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Before: Redis sorted set (leaderboard)</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">redis</span><span class="o">.</span><span class="n">zadd</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="p">{</span><span class="s1">&#39;alice&#39;</span><span class="p">:</span> <span class="mi">100</span><span class="p">,</span> <span class="s1">&#39;bob&#39;</span><span class="p">:</span> <span class="mi">95</span><span class="p">})</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">top_10</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">zrevrange</span><span class="p">(</span><span class="s1">&#39;leaderboard&#39;</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">9</span><span class="p">,</span> <span class="n">withscores</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># After: PostgreSQL + index + Memcached cache</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1"># Persistent: write to DB</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="c1"># Cache: pre-compute top 10 in DB query, cache in Memcached</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s1">&#39;leaderboard:top10&#39;</span><span class="p">,</span> <span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">db</span><span class="o">.</span><span class="n">query</span><span class="p">(</span><span class="s1">&#39;SELECT user, score FROM scores ORDER BY score DESC LIMIT 10&#39;</span><span class="p">)))</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># Before: Redis distributed lock</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="k">with</span> <span class="n">redis</span><span class="o">.</span><span class="n">lock</span><span class="p">(</span><span class="s1">&#39;resource:1&#39;</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">10</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># After: PostgreSQL advisory lock OR Consul session</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="k">with</span> <span class="n">db</span><span class="o">.</span><span class="n">advisory_lock</span><span class="p">(</span><span class="n">resource_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">    <span class="n">process_resource</span><span class="p">()</span></span></span></code></pre></div><p>每個 Redis-specific pattern 都要 per-call-site refactor、不是 SDK 換。</p>
<h2 id="migration-流程">Migration 流程</h2>
<p>跟 <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 同 <em>partial migration</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">1. Audit application code、列所有 Redis call site + feature 使用
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">2. 按 feature 分類處理 plan:
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Pure KV (GET/SET/DEL/TTL): 切 Memcached 直接
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - Hash → JSON + Memcached: per-call-site refactor
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">   - List/Sorted Set: 評估是 queue / leaderboard / 其他用途、對應 service
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">   - Pub/Sub: 移到 message queue
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - Lock: 移到 DB 或保留 Redis
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">3. 部分 application 先切（純 KV use case）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">4. 複雜 use case 逐步 refactor 到對應 service
</span></span><span class="line"><span class="ln">10</span><span class="cl">5. Memcached 跑 production 後、Redis 可降為 *narrow scope*（只跑剩餘 Redis-specific feature）
</span></span><span class="line"><span class="ln">11</span><span class="cl">   或完全退役（如果 application 已 refactor 乾淨）
</span></span><span class="line"><span class="ln">12</span><span class="cl">6. 長期混合架構：Memcached cache layer + DB persistent state + 可選的 Redis（locks / specialty）</span></span></code></pre></div><p>整體 3-12 個月、依 Redis-specific feature 使用深度。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1hash--json-後-getset-round-trip-變-n1">Case 1：Hash → JSON 後 GET/SET round-trip 變 N+1</h3>
<p><strong>徵兆</strong>：cutover 後 application latency p99 從 5ms 漲到 50ms；profiling 顯示「為了改 user.email、要先 GET user object → modify → SET」、原本 Redis <code>HSET</code> 1 個 round-trip 現在 2 個。</p>
<p><strong>根因</strong>：JSON-encoded value 不能 partial update、每次改一欄都要 read-modify-write。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Application 端 cache JSON object in memory</strong>：read-modify-write 仍 1 個 SET、但 read 是 memory</li>
<li><strong>Compare-and-swap (CAS)</strong>：Memcached CAS 防止 concurrent update lost</li>
<li><strong>Field-level cache key</strong>：把 hash 拆成 N 個 Memcached key（<code>user:123:email</code> / <code>user:123:name</code>）、避開 JSON</li>
</ol>
<h3 id="case-2sorted-set-leaderboard-退化recomputation-cost-爆">Case 2：Sorted set leaderboard 退化、recomputation cost 爆</h3>
<p><strong>徵兆</strong>：原本 Redis leaderboard <code>ZADD</code> + <code>ZREVRANGE</code> &lt; 1ms；切 DB-backed leaderboard 後 <code>SELECT ... ORDER BY ... LIMIT 10</code> 在 1M+ row 跑 100-500ms。</p>
<p><strong>根因</strong>：Memcached 不支援 sorted set、leaderboard 必須在 DB 算、N 大時 sort 慢。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Cache pre-computed top N</strong>：DB scheduled job 每分鐘算 top 100、寫 Memcached、application 讀 cache 不直查 DB</li>
<li><strong>Materialized view + index</strong>：DB 端用 materialized view + index、毫秒級 query</li>
<li><strong>保留 Redis sorted set</strong>：leaderboard 是 Redis 強項、不該退到 Memcached、走混合架構</li>
</ol>
<h3 id="case-3pubsub-移除缺-fan-out-機制">Case 3：Pub/Sub 移除、缺 fan-out 機制</h3>
<p><strong>徵兆</strong>：原本 Redis Pub/Sub 跑 cache invalidation broadcast、N 個 application instance 都收 invalidation msg；切 Memcached 後失去 broadcast、cache stale。</p>
<p><strong>根因</strong>：Memcached 沒 Pub/Sub；application 需要外部 fan-out 機制。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>NATS / Redis Streams + consumer group</strong>：each application instance 是 consumer、收 invalidation</li>
<li><strong>Database trigger + LISTEN/NOTIFY</strong>：PostgreSQL <code>LISTEN/NOTIFY</code> 對中型 fan-out 足夠</li>
<li><strong>Architecture rethink</strong>：是否真需要 broadcast invalidation？通常用 <em>TTL-based cache</em> + <em>cache key versioning</em> 就能 cover 多數 invalidation use case</li>
</ol>
<h3 id="case-4atomic-incr-沒對等race-condition">Case 4：Atomic INCR 沒對等、race condition</h3>
<p><strong>徵兆</strong>：rate limiter / counter pattern 切 Memcached、<code>mc.incr(key)</code> 在 key 不存在時 return None（不 auto-init 為 0）；application 端 <code>if None: mc.set(key, 1)</code> race condition、低機率 counter reset。</p>
<p><strong>根因</strong>：Memcached INCR 對 missing key 不像 Redis 自動 init；application 端 init logic 容易 race。</p>
<p><strong>修法</strong>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 用 ADD（atomic put-if-absent）</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">add</span><span class="p">(</span><span class="n">key</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span>  <span class="c1"># only sets if missing</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">mc</span><span class="o">.</span><span class="n">incr</span><span class="p">(</span><span class="n">key</span><span class="p">)</span>    <span class="c1"># always works after add</span></span></span></code></pre></div><p><code>ADD</code> + <code>INCR</code> 兩個 atomic operation 合起來 race-free。</p>
<h3 id="case-5eviction-policy-差異production-cache-hit-rate-降">Case 5：Eviction policy 差異、production cache hit rate 降</h3>
<p><strong>徵兆</strong>：cutover 後 cache hit rate 從 95% 降到 80%；profiling 發現「重要 key 沒在 cache」、新 key 一直擠走熱 key。</p>
<p><strong>根因</strong>：Redis 預設 <code>allkeys-lfu</code> (least frequently used)、長期熱 key 不被擠；Memcached 只有 LRU、單純按 access time、burst access 的 cold key 擠走 long-tail hot key。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Memory headroom</strong>：Memcached memory 限制拉高 30-50%、避免 eviction pressure</li>
<li><strong>Application-side cache priority</strong>：critical key 用 <em>no-expiration set</em> + 主動 refresh</li>
<li><strong>保留 Redis for LFU workload</strong>：long-tail hot key 場景 Redis LFU 更合適、不該退 Memcached</li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis</th>
          <th>Memcached</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Memory efficiency</td>
          <td>baseline</td>
          <td>+10-20%（無 metadata overhead）</td>
      </tr>
      <tr>
          <td>Throughput</td>
          <td>~100K ops/s single-thread</td>
          <td>~500K-1M ops/s multi-threaded</td>
      </tr>
      <tr>
          <td>Latency p99</td>
          <td>1-3ms</td>
          <td>0.5-1ms</td>
      </tr>
      <tr>
          <td>Persistence overhead</td>
          <td>5-15% CPU</td>
          <td>0</td>
      </tr>
      <tr>
          <td>Operational FTE</td>
          <td>0.3-0.8</td>
          <td>0.1-0.3</td>
      </tr>
      <tr>
          <td>Application complexity</td>
          <td>Low（feature 豐富）</td>
          <td>Higher（feature 移到 application）</td>
      </tr>
      <tr>
          <td>Cost per GB memory</td>
          <td>baseline</td>
          <td>略低（無 persistence I/O / replication overhead）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：純 cache use case 走 Memcached 省 ops + 略省 cost；application 已用 Redis-specific feature 不該切；混合架構是 long-term default。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a> 對比</h3>
<p>兩條路：</p>
<ul>
<li>DragonflyDB：保留 Redis paradigm、優化 throughput + memory；application 不用改</li>
<li>Memcached：退到 pure cache paradigm、application 必須改、但 ops 簡化</li>
</ul>
<p>選擇取決於 <em>是否真的需要 Redis multi-paradigm features</em>：用得到就 DragonflyDB / Redis、用不到就 Memcached。</p>
<h3 id="跟-nats-整合">跟 <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</a> 整合</h3>
<p>Redis Pub/Sub 移除後、應用端 fan-out / messaging 需求轉到 NATS / Redis Streams / Kafka；本文 cross-link migration playbook <a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> 有 paradigm shift 流程參考。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Memcached Cluster strategy</strong>：client-side consistent hashing vs server-side cluster mode、ops 簡化 vs scalability 取捨</li>
<li><strong>Long-term mixed architecture</strong>：80% Memcached + 20% Redis 是常見 stable state、不一定要完全消除 Redis</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>平行 migration playbook (Type E)：<a href="/blog/backend/03-message-queue/vendors/kafka/migrate-from-to-nats/" data-link-title="Kafka ↔ NATS：不是 migration、是 messaging paradigm 重設計" data-link-desc="Kafka 跟 NATS 不是同類產品（log-based event streaming vs subject-based messaging）、&#39;migration&#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &#43; 混合架構">Kafka ↔ NATS</a> / <a href="/blog/backend/01-database/vendors/postgresql/migrate-to-cockroachdb/" data-link-title="PostgreSQL → CockroachDB：三維皆 High 的多重歸類 migration" data-link-desc="PostgreSQL → CockroachDB 是 Schema / Operational / Paradigm 三維皆 High 的 multi-axis migration、實證 [#127](/report/content-structure-by-max-diff-dimension/) 的「多重歸類跟 tie-breaking」規則；主結構走 Type E paradigm shift、Schema 差 &#43; Operational redesign 抽出獨立段；涵蓋 transaction model 重設計、SQL dialect gap、5 個 production 踩雷">PostgreSQL → CockroachDB</a></li>
<li>平行 Type B 對照：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a>（保留 paradigm）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</link><pubDate>Tue, 19 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。本文是 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology&lt;/a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 &lt;em>data topology 重劃&lt;/em>、不在 5 type 內。&lt;/p>&lt;/blockquote>
&lt;h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃&lt;/h2>
&lt;p>Migration 通常假設 &lt;em>source 跟 target 是不同 cluster / vendor&lt;/em>；re-sharding 是 &lt;em>同 cluster 內的 slot 重分配&lt;/em>、source 跟 target 是 &lt;em>同一個 Redis Cluster 的不同 state&lt;/em>：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln">1&lt;/span>&lt;span class="cl">Before re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ~ 33% load ~ 50% load ~ 17% load (heavy imbalance)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl">
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl">After re-shard:
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">6&lt;/span>&lt;span class="cl"> Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">7&lt;/span>&lt;span class="cl"> ~ 25% load ~ 25% load ~ 25% load ~ 25% load&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>source 跟 target 是 &lt;em>同 cluster&lt;/em>、區別在 &lt;em>slot 對 node 的 mapping&lt;/em>。Application connection string 不變、cluster API 不變、data model 不變。但 &lt;em>slot migration 期間&lt;/em> application 行為跟 &lt;em>normal operation&lt;/em> 差很多 — 這是 re-sharding 主要工作。&lt;/p>
&lt;p>跑 &lt;a href="https://tarrragon.github.io/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit&lt;/a> 對 Redis cluster re-sharding：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。本文是 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a> 「何時不該套」段的第 3 項實證（容量重新規劃 / re-sharding）— source / target 同 vendor 同 cluster、但 <em>data topology 重劃</em>、不在 5 type 內。</p></blockquote>
<h2 id="source--target但-topology-重劃">Source = Target，但 topology 重劃</h2>
<p>Migration 通常假設 <em>source 跟 target 是不同 cluster / vendor</em>；re-sharding 是 <em>同 cluster 內的 slot 重分配</em>、source 跟 target 是 <em>同一個 Redis Cluster 的不同 state</em>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Before re-shard:
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Cluster A: [node1: slots 0-5460] [node2: slots 5461-10921] [node3: slots 10922-16383]
</span></span><span class="line"><span class="ln">3</span><span class="cl">              ~ 33% load           ~ 50% load              ~ 17% load (heavy imbalance)
</span></span><span class="line"><span class="ln">4</span><span class="cl">
</span></span><span class="line"><span class="ln">5</span><span class="cl">After re-shard:
</span></span><span class="line"><span class="ln">6</span><span class="cl">  Cluster A: [node1: slots 0-4095] [node2: slots 4096-8191] [node3: slots 8192-12287] [node4: slots 12288-16383]
</span></span><span class="line"><span class="ln">7</span><span class="cl">              ~ 25% load           ~ 25% load              ~ 25% load              ~ 25% load</span></span></code></pre></div><p>source 跟 target 是 <em>同 cluster</em>、區別在 <em>slot 對 node 的 mapping</em>。Application connection string 不變、cluster API 不變、data model 不變。但 <em>slot migration 期間</em> application 行為跟 <em>normal operation</em> 差很多 — 這是 re-sharding 主要工作。</p>
<p>跑 <a href="/blog/report/content-structure-by-max-diff-dimension/" data-link-title="Process content 結構由最大差異維度決定、不是 universal phased" data-link-desc="跨 X process content（migration / upgrade / rollout / playbook）的結構由 source / target 之間 *差異維度組合* 決定、不存在 universal phased 模板；6 種 migration / process type 實證（schema 差 / drop-in / operational / multi-tool / paradigm / topology re-layout）跑出 6 種不同結構；寫作前必須做 *6 維 diff dimension audit* 才能決定結構、跳過會套錯模板">diff dimension audit</a> 對 Redis cluster re-sharding：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 Redis、無變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>同 Redis Cluster、operational 不變</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>同 Redis Cluster、無 paradigm 差</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>同 1 個（cluster）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>多數不改、client cluster mode 自處理</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>重劃</strong> — slot mapping 跟 node 數</td>
          <td><strong>New axis</strong></td>
      </tr>
  </tbody>
</table>
<p>5 維皆 Low、對映 Type B drop-in；但 <em>data topology</em> 是 5 type 沒有的 <em>第 6 維度</em>。本文採用 <em>re-sharding-specific 結構</em>、不是 5 type 任一個。</p>
<h2 id="4-種-re-sharding-driver">4 種 re-sharding driver</h2>
<p>不同 driver 對應不同 re-sharding 策略：</p>
<table>
  <thead>
      <tr>
          <th>Driver</th>
          <th>觸發場景</th>
          <th>對應 re-sharding 操作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot imbalance</td>
          <td>業務熱點打到部分 slot、單 node CPU / memory 80%+</td>
          <td>Rebalance（slot 重分配、不加 node）</td>
      </tr>
      <tr>
          <td>Capacity expansion</td>
          <td>整 cluster memory / throughput 上限快到、要加 node</td>
          <td>Add node + slot migration（從現有 node 搬部分 slot 過去）</td>
      </tr>
      <tr>
          <td>Node decommission</td>
          <td>老 node 硬體淘汰 / cloud instance 換代</td>
          <td>Drain（該 node 的 slot 全搬走）+ remove</td>
      </tr>
      <tr>
          <td>Hash tag refactor</td>
          <td>業務 access pattern 變、需要 co-located key 群重分組</td>
          <td>Application-side migration（不是 cluster-level）</td>
      </tr>
  </tbody>
</table>
<p>前 3 種是 cluster-internal、用 <code>redis-cli --cluster</code> 工具完成；第 4 種需要 application 端 dual-write + migration、本文不展開。</p>
<h2 id="slot-migration-機制">Slot migration 機制</h2>
<p>Redis Cluster 16384 個 slot、每個 key 經 <code>CRC16(key) % 16384</code> 對應 slot。Slot migration 過程：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">Source node:     [slot N: MIGRATING to dest]
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">Dest node:       [slot N: IMPORTING from source]
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">Source node:     SCAN slot N → for each key:
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">                 1. DUMP key (serialize value)
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">                 2. send to dest via MIGRATE command
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">                 3. dest RESTORE key
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">                 4. source DEL key
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln">10</span><span class="cl">Source node:     [slot N: OWNED by dest]
</span></span><span class="line"><span class="ln">11</span><span class="cl">Dest node:       [slot N: OWNED]
</span></span><span class="line"><span class="ln">12</span><span class="cl">                 ↓
</span></span><span class="line"><span class="ln">13</span><span class="cl">跨 cluster broadcast: slot N 屬於 dest</span></span></code></pre></div><p>期間 client 行為：</p>
<ul>
<li>Key 在 source 端（未 migrate）：source 直接 serve</li>
<li>Key 在 dest 端（已 migrate）：source 回 <code>-ASK</code> redirect、client 重發到 dest</li>
<li>寫入 MIGRATING slot 的新 key：source serve、之後也會 migrate</li>
<li>Application 不需要改 code、cluster-aware client 自動處理 <code>-ASK</code> redirect</li>
</ul>
<h2 id="redis-cli-cluster-工具">redis-cli &ndash;cluster 工具</h2>
<p>production 用 official tool、不要手寫 slot migration：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. Rebalance（slot 重分配、適合 imbalance）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">redis-cli --cluster rebalance 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="se"></span>  --cluster-use-empty-masters <span class="se">\
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="se"></span>  --cluster-threshold <span class="m">5</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 2. Reshard（指定來源 → 目標、適合 capacity expansion）</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">redis-cli --cluster reshard 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="se"></span>  --cluster-from &lt;source-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="se"></span>  --cluster-to &lt;dest-node-id&gt; <span class="se">\
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="se"></span>  --cluster-slots <span class="m">4096</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="se"></span>  --cluster-yes
</span></span><span class="line"><span class="ln">12</span><span class="cl">
</span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1"># 3. Add-node（加新 node 進 cluster）</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli --cluster add-node 10.0.0.4:6379 10.0.0.1:6379 <span class="se">\
</span></span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="se"></span>  --cluster-master-id &lt;existing-master-id&gt;
</span></span><span class="line"><span class="ln">16</span><span class="cl">
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="c1"># 4. Del-node（移除 node、需先 drain slot）</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">redis-cli --cluster del-node 10.0.0.1:6379 &lt;node-to-remove&gt;</span></span></code></pre></div><p>關鍵：</p>
<ul>
<li><code>--cluster-threshold 5</code>：load 差異超過 5% 才 rebalance、避免反覆觸發</li>
<li><code>--cluster-slots</code>：一次 migrate 多少 slot；太大 lock 久、太小步驟多</li>
<li>Rebalance / reshard 過程 cluster 仍 serve traffic、但 <em>latency 升高</em>（migration overhead）</li>
</ul>
<h2 id="5-段執行流程">5 段執行流程</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">1. Pre-resharding analysis
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">   - 當前 slot 分佈跟 load
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">   - Hot key 識別（CLUSTER COUNTKEYSINSLOT）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">   - 預估 migration 時間
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">2. Backup checkpoint
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">   - BGSAVE on all master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">   - 確認 replica 跟得上（replication offset diff &lt; 10MB）
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">3. Execute re-sharding
</span></span><span class="line"><span class="ln">11</span><span class="cl">   - 用 redis-cli --cluster 工具
</span></span><span class="line"><span class="ln">12</span><span class="cl">   - Monitor cluster health（CLUSTER INFO + CLUSTER NODES）
</span></span><span class="line"><span class="ln">13</span><span class="cl">   - Migration 期間 application 端 latency baseline 比對
</span></span><span class="line"><span class="ln">14</span><span class="cl">
</span></span><span class="line"><span class="ln">15</span><span class="cl">4. Verify
</span></span><span class="line"><span class="ln">16</span><span class="cl">   - Slot distribution 對 expected mapping
</span></span><span class="line"><span class="ln">17</span><span class="cl">   - Application traffic pattern 對 baseline
</span></span><span class="line"><span class="ln">18</span><span class="cl">   - 跑 cross-node sanity check
</span></span><span class="line"><span class="ln">19</span><span class="cl">
</span></span><span class="line"><span class="ln">20</span><span class="cl">5. Cleanup
</span></span><span class="line"><span class="ln">21</span><span class="cl">   - 舊 node（若 decommission）reset / 釋放
</span></span><span class="line"><span class="ln">22</span><span class="cl">   - Monitoring dashboard 更新 (Prometheus target / Grafana panel)
</span></span><span class="line"><span class="ln">23</span><span class="cl">   - Document new topology</span></span></code></pre></div><p>整體 1-7 天、依 cluster 大小（10GB ~ 1 小時、TB 級 1-3 天）。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1cluster-busy-期間-application-timeout">Case 1：Cluster busy 期間 application timeout</h3>
<p><strong>徵兆</strong>：re-sharding 跑到一半、application 端開始大量 <code>CLUSTER BUSY</code> error / <code>OOM</code> warning / latency p99 從 5ms 跳到 200-2000ms；某些 batch operation 完全失敗。</p>
<p><strong>根因</strong>：MIGRATE command 對單 key 是 <em>blocking</em>（DUMP + send + RESTORE + DEL atomic）— 大 value（HASH / SORTED SET / LIST 含 100K+ entry）migration 可能 lock node 數秒；同期間其他 query 阻塞。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：<code>MEMORY USAGE</code> 跑 sample key、找 &gt; 1MB 的 <em>fat key</em>、列出單獨處理</li>
<li><strong>MIGRATE timeout 調</strong>：<code>redis.conf</code> 設 <code>cluster-migration-timeout 10000</code>（10s）、避免單 key migration 卡爆 cluster</li>
<li><strong>降低並行</strong>：<code>--cluster-pipeline 1</code> 一次只搬一個 slot（預設 10）、減少 CPU 壓力</li>
<li><strong>Fat key refactor</strong>：production 不該有 1M+ entry 的 collection、refactor 拆分</li>
</ol>
<h3 id="case-2replica-lag-during-re-sharding">Case 2：Replica lag during re-sharding</h3>
<p><strong>徵兆</strong>：reshard 完成後、replica 顯示 stale data 數分鐘、application 端 read from replica 拿到舊值。</p>
<p><strong>根因</strong>：master 端 slot migration 產生大量 <code>DEL</code> + <code>RESTORE</code> 命令、replication stream 量爆、replica 跟不上、accumulated lag。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding 確認 replica lag &lt; 5MB</strong>、否則先 fix replica issue 再開始</li>
<li><strong>Throttle migration</strong>：用 <code>--cluster-replace</code> + lower pipeline、放慢 master 寫入速度</li>
<li><strong>Application 端 read-write split policy</strong>：reshard 期間強制 read from master、暫時放棄 replica read</li>
<li><strong>預備計畫</strong>：若 lag &gt; 30s 撐了 5+ 分鐘、考慮暫停 reshard、wait replica catch up</li>
</ol>
<h3 id="case-3client-side-topology-cache-stale">Case 3：Client-side topology cache stale</h3>
<p><strong>徵兆</strong>：reshard 完、application 端持續報 <code>MOVED &lt;slot&gt; &lt;new-node&gt;</code> redirect、但隔 30s 又 redirect 一次；某些 client 直接 connection refused（連到已 decommission node）。</p>
<p><strong>根因</strong>：cluster-aware client（lettuce / Jedis cluster mode）有 <em>topology cache</em>、reshard 後不主動 refresh；遇 MOVED 後 refresh 一次、但 cache TTL 內可能繼續用舊 mapping。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Client config</strong>：lettuce <code>clusterTopologyRefreshOptions(...)</code> 設較短 refresh interval（60s）+ <code>enablePeriodicRefresh()</code></li>
<li><strong>Reshard 完後 trigger refresh</strong>：application 端可主動發 <code>CLUSTER NODES</code> 拿最新 topology、不依賴 client lib 自動 refresh</li>
<li><strong>Graceful client shutdown / restart</strong>：對 latency-sensitive 服務、reshard 完 rolling restart application pod、避免 stale cache</li>
<li><strong>Decommissioned node 保留 5 分鐘</strong>：不立刻 stop node、給 stale client 自然 retry 機會</li>
</ol>
<h3 id="case-4cross-slot-transaction-失敗">Case 4：Cross-slot transaction 失敗</h3>
<p><strong>徵兆</strong>：application 用 <code>MULTI/EXEC</code> 跨多 key、reshard 期間部分 transaction 報 <code>MOVED</code> error、整個 transaction 失敗、business logic 不一致。</p>
<p><strong>根因</strong>：Redis Cluster transaction 要求 <em>所有 key 在同 slot</em>（用 hash tag <code>{user:123}</code>）；reshard 期間如果 transaction 內某 key migrate 到 dest、cluster topology 暫時 inconsistent、transaction 拒絕。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Pre-resharding audit</strong>：grep application code 找 MULTI / pipeline 使用、確認所有都用 hash tag co-locate</li>
<li><strong>Reshard 期間 application 端加 retry</strong>：transaction failure 後 backoff retry、cluster stabilize 後成功</li>
<li><strong>架構</strong>：transaction-heavy 場景考慮不用 Redis Cluster、用 Redis Sentinel single master（無 slot 概念）</li>
</ol>
<h3 id="case-5monitor-visibility-gap-during-reshard">Case 5：Monitor visibility gap during reshard</h3>
<p><strong>徵兆</strong>：reshard 期間 Prometheus dashboard 對某 node 的 metric 突然顯示 <em>錯位</em> — load = 95% 但 slot count 顯示 6% slot；SOC 不知道 node 健康狀況。</p>
<p><strong>根因</strong>：Prometheus exporter 對 <em>slot count</em> 跟 <em>traffic load</em> 分開計算；reshard 期間 slot count 已 migrate 但流量仍打 source node（client cache stale）— metric 看似矛盾。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>Reshard 期間關 alert</strong>：knownmaintenance window、Prometheus silence alert</li>
<li><strong>加 reshard-aware metric</strong>：用 <code>redis_cluster_migration_slots</code> 量化 in-flight migration</li>
<li><strong>Dashboard 加註解</strong>：reshard 期間 SOC 看 dashboard 知道是 <em>normal anomaly</em></li>
</ol>
<h2 id="capacity--cost">Capacity / cost</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>估算</th>
          <th>警戒</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Slot migration 速度</td>
          <td>1-10K key / sec（依 key size + network）</td>
          <td>TB 級 10K key / sec → 1 天</td>
      </tr>
      <tr>
          <td>Application latency impact</td>
          <td>p99 +50-200% during migration</td>
          <td>設 latency budget、超出暫停</td>
      </tr>
      <tr>
          <td>Memory / node</td>
          <td>不變、但 temporary 雙寫期間 +5-15%</td>
          <td>不能在 memory 90%+ 時 reshard</td>
      </tr>
      <tr>
          <td>Network bandwidth</td>
          <td>跨 node 大流量、~100-500 Mbps per migration stream</td>
          <td>跨 AZ reshard egress cost 注意</td>
      </tr>
      <tr>
          <td>Recovery time</td>
          <td>Reshard 失敗回退 = 反向 reshard（時間相同）</td>
          <td>不能在 incident 期間 reshard</td>
      </tr>
  </tbody>
</table>
<p>實務 default：</p>
<ul>
<li>跑在 <em>低流量時段</em>（夜間 / 週末）</li>
<li>Throughput 容忍度 &lt; 50% 再 reshard、不要 80%+ 時操作</li>
<li>預留 <em>回退 window</em> — reshard 卡住時能 abort + 恢復原狀</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="跟-redis--dragonflydb-migration-對位">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB migration</a> 對位</h3>
<p>DragonflyDB 設計上 <em>單機效能取代 cluster</em>、re-sharding 議題消失；如果 cluster re-sharding 頻繁觸發、評估直接遷 DragonflyDB 是否更便宜。</p>
<h3 id="跟-sentinel-ha-對比">跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Sentinel HA</a> 對比</h3>
<p>Sentinel 模式無 slot 概念、re-sharding 不適用；但 <em>manual sharding by application</em> 場景仍可能需要類似 topology re-layout、application 端要自己處理。</p>
<h3 id="跟-redis-7-function--cluster-v2">跟 Redis 7+ Function / Cluster v2</h3>
<p>Redis 7 推 Cluster v2 跟 Functions、slot migration 機制部分升級；keyspace migration 仍是核心議題、但 API 跟 monitoring 改進。</p>
<h3 id="下一步議題">下一步議題</h3>
<ul>
<li><strong>Auto-rebalance via operator</strong>：Redis Enterprise / Aiven 等 managed Redis 提供自動 rebalance、不需手動觸發</li>
<li><strong>Cross-DC slot migration</strong>：跨 region cluster slot migration 對 latency / cost 影響大、通常用 <em>application-level sharding</em> 取代 cluster-level</li>
<li><strong>Hash tag 治理</strong>：application code grep / lint 強制 hash tag、避免 cross-slot transaction 反模式</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>平行 migration playbook：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-dragonflydb/" data-link-title="Redis → DragonflyDB：drop-in 相容下的容量躍升 &#43; 5 個踩雷" data-link-desc="DragonflyDB 號稱 Redis drop-in 替代、單機 throughput 25x、記憶體效率 30% 提升；遷移流程簡單但有 5 個 production 踩雷（RDB 版本差 / Lua 腳本不全支援 / Pub-Sub fanout 行為差異 / Cluster mode 兼容度 / Modules 不支援）、跟 Sentinel / Cluster 模式對位">Redis → DragonflyDB</a></li>
<li>對位 deep article：<a href="/blog/backend/01-database/vendors/postgresql/major-version-upgrade/" data-link-title="PostgreSQL major version upgrade (14 → 17)：為什麼這篇不套 5 type migration" data-link-desc="PostgreSQL major version upgrade 是 *5 type 漏類* 的實證 — source/target 同 vendor、5 維度都 Low 但 *upgrade-specific audit* 是核心；本文結構接近 deep article methodology 的 6-section &#43; 額外 upgrade audit 段；涵蓋 pg_upgrade / logical replication / blue-green 三方法、extension 相容性、5 production 踩雷">PostgreSQL major version upgrade</a>（另一個 5 type 漏類驗證）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章的寫作方法論</a> / <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（本文驗證 <em>容量重劃漏類</em>）</li>
</ul>
]]></content:encoded></item><item><title>Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM&lt;/h2>
&lt;p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 &lt;code>OOM command not allowed when used memory &amp;gt; 'maxmemory'&lt;/code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。&lt;/p>
&lt;p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：&lt;code>maxmemory&lt;/code> 設多少、&lt;code>maxmemory-policy&lt;/code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。&lt;/p>
&lt;p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。&lt;/p>
&lt;h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型&lt;/h2>
&lt;p>要調校記憶體，先要分清楚 &lt;code>used_memory&lt;/code> 這個數字到底由什麼組成。&lt;code>INFO memory&lt;/code> 回報的是幾層疊加的記憶體會計，每一層去處不同：&lt;/p>
&lt;p>&lt;strong>&lt;code>used_memory&lt;/code>&lt;/strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。&lt;strong>&lt;code>used_memory_rss&lt;/code>&lt;/strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 &lt;code>used_memory&lt;/code>——兩者的比值就是 &lt;code>mem_fragmentation_ratio&lt;/code>。&lt;strong>&lt;code>used_memory_dataset&lt;/code>&lt;/strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。&lt;/p>
&lt;p>理解三個跟 OOM 直接相關的記憶體去處：&lt;/p>
&lt;p>&lt;strong>資料本身的編碼會放大或縮小記憶體&lt;/strong>。一個小 hash（field 數少於 &lt;code>hash-max-listpack-entries&lt;/code>、value 短於 &lt;code>hash-max-listpack-value&lt;/code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。&lt;/p>
&lt;p>&lt;strong>client output buffer 不計入 dataset 但會吃光記憶體&lt;/strong>。慢速 consumer、&lt;code>MONITOR&lt;/code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。&lt;code>client-output-buffer-limit&lt;/code> 沒設好，一個讀很慢的 replica 或一個掛著的 &lt;code>MONITOR&lt;/code> 連線就能把記憶體推到 maxmemory。&lt;/p>
&lt;p>&lt;strong>fork 期間記憶體會短暫翻倍&lt;/strong>。RDB save 與 AOF rewrite 都靠 &lt;code>fork()&lt;/code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article&lt;/a>。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。選型層（Redis vs Valkey vs Memcached）見 overview；本文只處理「已經選了 Redis、記憶體怎麼配才不會在尖峰爆掉」。配置以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/optimization/memory-optimization/">Redis 官方 memory optimization 文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="你的-redis-會在凌晨三點-oom">你的 Redis 會在凌晨三點 OOM</h2>
<p>Redis 的記憶體問題很少在有人盯著儀表板時發生。它發生在流量爬升、某個 key 集合悄悄長大、AOF rewrite 剛好撞上 RDB save 的那個瞬間——通常是凌晨三點，沒人盯著。徵兆是 application 端突然一片 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，所有寫入失敗，但讀取還活著，於是監控的「Redis 還在回應」綠燈騙過了 on-call。</p>
<p>這類事故的根因幾乎都不是「Redis 不夠快」，而是三個記憶體旋鈕在設計時被當成預設值放著沒動：<code>maxmemory</code> 設多少、<code>maxmemory-policy</code> 選哪個、以及沒人注意到的記憶體碎片化。這三個旋鈕決定了 Redis 在記憶體壓力下是「優雅地淘汰冷資料繼續服務」還是「拒絕所有寫入直到有人重啟」。本文處理這三者的會計模型、選型判讀，以及它們怎麼被寫成事故。</p>
<p>對延遲就是業務 KPI 的服務，這個旋鈕的代價更直接。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動要查多個快取（profile、距離、偏好、推薦池），4700 萬月活下 cache 不是 DB 的補救、是主要服務面，cache miss 是邊緣案例。eviction policy 選錯，淘汰掉的若是熱資料，下一次滑動就打回 origin，sub-millisecond 的延遲預算瞬間破表。</p>
<h2 id="核心概念redis-記憶體的會計模型">核心概念：Redis 記憶體的會計模型</h2>
<p>要調校記憶體，先要分清楚 <code>used_memory</code> 這個數字到底由什麼組成。<code>INFO memory</code> 回報的是幾層疊加的記憶體會計，每一層去處不同：</p>
<p><strong><code>used_memory</code></strong> 是 Redis allocator（預設 jemalloc）配給資料、結構與 buffer 的總量。<strong><code>used_memory_rss</code></strong> 是作業系統視角看到的 Redis 進程實體記憶體（resident set size），它通常大於 <code>used_memory</code>——兩者的比值就是 <code>mem_fragmentation_ratio</code>。<strong><code>used_memory_dataset</code></strong> 才是純資料的部分，扣掉了 Redis 自身的 overhead。</p>
<p>理解三個跟 OOM 直接相關的記憶體去處：</p>
<p><strong>資料本身的編碼會放大或縮小記憶體</strong>。一個小 hash（field 數少於 <code>hash-max-listpack-entries</code>、value 短於 <code>hash-max-listpack-value</code>）用 listpack 緊湊編碼，記憶體可能只有大 hash 用 hashtable 編碼的幾分之一。同樣的邏輯套用在 list、set、sorted set。一個欄位設計的小決定（把 user object 拆成 200 個獨立 key 還是壓成一個 hash）會讓記憶體差好幾倍。</p>
<p><strong>client output buffer 不計入 dataset 但會吃光記憶體</strong>。慢速 consumer、<code>MONITOR</code>、大量 pub/sub 訂閱者都會讓 Redis 在 server 端堆積 reply buffer。<code>client-output-buffer-limit</code> 沒設好，一個讀很慢的 replica 或一個掛著的 <code>MONITOR</code> 連線就能把記憶體推到 maxmemory。</p>
<p><strong>fork 期間記憶體會短暫翻倍</strong>。RDB save 與 AOF rewrite 都靠 <code>fork()</code> + copy-on-write，父進程在 fork 後若持續寫入，被改動的 page 會被複製，最壞情況記憶體接近翻倍。這是 maxmemory 必須留 headroom 的核心原因，細節見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency deep article</a>。</p>
<p><code>maxmemory</code> 框住的是 <code>used_memory</code>，不是 <code>used_memory_rss</code>。所以 maxmemory 設成機器 RAM 的 100% 是錯的——碎片化、fork copy-on-write、client buffer 都在 maxmemory 之外，會把 RSS 推爆系統，觸發 Linux OOM killer 直接砍掉 Redis 進程（比 Redis 自己的 noeviction 更糟，因為是無預警 SIGKILL）。</p>
<h2 id="配置maxmemory-與-policy-的設定路徑">配置：maxmemory 與 policy 的設定路徑</h2>
<p>設定分兩步：先框住記憶體上限，再決定撞到上限時的行為。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 1. 設定記憶體上限（留 headroom 給 fork / fragmentation / client buffer）</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 機器 RAM 8GB → maxmemory 設 ~5-6GB、留 25-35% headroom</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">redis-cli CONFIG SET maxmemory 6gb
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 2. 設定撞到上限時的淘汰行為</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">redis-cli CONFIG SET maxmemory-policy allkeys-lfu
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># 3. 永久化到 redis.conf（CONFIG SET 重啟後失效）</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1">#   maxmemory 6gb</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1">#   maxmemory-policy allkeys-lfu</span></span></span></code></pre></div><p>八個 <code>maxmemory-policy</code> 選項分三類，選型靠「資料是不是全部都能淘汰」與「淘汰要靠存取頻率還是 TTL」兩個問題：</p>
<table>
  <thead>
      <tr>
          <th>policy</th>
          <th>淘汰範圍</th>
          <th>淘汰依據</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>noeviction</code></td>
          <td>不淘汰</td>
          <td>寫入直接報錯</td>
          <td>資料是 source-of-truth、不能丟（少見）</td>
      </tr>
      <tr>
          <td><code>allkeys-lru</code></td>
          <td>所有 key</td>
          <td>最近最少使用</td>
          <td>純 cache、無法預判哪些該留</td>
      </tr>
      <tr>
          <td><code>allkeys-lfu</code></td>
          <td>所有 key</td>
          <td>最少使用頻率</td>
          <td>純 cache、有明顯熱資料（多數 cache 場景）</td>
      </tr>
      <tr>
          <td><code>allkeys-random</code></td>
          <td>所有 key</td>
          <td>隨機</td>
          <td>key 存取均勻、省 LRU/LFU 計算</td>
      </tr>
      <tr>
          <td><code>volatile-lru</code></td>
          <td>有 TTL 的 key</td>
          <td>最近最少使用</td>
          <td>cache 與持久資料混存、只淘汰可過期的</td>
      </tr>
      <tr>
          <td><code>volatile-lfu</code></td>
          <td>有 TTL 的 key</td>
          <td>最少使用頻率</td>
          <td>同上、有熱資料</td>
      </tr>
      <tr>
          <td><code>volatile-random</code></td>
          <td>有 TTL 的 key</td>
          <td>隨機</td>
          <td>同上、省計算</td>
      </tr>
      <tr>
          <td><code>volatile-ttl</code></td>
          <td>有 TTL 的 key</td>
          <td>最接近過期的先淘汰</td>
          <td>想讓近期過期的提早讓位</td>
      </tr>
  </tbody>
</table>
<h3 id="lru-跟-lfu-的真實差異">LRU 跟 LFU 的真實差異</h3>
<p><code>allkeys-lru</code> 跟 <code>allkeys-lfu</code> 看起來像同一件事的兩種寫法，但選錯會在特定 workload 下讓 hit rate 掉一截。LRU 看「最後一次被存取是多久以前」，LFU 看「被存取的頻率」。差別在一次性掃描（scan pollution）：某個批次任務一次讀過大量冷 key，LRU 會把這些剛被碰過的冷 key 排到淘汰隊伍最後面，反而把真正的熱 key 擠出去。LFU 因為看頻率，一次性的存取不會讓冷 key 假裝成熱 key。</p>
<p>Redis 4.0 後的 LFU 用的是 probabilistic counter（Morris counter）加 decay，不是精確計數，靠兩個參數調：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># lfu-log-factor：counter 增長的對數速度、越大越能區分高頻 key</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET lfu-log-factor <span class="m">10</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># lfu-decay-time：counter 衰減的分鐘數、越小越快遺忘舊熱度</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">redis-cli CONFIG SET lfu-decay-time <span class="m">1</span></span></span></code></pre></div><p>對 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 這類有明顯熱資料</a>（熱門 profile、熱區域推薦池）的服務，<code>allkeys-lfu</code> 比 <code>allkeys-lru</code> 更能保護熱 key 不被批次掃描或冷流量擠出。</p>
<h3 id="approximate-eviction-的取樣">approximate eviction 的取樣</h3>
<p>Redis 的 LRU/LFU 都是近似演算法，不掃全 keyspace，而是每次取樣 <code>maxmemory-samples</code> 個 key（預設 5）挑最該淘汰的。樣本數越大越接近精確 LRU/LFU，但越吃 CPU。記憶體壓力大、淘汰頻繁時，預設 5 已夠；要更精準可調到 10，代價是淘汰路徑的 CPU 上升。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1noeviction-讓寫入全滅讀取假裝健康">Case 1：noeviction 讓寫入全滅、讀取假裝健康</h3>
<p><strong>徵兆</strong>：application 寫入路徑大量 <code>OOM command not allowed when used memory &gt; 'maxmemory'</code>，但 <code>GET</code> 仍正常、health check（通常打 <code>PING</code> 或 <code>GET</code>）綠燈，on-call 收到的是 application 層的 500、不是 Redis 告警。</p>
<p><strong>根因</strong>：<code>maxmemory-policy</code> 預設是 <code>noeviction</code>。當 Redis 把 cache 當 cache 用，但 policy 留在 <code>noeviction</code>，記憶體一滿，所有會增加記憶體的命令（<code>SET</code>、<code>LPUSH</code>、<code>HSET</code>）直接報錯，唯讀命令照常。health check 若只測讀取，完全偵測不到。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>純 cache 場景把 policy 改成 <code>allkeys-lru</code> 或 <code>allkeys-lfu</code>，讓記憶體壓力自動透過淘汰釋放</li>
<li>health check 加一個寫入探針（<code>SET healthcheck:probe &lt;ts&gt; EX 10</code>），讓 OOM 寫入失敗能被偵測</li>
<li>告警掛在 <code>used_memory / maxmemory &gt; 0.85</code>，不要等 OOM 才反應</li>
<li>若資料真的不能淘汰（誤把 Redis 當 source-of-truth），那不該用 cache 配置，見本文 Capacity / cost 邊界段的路由判斷</li>
</ol>
<h3 id="case-2碎片化吃掉-30-記憶體">Case 2：碎片化吃掉 30% 記憶體</h3>
<p><strong>徵兆</strong>：<code>used_memory</code> 顯示 4GB、但 <code>used_memory_rss</code> 是 5.5GB，<code>mem_fragmentation_ratio</code> 是 1.37，機器 RAM 開始吃緊但資料量沒漲。重啟 Redis 後 RSS 掉回 4GB 出頭。</p>
<p><strong>根因</strong>：大量寫入後刪除、或 value 大小頻繁變動（例如 list 一直 push/pop），jemalloc 的記憶體頁出現空洞——配出去的 page 還佔著 RSS，但裡面只有零星資料。<code>mem_fragmentation_ratio</code> 持續 &gt; 1.5 是明確訊號。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>開 active defrag 讓 Redis 在背景整理（4.0+）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli CONFIG SET activedefrag yes
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET active-defrag-ignore-bytes 100mb
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET active-defrag-threshold-lower <span class="m">10</span></span></span></code></pre></div></li>
<li>
<p>fragmentation_ratio &lt; 1.0 是另一種警訊——代表 Redis 在用 swap，比碎片化更危險，要立刻降記憶體壓力</p>
</li>
<li>
<p>結構選擇上避免大幅波動的 collection；穩態大小的資料碎片化天然較低</p>
</li>
<li>
<p>計算 maxmemory headroom 時把 1.2-1.4 的 fragmentation 算進去</p>
</li>
</ol>
<h3 id="case-3一個-monitor-連線把記憶體推爆">Case 3：一個 MONITOR 連線把記憶體推爆</h3>
<p><strong>徵兆</strong>：某次 debug 後記憶體莫名持續上升，<code>used_memory_dataset</code> 沒變但 <code>used_memory</code> 一直漲，<code>CLIENT LIST</code> 看到一個連線的 <code>omem</code>（output buffer memory）有幾百 MB。</p>
<p><strong>根因</strong>：有人開了 <code>MONITOR</code> 去看即時命令流、然後忘了關（或 client crash 但連線沒斷）。<code>MONITOR</code> 把每一條命令都推給該連線，高 QPS 下 server 端 output buffer 爆量堆積，計入 <code>used_memory</code> 但不在 dataset。慢速 replica 或大量 pub/sub 訂閱者也會觸發同類問題。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>設定 client output buffer 上限，超過就斷線：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># normal client / replica / pubsub 分開設</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;normal 256mb 64mb 60&#34;</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET client-output-buffer-limit <span class="s2">&#34;pubsub 32mb 8mb 60&#34;</span></span></span></code></pre></div></li>
<li>
<p><code>MONITOR</code> 在 production 嚴格禁用或限時，它本身也拖慢整個 server</p>
</li>
<li>
<p>監控加 <code>CLIENT LIST</code> 的 <code>omem</code> 巡檢，找出異常 buffer 的連線</p>
</li>
<li>
<p>replica lag 過大時 output buffer 會堆，對應 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">replication / failover deep article</a></p>
</li>
</ol>
<h3 id="case-4欄位設計讓記憶體多用三倍">Case 4：欄位設計讓記憶體多用三倍</h3>
<p><strong>徵兆</strong>：資料筆數跟預估一致，但 <code>used_memory</code> 是試算的 3 倍。<code>MEMORY USAGE &lt;key&gt;</code> 抽樣發現單筆 object 的記憶體遠超 value 本身的 byte 數。</p>
<p><strong>根因</strong>：把一個有 10 個欄位的 user object 拆成 10 個獨立 string key（<code>user:123:name</code>、<code>user:123:age</code>&hellip;），每個 key 都帶 Redis 的 key overhead（dict entry、expire dict entry、key 字串本身）。10 個 key 的 overhead 是一個 hash 的好幾倍。反過來，超過 <code>hash-max-listpack-entries</code> 的大 hash 從緊湊的 listpack 退化成 hashtable 編碼，也會放大記憶體。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>
<p>同一 entity 的欄位用一個 hash 存，共享 key overhead</p>
</li>
<li>
<p>保持 hash 在 listpack 閾值內以用緊湊編碼：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-entries  <span class="c1"># 預設 128</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG GET hash-max-listpack-value    <span class="c1"># 預設 64</span></span></span></code></pre></div></li>
<li>
<p>用 <code>MEMORY USAGE &lt;key&gt;</code> 跟 <code>redis-cli --bigkeys</code> 抽樣驗證實際記憶體，不靠試算</p>
</li>
<li>
<p><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">Shopify 的 serialization 遷移</a>（Marshal → MessagePack）正是用更省的編碼壓 payload，欄位編碼決策對記憶體與頻寬同時有效</p>
</li>
</ol>
<h3 id="case-5淘汰把熱-key-一起帶走hit-rate-崩">Case 5：淘汰把熱 key 一起帶走、hit rate 崩</h3>
<p><strong>徵兆</strong>：記憶體壓力下開始 eviction（<code>evicted_keys</code> 持續上升），同時 <code>keyspace_hits / (hits + misses)</code> 從 95% 掉到 70%，origin QPS 跟著飆，下游 DB 開始吃緊。</p>
<p><strong>根因</strong>：用了 <code>allkeys-random</code>，或 <code>allkeys-lru</code> 撞上批次掃描污染，淘汰演算法把熱 key 跟冷 key 一視同仁，熱 key 被淘汰後下一個請求 miss、回源、再寫回，形成淘汰與回填的拉鋸，hit rate 持續惡化。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>有明顯熱資料就用 <code>allkeys-lfu</code>，讓頻率高的 key 留下</li>
<li>把 maxmemory-samples 調到 10 提高淘汰精準度</li>
<li>根因常是記憶體真的不夠——<code>evicted_keys</code> 持續高代表 working set 超過 maxmemory，該擴容或分片，不是純調 policy 能解</li>
<li>熱 key 本身過熱（單 key QPS 遠超其他）要走 local cache + Redis 兩層，對應 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>記憶體配置的容量判讀，核心是「working set 對 maxmemory 的比值」與「淘汰是否健康」：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>used_memory / maxmemory</code></td>
          <td>&lt; 80%</td>
          <td>&gt; 85% 告警、&gt; 95% 接近 OOM 或大量淘汰</td>
      </tr>
      <tr>
          <td><code>mem_fragmentation_ratio</code></td>
          <td>1.0 - 1.5</td>
          <td>&gt; 1.5 開 active defrag、&lt; 1.0 在用 swap 要救火</td>
      </tr>
      <tr>
          <td><code>evicted_keys</code> 速率</td>
          <td>接近 0（working set 放得下）</td>
          <td>持續高 → working set 超量、該擴容 / 分片</td>
      </tr>
      <tr>
          <td>hit rate</td>
          <td>&gt; 90%（多數 cache）</td>
          <td>持續下滑 → 淘汰太兇或 TTL 太短</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足、降 maxmemory</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單機記憶體不夠、working set 持續超量</strong>：垂直擴容（換更大記憶體機型）是第一步，但有單機上限。超過後走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster 分片</a>，把 keyspace 切到多 node。</li>
<li><strong>想用 Redis API 但要極致單機記憶體效率</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 dashtable 在同 dataset 下通常比 Redis 省 20-40% 記憶體（依資料形狀、以官方 benchmark 為準），且單機多核能撐到 Redis 要靠 cluster 才能達到的規模——若 cluster re-sharding 頻繁觸發，評估直接遷 DragonflyDB 是否更省維運。</li>
<li><strong>資料其實不能淘汰（被當 source-of-truth）</strong>：那它不是 cache，該走 durable store。AWS 生態下用 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a>（Redis-compatible durable），或把正式狀態放回 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>記憶體與淘汰是 Redis 運維的第一層旋鈕，但它跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：fork 期間的 copy-on-write 是 maxmemory headroom 的主要消耗者，記憶體調校跟持久化調校必須一起看。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">TTL 與 eviction 概念</a></strong>：TTL 設計決定哪些 key 帶過期時間，直接影響 <code>volatile-*</code> policy 的淘汰範圍。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">cache stampede</a></strong>：大量 key 同時被淘汰或同時過期會引發回源雪崩，eviction 調校要跟 TTL jitter / singleflight 一起設計。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB 遷到 ElastiCache，前提是「feature 可重新計算」——這個判斷決定了 eviction 是可接受的，記憶體調校才有意義。資料若不可重建，問題不在淘汰 policy，在選錯了儲存層。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。持久化跟&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校&lt;/a>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="fork-那一瞬間">fork 那一瞬間&lt;/h2>
&lt;p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 &lt;code>fork()&lt;/code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。&lt;/p>
&lt;p>問題在 &lt;code>fork()&lt;/code> 本身不是免費的。Linux 的 &lt;code>fork()&lt;/code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。&lt;/p>
&lt;p>更糟的是 fork 之後。&lt;code>fork()&lt;/code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。&lt;/p>
&lt;p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。&lt;/p>
&lt;h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意&lt;/h2>
&lt;p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。&lt;/p>
&lt;p>&lt;strong>RDB 是某個時間點的記憶體快照&lt;/strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（&lt;code>dump.rdb&lt;/code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。&lt;/p>
&lt;p>&lt;strong>AOF 是命令的 append-only log&lt;/strong>。每個改變資料的命令（&lt;code>SET&lt;/code>、&lt;code>LPUSH&lt;/code>&amp;hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 &lt;code>fsync&lt;/code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。&lt;/p>
&lt;p>兩者的 fork 觸發點不同但機制相同：RDB 是 &lt;code>BGSAVE&lt;/code>（手動或 save 規則觸發）fork，AOF 是 &lt;code>BGREWRITEAOF&lt;/code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。&lt;/p>
&lt;h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料&lt;/h3>
&lt;p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。&lt;code>appendfsync&lt;/code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>&lt;code>appendfsync&lt;/code>&lt;/th>
 &lt;th>fsync 時機&lt;/th>
 &lt;th>崩潰最多丟&lt;/th>
 &lt;th>延遲影響&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;code>always&lt;/code>&lt;/td>
 &lt;td>每個寫命令&lt;/td>
 &lt;td>幾乎不丟&lt;/td>
 &lt;td>每次寫都等磁碟、延遲最高&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>everysec&lt;/code>&lt;/td>
 &lt;td>每秒一次（背景）&lt;/td>
 &lt;td>最多 1 秒&lt;/td>
 &lt;td>多數場景的平衡點（預設）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;code>no&lt;/code>&lt;/td>
 &lt;td>交給 OS（~30 秒）&lt;/td>
 &lt;td>OS 決定、可能丟很多&lt;/td>
 &lt;td>延遲最低、持久性最弱&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;code>everysec&lt;/code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 &lt;code>always&lt;/code> 一樣明顯。&lt;/p>
&lt;h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail&lt;/h3>
&lt;p>Redis 4.0 後的 &lt;code>aof-use-rdb-preamble yes&lt;/code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。持久化跟<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a>互相耦合（fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者），兩篇建議一起讀。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/persistence/">Redis persistence 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="fork-那一瞬間">fork 那一瞬間</h2>
<p>Redis 是單執行緒處理命令的，這是它延遲可預測的根基——直到它需要把記憶體裡的資料寫到磁碟。RDB snapshot 跟 AOF rewrite 都不能在主執行緒上慢慢做（會凍結所有命令），於是 Redis 的解法是 <code>fork()</code>：複製出一個子進程，由子進程把當下的記憶體快照寫到磁碟，主進程繼續服務。</p>
<p>問題在 <code>fork()</code> 本身不是免費的。Linux 的 <code>fork()</code> 要複製父進程的分頁表（page table），記憶體越大、分頁表越大，這個複製動作越久——而它發生在主執行緒上，是阻塞的。一個 20GB 的 Redis 實例，fork 可能凍結主執行緒數百毫秒到一秒。在這段時間裡，所有命令排隊，p99 延遲從 1ms 跳到 500ms+。</p>
<p>更糟的是 fork 之後。<code>fork()</code> 用 copy-on-write：子進程跟父進程共享實體分頁，直到某一方寫入才複製。子進程只讀（在寫 snapshot），但父進程持續服務寫入，每改一個分頁就觸發一次複製。寫入越密集、snapshot 跑越久，被複製的分頁越多，最壞情況記憶體接近翻倍。這就是為什麼 Redis 的 maxmemory 必須留 headroom——不是給資料，是給 fork 期間的分頁複製。</p>
<p>理解持久化，本質是理解「fork 的延遲尖峰」與「資料持久性」之間的取捨。本文按這條線展開機制、配置與踩坑。</p>
<h2 id="核心概念aof-與-rdb-是兩種不同的持久性語意">核心概念：AOF 與 RDB 是兩種不同的持久性語意</h2>
<p>Redis 的兩種持久化不是「二選一的同類選項」，它們回答的是不同問題。</p>
<p><strong>RDB 是某個時間點的記憶體快照</strong>。它把整個 dataset 序列化成一個緊湊的二進位檔（<code>dump.rdb</code>）。優點是檔案小、還原快（直接載入記憶體）、fork 一次寫完。缺點是兩次 snapshot 之間的寫入會在崩潰時全部遺失——RDB 的持久性顆粒度是「上一次 save 到現在」，可能是幾分鐘的資料。</p>
<p><strong>AOF 是命令的 append-only log</strong>。每個改變資料的命令（<code>SET</code>、<code>LPUSH</code>&hellip;）被追加到 log 檔，還原時重放整個 log。優點是持久性顆粒度細（最多丟 <code>fsync</code> 策略決定的一小段）。缺點是 log 會無限增長，需要定期 rewrite 壓縮——而 rewrite 也要 fork。</p>
<p>兩者的 fork 觸發點不同但機制相同：RDB 是 <code>BGSAVE</code>（手動或 save 規則觸發）fork，AOF 是 <code>BGREWRITEAOF</code>（log 太大時觸發）fork。兩個若同時跑，記憶體壓力疊加。</p>
<h3 id="aof-的-fsync-策略決定丟多少資料">AOF 的 fsync 策略決定丟多少資料</h3>
<p>AOF 寫 log 分兩步：先 write 到 OS 的 page cache，再 fsync 刷到磁碟。<code>appendfsync</code> 控制 fsync 頻率，這是持久性與延遲的核心旋鈕：</p>
<table>
  <thead>
      <tr>
          <th><code>appendfsync</code></th>
          <th>fsync 時機</th>
          <th>崩潰最多丟</th>
          <th>延遲影響</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>always</code></td>
          <td>每個寫命令</td>
          <td>幾乎不丟</td>
          <td>每次寫都等磁碟、延遲最高</td>
      </tr>
      <tr>
          <td><code>everysec</code></td>
          <td>每秒一次（背景）</td>
          <td>最多 1 秒</td>
          <td>多數場景的平衡點（預設）</td>
      </tr>
      <tr>
          <td><code>no</code></td>
          <td>交給 OS（~30 秒）</td>
          <td>OS 決定、可能丟很多</td>
          <td>延遲最低、持久性最弱</td>
      </tr>
  </tbody>
</table>
<p><code>everysec</code> 是多數場景的預設選擇——背景執行緒每秒 fsync，主執行緒不等磁碟，崩潰最多丟 1 秒。但要注意：當磁碟 I/O 飽和，背景 fsync 跑超過 1 秒沒完成，主執行緒會被迫等待（避免 buffer 無限堆積），這時延遲尖峰跟 <code>always</code> 一樣明顯。</p>
<h3 id="混合持久化rdb-preamble--aof-tail">混合持久化：RDB preamble + AOF tail</h3>
<p>Redis 4.0 後的 <code>aof-use-rdb-preamble yes</code>（4.0+ 預設開）把兩者結合：AOF rewrite 時，先寫一段 RDB 格式的快照當前綴，後面接增量命令 log。還原時先快速載入 RDB preamble，再重放尾端的 log。這拿到了 RDB 的還原速度與 AOF 的細顆粒持久性，是目前的建議配置。</p>
<h2 id="配置持久化的設定路徑">配置：持久化的設定路徑</h2>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># --- RDB snapshot 規則（多久 + 多少改動觸發 BGSAVE）---</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># redis.conf:</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1">#   save 900 1      # 900 秒內有 1 個 key 改動</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="c1">#   save 300 100    # 300 秒內有 100 個改動</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1">#   save 60 10000   # 60 秒內有 10000 個改動</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="c1"># 純 cache 不需要 RDB 可關閉：</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1">#   save &#34;&#34;</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># --- AOF 設定 ---</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">redis-cli CONFIG SET appendonly yes
</span></span><span class="line"><span class="ln">11</span><span class="cl">redis-cli CONFIG SET appendfsync everysec
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># AOF rewrite 觸發條件：比上次 rewrite 大 100% 且至少 64MB</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-percentage <span class="m">100</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">redis-cli CONFIG SET auto-aof-rewrite-min-size 64mb
</span></span><span class="line"><span class="ln">15</span><span class="cl"><span class="c1"># 混合持久化（4.0+ 預設）</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">redis-cli CONFIG SET aof-use-rdb-preamble yes</span></span></code></pre></div><p>降低 fork 衝擊的兩個系統層設定：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 1. 關閉 Transparent Huge Pages（THP）——THP 會讓 copy-on-write 以 2MB 為單位複製、放大 fork 後的記憶體與延遲</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="nb">echo</span> never &gt; /sys/kernel/mm/transparent_hugepage/enabled
</span></span><span class="line"><span class="ln">3</span><span class="cl">
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="c1"># 2. 允許 overcommit memory——fork 時 Linux 預設可能因 overcommit 檢查拒絕 fork、導致 BGSAVE 失敗</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># /etc/sysctl.conf:</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="c1">#   vm.overcommit_memory = 1</span></span></span></code></pre></div><p>這兩個是 Redis 官方明確建議的系統設定，沒設好會直接讓 fork 失敗或放大延遲尖峰。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1bgsave-那一刻-p99-延遲尖峰">Case 1：BGSAVE 那一刻 p99 延遲尖峰</h3>
<p><strong>徵兆</strong>：監控上每隔一段時間（對齊 save 規則）出現規律的延遲尖峰，p99 從 2ms 跳到 300-800ms，持續一兩秒後恢復。<code>INFO stats</code> 的 <code>latest_fork_usec</code> 顯示某次 fork 花了 700000 微秒（0.7 秒）。</p>
<p><strong>根因</strong>：大記憶體實例的 <code>fork()</code> 要複製分頁表，這個動作阻塞主執行緒。實例越大尖峰越明顯，THP 開著會更嚴重。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 THP 關閉（最常見的放大原因）</li>
<li>把 RDB save 規則放寬或關閉——純 cache 場景靠 AOF 或乾脆不持久化</li>
<li>大實例考慮分片，把單實例記憶體降下來，fork 成本隨之降低</li>
<li>在 replica 上做持久化（master 只服務、replica 負責 BGSAVE），把 fork 尖峰移出服務路徑</li>
</ol>
<h3 id="case-2fork-期間記憶體翻倍觸發-oom">Case 2：fork 期間記憶體翻倍觸發 OOM</h3>
<p><strong>徵兆</strong>：BGSAVE 開始後記憶體快速上升，<code>used_memory_rss</code> 在 snapshot 期間衝高，撞到機器 RAM 上限，Linux OOM killer 把 redis-server 進程 SIGKILL，無預警下線。</p>
<p><strong>根因</strong>：copy-on-write 在寫入密集期間複製大量分頁，maxmemory 沒留足夠 headroom。maxmemory 設成 RAM 的 90%+ 時，fork 期間的分頁複製把 RSS 推爆系統。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>maxmemory 設成 RAM 的 60-70%，留 30-40% 給 fork copy-on-write（寫入越密集留越多）</li>
<li>設 <code>vm.overcommit_memory = 1</code> 避免 fork 直接被拒</li>
<li>在低寫入時段（夜間）排程 BGSAVE，減少 fork 期間被複製的分頁</li>
<li>監控 <code>latest_fork_usec</code> 與 BGSAVE 期間的 RSS 峰值，跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a>的 headroom 計算合看</li>
</ol>
<h3 id="case-3aof-everysec-在磁碟飽和時退化成-always">Case 3：AOF everysec 在磁碟飽和時退化成 always</h3>
<p><strong>徵兆</strong>：平常延遲穩定，某段時間（通常伴隨大量寫入或磁碟被其他進程佔用）延遲全面上升，<code>INFO</code> 的 <code>aof_delayed_fsync</code> 計數持續增加。</p>
<p><strong>根因</strong>：<code>everysec</code> 的背景 fsync 應該每秒完成，但磁碟 I/O 飽和時 fsync 跑超過 1 秒。Redis 為了不讓 AOF buffer 無限堆積，會在主執行緒上阻塞等 fsync 完成——<code>everysec</code> 在這個情境下退化成接近 <code>always</code> 的延遲行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用獨立的高 IOPS 磁碟給 AOF（不要跟 OS / log / 其他服務共用 I/O）</li>
<li>監控 <code>aof_delayed_fsync</code>，持續增加代表磁碟跟不上寫入</li>
<li>評估 <code>no-appendfsync-on-rewrite yes</code>——AOF rewrite 期間暫停 fsync，避免 rewrite 的 I/O 跟 fsync 互搶（代價是 rewrite 期間崩潰丟更多）</li>
<li>寫入吞吐超過單磁碟負荷是擴容訊號，不是調 fsync 能解</li>
</ol>
<h3 id="case-4aof-檔尾損壞讓-redis-起不來">Case 4：AOF 檔尾損壞讓 Redis 起不來</h3>
<p><strong>徵兆</strong>：Redis 崩潰後重啟失敗，log 顯示 <code>Bad file format reading the append only file</code>，服務無法載入 AOF。</p>
<p><strong>根因</strong>：崩潰發生在 AOF 寫到一半，最後一條命令只寫了部分 byte，AOF 檔尾不完整。Redis 預設 <code>aof-load-truncated yes</code> 應能容忍尾端截斷，但若損壞在中段（罕見的磁碟錯誤）或設了 <code>aof-load-truncated no</code>，載入直接失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>確認 <code>aof-load-truncated yes</code>（預設），容忍尾端截斷自動修復</li>
<li>中段損壞用 <code>redis-check-aof --fix appendonly.aof</code> 修復（會截掉損壞點之後的內容、有資料遺失）</li>
<li>修復前先備份原 AOF 檔，不要直接覆蓋</li>
<li>混合持久化下還原優先用 RDB preamble，降低純 AOF replay 的損壞風險</li>
</ol>
<h3 id="case-5以為有持久化其實-bgsave-一直在失敗">Case 5：以為有持久化、其實 BGSAVE 一直在失敗</h3>
<p><strong>徵兆</strong>：某次需要從 RDB 還原時發現 <code>dump.rdb</code> 是好幾天前的，期間的資料全沒了。回查 log 發現 BGSAVE 一直報 <code>Can't save in background: fork: Cannot allocate memory</code>。</p>
<p><strong>根因</strong>：<code>vm.overcommit_memory</code> 是預設的 0，Linux 在 fork 時做嚴格的記憶體檢查——當 Redis 已用掉大半 RAM，fork 估算可能需要翻倍記憶體而被拒。BGSAVE 靜默失敗，RDB 停留在最後一次成功的版本，但沒人在看 log。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>vm.overcommit_memory = 1</code>，讓 fork 在記憶體吃緊時仍能成功（靠 copy-on-write 實際不會真的翻倍）</li>
<li>監控 <code>rdb_last_bgsave_status</code> 與 <code>aof_last_bgrewrite_status</code>，<code>err</code> 要立刻告警</li>
<li>監控 <code>rdb_last_save_time</code>，距今太久代表持久化已停擺</li>
<li>持久化的存在不等於可用——定期演練從備份還原，驗證 RDB / AOF 真的能載入</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>持久化的容量判讀，圍繞 fork 成本與磁碟負荷：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>latest_fork_usec</code></td>
          <td>&lt; 100ms（小實例）</td>
          <td>&gt; 數百 ms → 實例太大、考慮分片或 replica 持久化</td>
      </tr>
      <tr>
          <td>fork 期間 RSS 峰值</td>
          <td>&lt; 機器 RAM</td>
          <td>接近 RAM → maxmemory headroom 不足</td>
      </tr>
      <tr>
          <td><code>aof_delayed_fsync</code></td>
          <td>接近 0</td>
          <td>持續增加 → 磁碟 I/O 跟不上、換高 IOPS 磁碟</td>
      </tr>
      <tr>
          <td><code>rdb_last_bgsave_status</code></td>
          <td><code>ok</code></td>
          <td><code>err</code> → fork 失敗、查 overcommit / 記憶體</td>
      </tr>
      <tr>
          <td>AOF 檔大小 / dataset</td>
          <td>rewrite 後接近 dataset 大小</td>
          <td>遠大於 dataset → rewrite 沒觸發、檢查閾值</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>fork 尖峰無法接受、實例又必須大</strong>：把持久化移到 replica（master 純服務），或走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>降低單實例記憶體。</li>
<li><strong>大記憶體下 fork 成本是結構性瓶頸</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 用 fork-less snapshot 機制，大記憶體場景的快照不付 fork 的延遲與記憶體翻倍代價——若 fork 尖峰是主要痛點，這是值得評估的架構替代。</li>
<li><strong>需要真正的 source-of-truth 持久性（不是盡力而為）</strong>：Redis 持久化本質是 cache 的回填保險，不是交易級持久性。要強持久性走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a>（multi-AZ transaction log）或 <a href="/blog/backend/01-database/" data-link-title="模組一：資料庫與持久化" data-link-desc="整理 SQL、transaction、migration 與 repository adapter 的後端實務">database 模組</a>。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>持久化決策的起點其實是一個選型問題：這份資料是 cache 還是 source-of-truth。</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：fork 的 copy-on-write 是 maxmemory headroom 的主要消耗者，兩者必須一起算。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">replication / failover</a></strong>：replica 是承接持久化負擔的地方，也是 fork 尖峰的替代執行點。</li>
<li><strong>跟 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">Tubi 的 cache vs durable 選型</a></strong>：Tubi 把 ML feature store 從 ScyllaDB（durable）遷到 ElastiCache，判斷是「feature 可重新計算」——這正是「不需要持久化」的判斷，持久化配置應隨之簡化甚至關閉。反過來，若資料不可重建，問題在選錯儲存層，不在持久化調校。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a></strong>：服務若把 Redis 當主要 serving layer，持久化決定了重啟後是冷啟動回源雪崩還是溫啟動，跟 stampede 防護直接相關。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 cache copy boundary</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding&lt;/a>，兩者解的問題不同。機制以 &lt;a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間&lt;/h2>
&lt;p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：&lt;/p>





&lt;div class="highlight">&lt;pre tabindex="0" class="chroma">&lt;code class="language-text" data-lang="text">&lt;span class="line">&lt;span class="ln"> 1&lt;/span>&lt;span class="cl">T0 master 失去回應
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl"> ↓ (down-after-milliseconds)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">T1 單一 Sentinel 標記 master 為 SDOWN（主觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 4&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間互問)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 5&lt;/span>&lt;span class="cl">T2 達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl"> ↓ (Sentinel 之間選出 leader 來主導 failover)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 7&lt;/span>&lt;span class="cl">T3 leader Sentinel 從 replica 中挑一個當新 master
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 8&lt;/span>&lt;span class="cl"> ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 9&lt;/span>&lt;span class="cl">T4 新 master 提升完成
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl"> ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">11&lt;/span>&lt;span class="cl">T5 client 發現新 master、重連、恢復寫入&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 &lt;code>down-after-milliseconds&lt;/code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。&lt;/p>
&lt;p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎&lt;/a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。Sentinel 處理的是「單 master 容量夠、但 master 不能是單點」的 HA 場景；要橫向擴容超過單機記憶體則走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>，兩者解的問題不同。機制以 <a href="https://redis.io/docs/latest/operate/oss_and_stack/management/sentinel/">Redis Sentinel 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="failover-是一條時序鏈不是一個瞬間">Failover 是一條時序鏈、不是一個瞬間</h2>
<p>「master 掛了 Sentinel 會自動切換」這句話把 failover 講成一個原子動作，但真正在 production 出事時，問題永遠出在這條鏈的某一段卡住。把 failover 攤開成時序，才看得到延遲跟資料遺失藏在哪：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">T0   master 失去回應
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">     ↓ (down-after-milliseconds)
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">T1   單一 Sentinel 標記 master 為 SDOWN（主觀下線）
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">     ↓ (Sentinel 之間互問)
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">T2   達到 quorum 數量的 Sentinel 同意 → ODOWN（客觀下線）
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">     ↓ (Sentinel 之間選出 leader 來主導 failover)
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">T3   leader Sentinel 從 replica 中挑一個當新 master
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">     ↓ (SLAVEOF NO ONE + 其他 replica 改指向新 master)
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">T4   新 master 提升完成
</span></span><span class="line"><span class="ln">10</span><span class="cl">     ↓ (Sentinel 廣播新 topology、更新 DNS / 通知 client)
</span></span><span class="line"><span class="ln">11</span><span class="cl">T5   client 發現新 master、重連、恢復寫入</span></span></code></pre></div><p>從 T0 到 T5 的總時間決定了「寫入中斷多久」。每一段都有對應的旋鈕跟失敗模式：T0→T1 由 <code>down-after-milliseconds</code> 控制（太短誤判、太長反應慢）；T1→T2 由 quorum 設定控制（太低腦裂風險、太高切不動）；T4→T5 由 client 的 topology 感知能力控制。理解 failover 就是理解這條鏈的每一段。</p>
<p>對把 cache 當主要 serving layer 的服務，這條鏈的長度直接是業務中斷時間。<a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Tinder 的配對引擎</a>每次滑動讀多個 cache、cache miss 是邊緣案例——failover 期間若寫入中斷十幾秒，新寫入的 profile 互動全部 hang，sub-millisecond 的 SLA 在這幾秒徹底失守。這也是為什麼大規模服務多半走 managed multi-AZ failover（見 ElastiCache）而非自管 Sentinel。</p>
<h2 id="核心概念sentinel-的判定模型">核心概念：Sentinel 的判定模型</h2>
<p>Sentinel 是獨立於 Redis 資料節點的監控進程，它的判定靠兩層共識避免單一 Sentinel 誤判。</p>
<p><strong>SDOWN（Subjectively Down，主觀下線）</strong>：單一 Sentinel 在 <code>down-after-milliseconds</code> 內收不到 master 的有效回應（<code>PING</code>），就主觀認定它下線。這只是一個 Sentinel 的意見，不觸發 failover。</p>
<p><strong>ODOWN（Objectively Down，客觀下線）</strong>：當標記 SDOWN 的 Sentinel 數量達到 <code>quorum</code> 設定值，master 被客觀認定下線。只有 master 的 ODOWN 才會觸發 failover（replica 的下線只標記不 failover）。</p>
<p><code>quorum</code> 是「多少個 Sentinel 同意才算真的下線」，它跟「多少個 Sentinel 同意才能執行 failover」是兩個不同的數字——後者需要 Sentinel 的多數（majority），確保同時只有一個 leader 主導 failover，避免兩個 Sentinel 各自提升不同 replica 造成腦裂。</p>
<p><strong>為什麼 Sentinel 要部署奇數個且至少三個</strong>：quorum 跟 majority 都需要足夠的 Sentinel 投票。兩個 Sentinel 無法在其中一個故障時達成 majority；三個才能容忍一個故障。Sentinel 應部署在不同故障域（不同 AZ / 機架），且不要跟 Redis 資料節點同生共死。</p>
<p><strong>Sentinel 不是 proxy</strong>：client 不透過 Sentinel 讀寫資料。client 向 Sentinel 查詢「現在的 master 是誰」，拿到地址後直連 Redis。failover 後 client 必須重新向 Sentinel 查詢——這是 T4→T5 的關鍵，client library 要支援 Sentinel 模式才能自動完成。</p>
<h2 id="配置sentinel-的設定路徑">配置：Sentinel 的設定路徑</h2>
<p>最小三 Sentinel 配置，每個 Sentinel 一份 <code>sentinel.conf</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># sentinel.conf</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="c1"># 監控名為 mymaster 的 master、quorum=2（三個 Sentinel 中兩個同意算 ODOWN）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">sentinel monitor mymaster 10.0.0.1 <span class="m">6379</span> <span class="m">2</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"># 多久收不到回應算 SDOWN（5 秒）</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">sentinel down-after-milliseconds mymaster <span class="m">5000</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="c1"># failover 後同時最多幾個 replica 去 resync 新 master</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># 設 1 = 串行 resync、避免所有 replica 同時 resync 拖垮新 master</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">sentinel parallel-syncs mymaster <span class="m">1</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="c1"># failover 整體逾時（三分鐘內沒完成算失敗、可重試）</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">sentinel failover-timeout mymaster <span class="m">180000</span></span></span></code></pre></div><p>啟動 Sentinel：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">redis-sentinel /path/to/sentinel.conf
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 或 redis-server /path/to/sentinel.conf --sentinel</span></span></span></code></pre></div><p>client 端要用 Sentinel-aware 連線（以 Python redis-py 為例）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">redis.sentinel</span> <span class="kn">import</span> <span class="n">Sentinel</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">sentinel</span> <span class="o">=</span> <span class="n">Sentinel</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="p">[(</span><span class="s2">&#34;10.0.0.10&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.11&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">),</span> <span class="p">(</span><span class="s2">&#34;10.0.0.12&#34;</span><span class="p">,</span> <span class="mi">26379</span><span class="p">)],</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="c1"># 寫入走 master（failover 後自動重新發現）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="n">master</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">master_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="n">master</span><span class="o">.</span><span class="n">set</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">,</span> <span class="s2">&#34;value&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="c1"># 讀取可走 replica</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">replica</span> <span class="o">=</span> <span class="n">sentinel</span><span class="o">.</span><span class="n">slave_for</span><span class="p">(</span><span class="s2">&#34;mymaster&#34;</span><span class="p">,</span> <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="n">replica</span><span class="o">.</span><span class="n">get</span><span class="p">(</span><span class="s2">&#34;key&#34;</span><span class="p">)</span></span></span></code></pre></div><p>關鍵：client 透過 <code>master_for</code> 拿到的是一個會在 failover 後重新查詢 Sentinel 的連線封裝，不是寫死的 IP。直接寫死 master IP 的 client 在 failover 後會持續連到死掉的舊 master。</p>
<h3 id="防腦裂的兩個-master-端設定">防腦裂的兩個 master 端設定</h3>
<p>Sentinel 選主的同時，要防止舊 master 復活後繼續接受寫入（split-brain）。在 Redis master 端設：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 至少要有 1 個 replica 連著、且 replica lag &lt; 10 秒、master 才接受寫入</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">redis-cli CONFIG SET min-replicas-to-write <span class="m">1</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">redis-cli CONFIG SET min-replicas-max-lag <span class="m">10</span></span></span></code></pre></div><p>這讓被網路隔離的舊 master（連不到 replica）自動停止接受寫入，避免它在隔離期間累積的寫入在復活後跟新 master 衝突。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1down-after-太短網路抖動誤觸-failover">Case 1：down-after 太短、網路抖動誤觸 failover</h3>
<p><strong>徵兆</strong>：master 其實沒死，只是一次短暫的網路抖動或 GC 暫停，Sentinel 卻觸發了 failover，造成一次不必要的中斷；甚至反覆 failover（flapping）。</p>
<p><strong>根因</strong>：<code>down-after-milliseconds</code> 設太短（例如 1000ms），master 一個短暫的 STW GC 或跨 AZ 網路抖動就超過閾值，被誤判 SDOWN→ODOWN。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>down-after-milliseconds</code> 設成能容忍正常抖動的值（5000-10000ms 是常見起點），用實際 RTT 與 GC pause 分布反推</li>
<li>quorum 設成多數而非 1，要求多個 Sentinel 同時看到下線，過濾單一 Sentinel 的網路問題</li>
<li>Sentinel 跟 Redis 不要跨高延遲鏈路放，網路品質直接影響誤判率</li>
<li>監控 failover 觸發頻率，flapping 是調參訊號</li>
</ol>
<h3 id="case-2failover-後-client-連到死掉的舊-master">Case 2：failover 後 client 連到死掉的舊 master</h3>
<p><strong>徵兆</strong>：failover 完成、Sentinel 日誌顯示新 master 已提升，但部分 application 持續寫入失敗或寫到舊 master（資料進黑洞），<code>CLIENT LIST</code> 在新 master 上看不到這些 client。</p>
<p><strong>根因</strong>：client 寫死了 master IP，或用的 client library 不支援 Sentinel 模式，failover 後不會重新向 Sentinel 查詢新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 一律用 Sentinel-aware 連線（<code>master_for</code> / lettuce 的 Sentinel 配置），不寫死 IP</li>
<li>確認 client library 版本支援 Sentinel 且配置正確（連的是 Sentinel port 26379，不是 Redis 6379）</li>
<li>對 latency-sensitive 服務，failover 後可主動 rolling restart application，清掉殘留連線</li>
<li>設 <code>min-replicas-to-write</code> 讓被隔離的舊 master 自動停寫，即使 client 連上去也寫不進，避免資料進黑洞</li>
</ol>
<h3 id="case-3選到-lag-大的-replicafailover-丟資料">Case 3：選到 lag 大的 replica、failover 丟資料</h3>
<p><strong>徵兆</strong>：failover 後發現最近幾秒的寫入不見了，新 master 的資料比預期舊。</p>
<p><strong>根因</strong>：Redis replication 是非同步的，replica 之間 lag 不一。Sentinel 選主會優先選 lag 小的（靠 <code>replica-priority</code> 與複製 offset），但若所有 replica 都 lag 大（master 寫入遠快於複製），無論選哪個都會丟掉未複製的寫入。Sentinel 的 failover 保證可用性，不保證零資料遺失。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>設 <code>min-replicas-to-write</code> + <code>min-replicas-max-lag</code>，lag 過大時 master 主動停寫，限制資料遺失窗口</li>
<li>監控 replication lag（<code>master_repl_offset</code> vs replica 的 offset），lag 持續大代表複製跟不上寫入，要降寫入或擴容</li>
<li>用 <code>replica-priority</code> 把不適合當 master 的 replica（例如做備份的、跨區的）設成 0 排除</li>
<li>需要零資料遺失的場景，Sentinel 的非同步複製不夠，走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log（強一致持久性）</li>
</ol>
<h3 id="case-4腦裂舊-master-復活後雙寫衝突">Case 4：腦裂——舊 master 復活後雙寫衝突</h3>
<p><strong>徵兆</strong>：網路分區期間 Sentinel 提升了新 master，分區恢復後舊 master 回來，兩個 master 各自接受過寫入，資料出現衝突或舊 master 的寫入被覆蓋遺失。</p>
<p><strong>根因</strong>：舊 master 在分區期間被隔離（連不到 Sentinel 多數），但 client 若還連得到它且它沒設停寫保護，就繼續接受寫入。分區恢復後舊 master 被降為 replica，它在分區期間的寫入被新 master 的資料覆蓋。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>min-replicas-to-write 1</code> + <code>min-replicas-max-lag 10</code> 是核心防護——被隔離的舊 master 連不到 replica，自動停寫</li>
<li>Sentinel 部署在多數能存活的故障域，確保分區時多數 Sentinel 在新 master 那側</li>
<li>接受 Redis 的 CAP 取捨：Sentinel 偏向可用性，極端分區下無法完全避免資料遺失，要強一致走別的儲存層</li>
<li>failover 後監控舊 master 復活的降級流程，確認它正確變成 replica 且 resync</li>
</ol>
<h3 id="case-5parallel-syncs-設太大failover-後新-master-被-resync-拖垮">Case 5：parallel-syncs 設太大、failover 後新 master 被 resync 拖垮</h3>
<p><strong>徵兆</strong>：failover 完成的瞬間新 master 延遲暴增、甚至短暫無回應，所有 replica 同時對它發起全量同步。</p>
<p><strong>根因</strong>：<code>parallel-syncs</code> 設成大於 1（或等於 replica 數），failover 後所有 replica 同時對新 master 做 full resync。full resync 要新 master 做 BGSAVE（fork、見 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence deep article</a>）並把 RDB 傳給每個 replica，多個同時進行直接打爆新 master。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><code>parallel-syncs</code> 設 1，replica 串行 resync，犧牲一點恢復速度換新 master 不被拖垮</li>
<li>確認 master 端 <code>repl-backlog-size</code> 夠大，讓短暫斷線的 replica 走部分同步（partial resync）而非全量</li>
<li>監控 failover 後新 master 的 CPU / 記憶體，resync 期間是脆弱窗口</li>
<li>resync 的 fork 成本跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體 headroom</a> 直接相關，新 master 也要留 fork 空間</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>Sentinel 的容量判讀，圍繞 failover 時間與資料遺失窗口：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>failover 總時間（T0→T5）</td>
          <td>數秒到十幾秒</td>
          <td>過長 → 查 down-after / parallel-syncs / client</td>
      </tr>
      <tr>
          <td>failover 觸發頻率</td>
          <td>罕見（真實故障才觸發）</td>
          <td>flapping → down-after 太短、quorum 太低</td>
      </tr>
      <tr>
          <td>replication lag</td>
          <td>&lt; 1 秒</td>
          <td>持續大 → 寫入超過複製能力、failover 會丟資料</td>
      </tr>
      <tr>
          <td>Sentinel 數量</td>
          <td>奇數、≥ 3、跨故障域</td>
          <td>&lt; 3 或同故障域 → 無法容忍 Sentinel 故障</td>
      </tr>
      <tr>
          <td>寫入中斷可容忍時間</td>
          <td>業務定義</td>
          <td>不可容忍 → Sentinel 不夠、走 managed multi-AZ</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單 master 容量不夠（記憶體 / 吞吐超過單機）</strong>：Sentinel 解 HA 不解容量。要橫向擴容走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a>，它自帶 sharding 與 per-shard failover。</li>
<li><strong>不想自己運維 Sentinel 與 failover 演練</strong>：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">ElastiCache</a> 的 Multi-AZ 自動 failover 把這條時序鏈託管，failover ~30 秒到幾分鐘，省掉 Sentinel 部署與調參，代價是 managed premium。</li>
<li><strong>需要零資料遺失的強持久性</strong>：Sentinel 的非同步複製在 failover 時會丟未複製的寫入。要強一致走 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">MemoryDB</a> 的 multi-AZ transaction log。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Sentinel 是 HA 的一層，但它的每一段都跟其他子系統耦合：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></strong>：Sentinel 是「不分片的 HA」，Cluster 是「分片 + 每 shard 自帶 failover」。容量需求決定走哪條，本文是前者。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：failover 後的 resync 靠 BGSAVE（fork），新 master 的 fork 成本是 resync 期間的脆弱點。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：新 master 提升後要承接全部寫入並支援 replica resync 的 fork，記憶體 headroom 不能少。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">Meta cache consistency</a></strong>：failover / replica promotion 期間的 stale read 與一致性議題，是大規模 cache 治理的核心，Sentinel 的非同步複製是 stale window 的來源之一。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a></li>
<li>平行 vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（managed multi-AZ failover）</li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> overview 的 implementation-layer deep article。連線與往返是 application 端量到的延遲主因，跟 server 端的&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體&lt;/a>、&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">持久化&lt;/a>調校互補。pipeline 機制以 &lt;a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件&lt;/a> 為準、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返&lt;/h2>
&lt;p>把單一 &lt;code>GET&lt;/code> 丟進 &lt;code>redis-cli --latency&lt;/code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。&lt;/p>
&lt;p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。&lt;/p>
&lt;p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &amp;#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點&lt;/a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。&lt;/p>
&lt;h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段&lt;/h2>
&lt;p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：&lt;/p>
&lt;p>&lt;strong>連線池消除「每次都建連線」的稅&lt;/strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> overview 的 implementation-layer deep article。連線與往返是 application 端量到的延遲主因，跟 server 端的<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">持久化</a>調校互補。pipeline 機制以 <a href="https://redis.io/docs/latest/develop/use/pipelining/">Redis pipelining 官方文件</a> 為準、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="延遲不在-redis在往返">延遲不在 Redis、在往返</h2>
<p>把單一 <code>GET</code> 丟進 <code>redis-cli --latency</code>，會看到 server 端執行時間是微秒級。但 application 端的 APM 量到的 Redis 呼叫卻是 1-3ms。這個差距不是 Redis 變慢了，是網路往返（round-trip time，RTT）——命令從 application 送到 Redis、結果送回來，這趟來回就是毫秒級，而 Redis 的執行只佔其中一小部分。</p>
<p>這個認知翻轉了 Redis 優化的方向：當你的服務每個請求要打 10 個 Redis 命令，瓶頸不是 Redis 的吞吐，是 10 次 RTT 疊加成 10-30ms。pipelining 常被講成「批次發命令省效能」，但它真正消除的是 RTT 稅——把 10 次往返打包成 1 次往返，server 端執行時間幾乎不變，但 application 端延遲從 10×RTT 降到 1×RTT。</p>
<p>對每次互動要查多個 cache 的服務，這筆 RTT 稅是延遲預算的主要支出。<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 在 multi-cloud 架構下的痛點</a>正是這個放大版：application 在一個 cloud、cache 在另一個，每次 lookup 多吃 5-30ms 跨 cloud RTT，「5ms × 10 cache lookup = 50ms 額外延遲」。Snap 把 KeyDB 部署到同 cloud 減少跨 cloud RTT，本質就是降低往返稅。本文處理 RTT 的會計、連線池配置與 pipeline 的正確使用。</p>
<h2 id="核心概念rtt-會計與三種降稅手段">核心概念：RTT 會計與三種降稅手段</h2>
<p>Redis 一次請求的延遲拆成三段：client 序列化 + 送出、網路往返（RTT）、server 執行。多數 cache 場景下 RTT 是主導項，server 執行可忽略。降低總延遲有三種手段，對應三種「省 RTT」的方式：</p>
<p><strong>連線池消除「每次都建連線」的稅</strong>。建立 TCP 連線（三次握手）本身就是一趟 RTT，若還有 TLS 再加幾趟。每個請求都新建連線等於每次都付建連稅。連線池讓連線重用，把建連成本攤平到接近零。</p>
<p><strong>pipelining 把 N 次 RTT 壓成 1 次</strong>。連續送 N 個命令而不等每個的回應，一次讀回 N 個結果。這要求這 N 個命令彼此無依賴（後一個不需要前一個的結果）。</p>
<p><strong>Lua script / 多 key 命令把多操作合成 1 次往返且原子</strong>。當命令之間有依賴（讀了再決定怎麼寫），pipeline 不適用（後面的命令送出時前面的結果還沒回來），這時用 Lua script 把邏輯放到 server 端一次執行，省 RTT 又拿到原子性。</p>
<h3 id="pipeline-跟-multi-是不同的東西">pipeline 跟 MULTI 是不同的東西</h3>
<p>這兩個常被混淆，但解的問題不同：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>pipeline</th>
          <th>MULTI / EXEC（transaction）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>主要目的</td>
          <td>省 RTT（效能）</td>
          <td>原子性（多命令不被打斷）</td>
      </tr>
      <tr>
          <td>原子性</td>
          <td>無——命令間可能插入其他 client</td>
          <td>有——EXEC 內命令連續執行不被插入</td>
      </tr>
      <tr>
          <td>回應時機</td>
          <td>全部送完一次讀回</td>
          <td>EXEC 後一次回所有結果</td>
      </tr>
      <tr>
          <td>失敗處理</td>
          <td>各命令獨立成敗</td>
          <td>入隊期語法錯整批拒、執行期錯不回滾</td>
      </tr>
      <tr>
          <td>適用</td>
          <td>大量無依賴命令的批次讀寫</td>
          <td>需要「一組命令不被其他 client 插隊」</td>
      </tr>
  </tbody>
</table>
<p>pipeline 純粹是傳輸層優化，不保證原子性——pipeline 裡的命令在 server 端仍可能跟其他 client 的命令交錯。要原子性用 MULTI/EXEC 或 Lua。兩者也可以組合（在 pipeline 裡送 MULTI&hellip;EXEC）。</p>
<p>注意 Redis 的 MULTI/EXEC 不是關聯式 DB 的 transaction：執行期某命令出錯（例如對 string 做 list 操作）不會回滾已執行的命令，它沒有 rollback。</p>
<h2 id="配置連線池與-pipeline-的設定路徑">配置：連線池與 pipeline 的設定路徑</h2>
<p>連線池配置（以 Python redis-py 為例，多數 client library 概念一致）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">import</span> <span class="nn">redis</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="n">pool</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">ConnectionPool</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">host</span><span class="o">=</span><span class="s2">&#34;10.0.0.1&#34;</span><span class="p">,</span> <span class="n">port</span><span class="o">=</span><span class="mi">6379</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="n">max_connections</span><span class="o">=</span><span class="mi">50</span><span class="p">,</span>          <span class="c1"># 池上限、依並發量與 Redis maxclients 反推</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">socket_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>          <span class="c1"># 單命令逾時（秒）——必設、否則慢命令拖垮 caller</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="n">socket_connect_timeout</span><span class="o">=</span><span class="mf">0.5</span><span class="p">,</span>  <span class="c1"># 建連逾時</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="n">health_check_interval</span><span class="o">=</span><span class="mi">30</span><span class="p">,</span>    <span class="c1"># 定期檢查連線存活、清掉壞連線</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="p">)</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">r</span> <span class="o">=</span> <span class="n">redis</span><span class="o">.</span><span class="n">Redis</span><span class="p">(</span><span class="n">connection_pool</span><span class="o">=</span><span class="n">pool</span><span class="p">)</span></span></span></code></pre></div><p><code>socket_timeout</code> 是最常被遺漏卻最關鍵的設定——沒設逾時，一個慢命令或網路黑洞會讓 caller 無限等待，連鎖拖垮上游。</p>
<p>pipeline 的使用：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># pipeline：N 個無依賴命令、一次往返</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">pipe</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">pipeline</span><span class="p">(</span><span class="n">transaction</span><span class="o">=</span><span class="kc">False</span><span class="p">)</span>  <span class="c1"># transaction=False 純 pipeline、不包 MULTI</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="k">for</span> <span class="n">uid</span> <span class="ow">in</span> <span class="n">user_ids</span><span class="p">:</span>                  <span class="c1"># 假設要拿 100 個 user 的 profile</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">pipe</span><span class="o">.</span><span class="n">hgetall</span><span class="p">(</span><span class="sa">f</span><span class="s2">&#34;user:</span><span class="si">{</span><span class="n">uid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="n">results</span> <span class="o">=</span> <span class="n">pipe</span><span class="o">.</span><span class="n">execute</span><span class="p">()</span>              <span class="c1"># 一次往返拿回 100 個結果</span></span></span></code></pre></div><p>依賴型操作改用 Lua（命令間有讀後寫的依賴，pipeline 不適用）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="c1"># 原子的 check-and-set：讀目前值、符合條件才更新——一次往返且原子</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="n">lua</span> <span class="o">=</span> <span class="s2">&#34;&#34;&#34;
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="s2">local current = redis.call(&#39;GET&#39;, KEYS[1])
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="s2">if current == ARGV[1] then
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="s2">  redis.call(&#39;SET&#39;, KEYS[1], ARGV[2])
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="s2">  return 1
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="s2">end
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="s2">return 0
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="s2">&#34;&#34;&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="n">cas</span> <span class="o">=</span> <span class="n">r</span><span class="o">.</span><span class="n">register_script</span><span class="p">(</span><span class="n">lua</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="n">cas</span><span class="p">(</span><span class="n">keys</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;lock:resource&#34;</span><span class="p">],</span> <span class="n">args</span><span class="o">=</span><span class="p">[</span><span class="s2">&#34;old_token&#34;</span><span class="p">,</span> <span class="s2">&#34;new_token&#34;</span><span class="p">])</span></span></span></code></pre></div><p><code>MGET</code> / <code>MSET</code> / <code>HMGET</code> 等原生多 key 命令是最簡單的省 RTT 手段——能用多 key 命令就不用 pipeline，更省事且原子。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1每請求新建連線延遲全是建連稅">Case 1：每請求新建連線、延遲全是建連稅</h3>
<p><strong>徵兆</strong>：Redis 呼叫延遲偏高且不穩，<code>INFO stats</code> 的 <code>total_connections_received</code> 速率極高（接近 QPS），Redis 的 <code>connected_clients</code> 反覆上下震盪。</p>
<p><strong>根因</strong>：application 沒用連線池，或每個請求 <code>redis.Redis(...)</code> 重新建立 client。每次請求付一趟 TCP 握手（加 TLS 更多）的 RTT，建連稅疊在每個請求上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>用連線池並重用，client 物件在 application 生命週期內共用，不是每請求建立</li>
<li>短生命週期環境（Lambda / serverless）把連線池放在 handler 外（容器重用時連線存活）</li>
<li>監控 <code>total_connections_received</code> 速率，遠高於合理重連頻率代表沒重用</li>
<li>TLS 場景的建連稅更高，連線重用的收益更大</li>
</ol>
<h3 id="case-2沒設-socket_timeout一個慢命令拖垮整條鏈">Case 2：沒設 socket_timeout、一個慢命令拖垮整條鏈</h3>
<p><strong>徵兆</strong>：某次 Redis 短暫卡頓（fork 尖峰、網路抖動），application 端大量請求 hang 住不回，thread / connection 被耗盡，影響擴散到跟 Redis 無關的請求。</p>
<p><strong>根因</strong>：連線沒設 <code>socket_timeout</code>。Redis 一旦慢回應或網路黑洞，caller 無限等待，佔住 thread 與連線，連鎖拖垮整個服務。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>一律設 <code>socket_timeout</code>（cache 場景通常幾百 ms 就該逾時，cache 本來就該快）</li>
<li>逾時後 application 要有 fallback（回源或降級），不是把逾時當 fatal</li>
<li>連線池 <code>max_connections</code> 設上限，避免無限建連把 Redis 的 <code>maxclients</code> 打滿</li>
<li>fork 尖峰是常見的慢源頭，對應 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence deep article</a> 的延遲尖峰治理</li>
</ol>
<h3 id="case-3一個巨大-pipeline-把-server-跟-client-都撐爆">Case 3：一個巨大 pipeline 把 server 跟 client 都撐爆</h3>
<p><strong>徵兆</strong>：用 pipeline 批次處理時，某次塞了幾十萬個命令進一個 pipeline，Redis 記憶體尖峰、client 端記憶體爆，甚至 OOM。</p>
<p><strong>根因</strong>：pipeline 把所有命令的 request 跟 response 都 buffer 起來。一次塞太多，server 端要 buffer 全部 reply（計入 <code>used_memory</code>、見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a> 的 output buffer），client 端要 hold 全部結果，雙邊記憶體尖峰。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pipeline 分批（chunk），每批幾百到幾千命令，不要一個 pipeline 塞無上限</li>
<li>大量資料的掃描用 <code>SCAN</code> 游標分批，不要 <code>KEYS *</code> 一次撈</li>
<li>監控 client output buffer（<code>CLIENT LIST</code> 的 <code>omem</code>），異常大代表有巨型 pipeline 或慢 consumer</li>
<li>批次大小靠 RTT 與記憶體權衡——批次越大省越多 RTT，但記憶體尖峰越高</li>
</ol>
<h3 id="case-4在-cluster-模式對跨-slot-key-開-pipeline--transaction-失敗">Case 4：在 cluster 模式對跨 slot key 開 pipeline / transaction 失敗</h3>
<p><strong>徵兆</strong>：單機 Redis 上運作正常的 pipeline 或 MULTI，搬到 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Redis Cluster</a> 後報 <code>CROSSSLOT Keys in request don't hash to the same slot</code>。</p>
<p><strong>根因</strong>：Cluster 模式下 MULTI/EXEC 與某些多 key 命令要求所有 key 在同一個 hash slot。pipeline 在 cluster 下也要按 slot 分組送到對應 node——若 client library 不自動處理跨 slot，會失敗。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>同組操作的 key 用 hash tag <code>{...}</code> 強制同 slot（例如 <code>user:{123}:profile</code>、<code>user:{123}:settings</code>）</li>
<li>用支援 cluster pipeline 的 client library，它會自動按 slot 分組</li>
<li>設計階段就考慮 key 的 slot 分布，避免事後重構，對應 cluster re-sharding 的 hash tag 治理</li>
<li>跨 slot 的批次邏輯改用 application 端聚合，不依賴 server 端原子性</li>
</ol>
<h3 id="case-5把-pipeline-當-transaction-用出現資料競態">Case 5：把 pipeline 當 transaction 用、出現資料競態</h3>
<p><strong>徵兆</strong>：用 pipeline 做「讀一個值、根據它決定寫什麼」的邏輯，高並發下偶發資料不一致——兩個 client 讀到同樣的舊值、各自寫入，一方覆蓋另一方。</p>
<p><strong>根因</strong>：把 pipeline 誤當原子操作。pipeline 只是把命令打包傳輸，命令之間 server 端仍可能插入其他 client 的命令——它沒有原子性。讀後寫的依賴邏輯放 pipeline 裡，等於沒有任何併發保護。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>讀後寫的依賴邏輯用 Lua script（server 端原子執行），不用 pipeline</li>
<li>樂觀鎖場景用 <code>WATCH</code> + MULTI/EXEC（watch 的 key 被改則 EXEC 失敗、重試）</li>
<li>分清楚需求：要省 RTT 用 pipeline，要原子性用 Lua / MULTI，兩者目的不同</li>
<li>distributed lock 場景見 <a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a>，Redis 的鎖有自己的正確性陷阱</li>
</ol>
<h2 id="capacity--cost-邊界">Capacity / cost 邊界</h2>
<p>連線與往返的容量判讀，圍繞連線數與每請求往返次數：</p>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>健康區間</th>
          <th>警戒與動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>connected_clients</code></td>
          <td>穩定、遠低於 <code>maxclients</code></td>
          <td>接近 maxclients → 池太大或洩漏、調池上限</td>
      </tr>
      <tr>
          <td><code>total_connections_received</code> 速率</td>
          <td>低（連線重用）</td>
          <td>接近 QPS → 沒用連線池、每請求建連</td>
      </tr>
      <tr>
          <td>每請求 Redis 往返次數</td>
          <td>盡量合併（多 key / pipeline）</td>
          <td>多次獨立往返 → 用 pipeline / MGET 合併</td>
      </tr>
      <tr>
          <td>client output buffer (<code>omem</code>)</td>
          <td>小</td>
          <td>大 → 巨型 pipeline 或慢 consumer</td>
      </tr>
      <tr>
          <td>Redis CPU</td>
          <td>有餘裕</td>
          <td>單執行緒 CPU 滿 → 命令太重或 QPS 超單機</td>
      </tr>
  </tbody>
</table>
<p>撞牆後的路由判斷：</p>
<ul>
<li><strong>單執行緒 CPU 打滿、命令吞吐到頂</strong>：Redis 主執行緒單線處理命令，pipeline 省 RTT 但不增加 server 端平行度。CPU 到頂走 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster 分片</a>把命令分散到多 node。</li>
<li><strong>想要單機多核平行處理命令</strong>：<a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> 的 shared-nothing 多核架構讓命令在單機就能多核平行，Redis 要靠 cluster 才能達到的吞吐它單機就能撐——高吞吐單機 workload 的替代。</li>
<li><strong>跨 cloud / 跨 region 的 RTT 是結構性瓶頸</strong>：<a href="/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/" data-link-title="9.C35 Snap：GCP &#43; KeyDB 在 multi-cloud 架構下的低延遲快取" data-link-desc="Snap 用 GCP 上的 KeyDB cluster 減少跨 cloud cache 延遲、用 TPU 訓練廣告推薦模型">Snap 的解法</a>是把 cache 部署到跟 application 同 cloud / 同 region，從根本消除跨區 RTT——這是架構層決策，不是 pipeline 能補的。</li>
</ul>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>連線與往返是 application 端延遲的主因，但它跟 server 端調校互補：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></strong>：巨型 pipeline 的 server 端 reply buffer 計入 <code>used_memory</code>、慢 consumer 的 output buffer 是記憶體洩漏源頭。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence / fork latency</a></strong>：fork 尖峰是 socket_timeout 必須存在的理由之一——慢源頭不只網路。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></strong>：cluster 模式改變 pipeline / transaction 的 key 分布規則，hash tag 治理是前提。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a></strong>：高並發下的連線數爆炸與熱 key 是同一組壓力的不同面向，連線池上限與 local cache 兩層都是解法。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>上游 vendor 頁：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></li>
<li>同 vendor deep article：<a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/persistence-fork-latency/" data-link-title="Redis 持久化與 fork latency：AOF、RDB 與那一次卡住整個 cluster 的 fork" data-link-desc="Redis 的 RDB save 與 AOF rewrite 都靠一次 fork()，而 fork 在大記憶體實例上會凍結主執行緒數百毫秒、複製分頁讓記憶體逼近翻倍。本文展開 AOF / RDB 的機制與 fsync 取捨、copy-on-write 的記憶體放大、5 個把持久化寫成延遲尖峰與資料遺失的 production 踩坑，以及 cache 場景到底要不要持久化的邊界">persistence 與 fork latency</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Sentinel 與 failover 時序</a>、<a href="/blog/backend/02-cache-redis/vendors/redis/cluster-resharding/" data-link-title="Redis Cluster Re-sharding：source = target，但 topology 重劃的 5 段流程" data-link-desc="Redis cluster re-sharding 是 5 type migration 漏類實證 — source / target 同 cluster、無 schema / paradigm 差、但 16384 slot 重分配是核心；本文涵蓋 4 種 re-sharding driver、slot migration 機制、redis-cli --cluster rebalance / reshard 工具、5 個 production 踩雷（cluster busy / replica lag / client cache stale / cross-slot transaction / monitor gap）">Cluster re-sharding</a></li>
<li>上游概念：<a href="/blog/backend/02-cache-redis/high-concurrency-access/" data-link-title="2.1 高併發下的 Redis 讀寫邊界" data-link-desc="說明高併發服務如何共用 Redis client、控制 pipeline 與避免 cache stampede">2.6 high concurrency</a>、<a href="/blog/backend/02-cache-redis/distributed-lock/" data-link-title="2.4 distributed lock 與租約" data-link-desc="整理鎖語意、租約風險與適用場景">2.5 distributed lock</a></li>
<li>Methodology：<a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章寫作方法論</a></li>
</ul>
]]></content:encoded></item><item><title>Memcached → Redis：不搬資料、搬存取層的能力升級遷移</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/migrate-to-redis/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/migrate-to-redis/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>（source）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>（target）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 後對映 Schema/API + application change High、但 &lt;strong>data topology Low（cache 可重建）&lt;/strong>——本文是「能力升級 + 資料層免遷」的 dogfood，跟反向的 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &amp;#43; 資料結構 &amp;#43; pub/sub &amp;#43; Lua &amp;#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &amp;#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）&lt;/a> 對位。&lt;/p>&lt;/blockquote>
&lt;h2 id="cache-遷移不搬資料搬存取層">cache 遷移不搬資料、搬存取層&lt;/h2>
&lt;p>一般 migration 最重、最危險的部分是搬資料——schema 要對、一致性要保、cutover 要不丟。Memcached → Redis 把這塊幾乎拿掉了，因為 &lt;strong>cache 的資料本來就是可重建的副本&lt;/strong>。遷移不需要把 Memcached 裡的東西搬到 Redis；讓 Redis 空著上線、cache miss 自然回源、命中率慢慢 warm 起來即可。Memcached 在 warm-up 期間繼續服務，等 Redis 命中率追上來再切。&lt;/p>
&lt;p>這個性質讓 Memcached → Redis 的工作重心完全不同：不在資料層，在&lt;strong>存取層&lt;/strong>（換 client library、換協定）跟&lt;strong>可選的能力升級&lt;/strong>。觸發這個遷移的通常是「outgrew pure KV」——本來只用 Memcached 存 string，後來需要 counter（用 application 層讀-改-寫硬湊、有 race）、需要 session 物件（serialize 整包 JSON、改一個欄位要全寫）、需要 leaderboard（在 app 排序、慢）。這些 Redis 用 INCR / Hash / Sorted Set 原生解，把 application 層硬湊的邏輯收回 cache 層。&lt;/p>
&lt;p>本文跑 diff audit 確認這個形狀、用兩階段結構（先 drop-in、再升級能力）展開遷移與踩坑。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>Memcached 協定 → Redis RESP、純 string → 可選 data types&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Operational model&lt;/td>
 &lt;td>Redis 多了 eviction policy / persistence / cluster 決策&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>pure cache → data structure store（但可先維持 pure KV 用法）&lt;/td>
 &lt;td>Medium&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>client library 換、可選改用 data types&lt;/td>
 &lt;td>High&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Data topology&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>cache 可重建、不搬資料、re-warm&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>Low&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>主導維度是 Schema/API + application change（存取層），但這個 migration 的特徵是 &lt;strong>data topology Low&lt;/strong>——這是 cache 類遷移獨有的性質。對映 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論&lt;/a> 的 type：本文是 &lt;strong>cache 類 Type A 的簡化變體&lt;/strong>（phased translation 的存取層翻譯，但因 data topology Low 省掉了資料遷移階段）。結構上採兩階段：&lt;strong>Phase 1 drop-in 替換（維持 pure KV 用法、先把 client 換掉）&lt;/strong>，&lt;strong>Phase 2 漸進採用 data types（把 application 層硬湊的邏輯收回 Redis）&lt;/strong>。Phase 2 是可選的、可以慢慢來。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a>（source）跟 <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a>（target）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 後對映 Schema/API + application change High、但 <strong>data topology Low（cache 可重建）</strong>——本文是「能力升級 + 資料層免遷」的 dogfood，跟反向的 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）</a> 對位。</p></blockquote>
<h2 id="cache-遷移不搬資料搬存取層">cache 遷移不搬資料、搬存取層</h2>
<p>一般 migration 最重、最危險的部分是搬資料——schema 要對、一致性要保、cutover 要不丟。Memcached → Redis 把這塊幾乎拿掉了，因為 <strong>cache 的資料本來就是可重建的副本</strong>。遷移不需要把 Memcached 裡的東西搬到 Redis；讓 Redis 空著上線、cache miss 自然回源、命中率慢慢 warm 起來即可。Memcached 在 warm-up 期間繼續服務，等 Redis 命中率追上來再切。</p>
<p>這個性質讓 Memcached → Redis 的工作重心完全不同：不在資料層，在<strong>存取層</strong>（換 client library、換協定）跟<strong>可選的能力升級</strong>。觸發這個遷移的通常是「outgrew pure KV」——本來只用 Memcached 存 string，後來需要 counter（用 application 層讀-改-寫硬湊、有 race）、需要 session 物件（serialize 整包 JSON、改一個欄位要全寫）、需要 leaderboard（在 app 排序、慢）。這些 Redis 用 INCR / Hash / Sorted Set 原生解，把 application 層硬湊的邏輯收回 cache 層。</p>
<p>本文跑 diff audit 確認這個形狀、用兩階段結構（先 drop-in、再升級能力）展開遷移與踩坑。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>Memcached 協定 → Redis RESP、純 string → 可選 data types</td>
          <td>High</td>
      </tr>
      <tr>
          <td>Operational model</td>
          <td>Redis 多了 eviction policy / persistence / cluster 決策</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>pure cache → data structure store（但可先維持 pure KV 用法）</td>
          <td>Medium</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>client library 換、可選改用 data types</td>
          <td>High</td>
      </tr>
      <tr>
          <td><strong>Data topology</strong></td>
          <td><strong>cache 可重建、不搬資料、re-warm</strong></td>
          <td><strong>Low</strong></td>
      </tr>
  </tbody>
</table>
<p>主導維度是 Schema/API + application change（存取層），但這個 migration 的特徵是 <strong>data topology Low</strong>——這是 cache 類遷移獨有的性質。對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的 type：本文是 <strong>cache 類 Type A 的簡化變體</strong>（phased translation 的存取層翻譯，但因 data topology Low 省掉了資料遷移階段）。結構上採兩階段：<strong>Phase 1 drop-in 替換（維持 pure KV 用法、先把 client 換掉）</strong>，<strong>Phase 2 漸進採用 data types（把 application 層硬湊的邏輯收回 Redis）</strong>。Phase 2 是可選的、可以慢慢來。</p>
<h2 id="phase-1drop-in-替換pure-kv不搬資料">Phase 1：drop-in 替換（pure KV、不搬資料）</h2>
<p>第一階段把 Memcached 換成 Redis，但<strong>只用 Redis 當 pure KV</strong>（GET / SET / DEL + TTL），存取行為跟 Memcached 一樣。這一步風險最低，因為不碰 data model、不搬資料。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">存取層對應（Phase 1 維持 pure KV 語意）：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  Memcached set(key, val, ttl)   →  Redis SET key val EX ttl
</span></span><span class="line"><span class="ln">3</span><span class="cl">  Memcached get(key)             →  Redis GET key
</span></span><span class="line"><span class="ln">4</span><span class="cl">  Memcached delete(key)          →  Redis DEL key
</span></span><span class="line"><span class="ln">5</span><span class="cl">  Memcached incr/decr            →  Redis INCR/DECR（Redis 原生原子、比 Memcached 更穩）</span></span></code></pre></div><p>cutover 流程（cache 可重建、無資料遷移）：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">1. 部署 Redis（空的）、設 maxmemory + eviction policy（見記憶體調校）
</span></span><span class="line"><span class="ln">2</span><span class="cl">2. application 改用 Redis client（雙寫期：同時寫 Memcached + Redis，讀仍走 Memcached）
</span></span><span class="line"><span class="ln">3</span><span class="cl">3. 讀切到 Redis（cache miss 回源 + 寫回 Redis、命中率逐步 warm up）
</span></span><span class="line"><span class="ln">4</span><span class="cl">4. 觀察 Redis 命中率追上 Memcached、origin 壓力無異常
</span></span><span class="line"><span class="ln">5</span><span class="cl">5. 停止寫 Memcached、下線 Memcached</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>不需要資料遷移工具——Redis 空上線、靠 cache-aside 自然 warm（見 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 cache aside</a>）</li>
<li>warm-up 期 origin 壓力會短暫上升（命中率從 0 爬升），低流量時段切、或預熱熱 key</li>
<li>Phase 1 完成後 application 行為跟用 Memcached 時一致，只是底層換 Redis</li>
<li>想保留開源 OSI 授權，target 直接選 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（Redis 相容、BSD）</li>
</ul>
<h2 id="phase-2漸進採用-data-types可選">Phase 2：漸進採用 data types（可選）</h2>
<p>Phase 1 上線穩定後，再把 application 層硬湊的邏輯逐步收回 Redis 的原生 data types。這一階段是能力升級、不是遷移必需，可以一個場景一個場景來。</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">application 硬湊 → Redis 原生：
</span></span><span class="line"><span class="ln">2</span><span class="cl">  讀 JSON → 改欄位 → 寫回整包    →  Redis Hash（HSET/HGET 單欄位、免全寫）
</span></span><span class="line"><span class="ln">3</span><span class="cl">  app 端計數 + CAS 重試           →  Redis INCR（原子、無 race）
</span></span><span class="line"><span class="ln">4</span><span class="cl">  app 端排序 leaderboard          →  Redis Sorted Set（ZADD/ZRANGE）
</span></span><span class="line"><span class="ln">5</span><span class="cl">  app 端 set 去重                 →  Redis Set（SADD/SISMEMBER）
</span></span><span class="line"><span class="ln">6</span><span class="cl">  多 key 操作要原子               →  Redis MULTI / Lua（Memcached 只有 CAS）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>Phase 2 每個改動是獨立的小重構，不必一次到位</li>
<li>收回 data types 的收益是「消除 application 層的 read-modify-write race + 減少網路往返」</li>
<li>不是所有東西都要升級——純 string cache 留在 GET/SET 就好，別為了用而用</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1warm-up-期-origin-被打爆">Case 1：warm-up 期 origin 被打爆</h3>
<p><strong>徵兆</strong>：切讀到 Redis 的瞬間，origin（DB）QPS 暴增、延遲升高，因為 Redis 還是空的、大量 cache miss 同時回源。</p>
<p><strong>根因</strong>：Redis 空上線、命中率從 0 開始，warm-up 期所有讀都 miss 回源。沒有控制就是一次 origin 衝擊（類似冷啟動 stampede）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>低流量時段切讀、讓命中率平緩爬升</li>
<li>預熱熱 key（migration 前先把已知熱 key 灌進 Redis）</li>
<li>cache miss 回源加 singleflight / jitter，避免同 key 並發回源（見 <a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 stampede rollback</a>）</li>
<li>雙寫期先讓 Redis 被寫入 warm 一段時間，再切讀</li>
</ol>
<h3 id="case-2把-memcached-的-multi-get-行為直接搬效能不如預期">Case 2：把 Memcached 的 multi-get 行為直接搬、效能不如預期</h3>
<p><strong>徵兆</strong>：Memcached 的 batch get（一次拿多 key）搬到 Redis 後延遲沒改善甚至更差。</p>
<p><strong>根因</strong>：Memcached client 的 multi-get 跟 Redis 的 MGET / pipeline 行為不同。直接一個 key 一個 GET（N 次往返）會比 Memcached 的 batch 慢——Redis 要用 MGET 或 pipeline 才能合併往返（見 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a>）。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>Memcached multi-get → Redis MGET（同 slot）或 pipeline</li>
<li>不要把「N 次獨立 GET」當成 multi-get 的等價</li>
<li>cluster 模式下 MGET 跨 slot 會失敗，用 hash tag 或 pipeline 分組</li>
<li>量測往返次數，存取層遷移要保持「一次互動的往返數」不退化</li>
</ol>
<h3 id="case-3ttl-精度與-eviction-行為差異造成命中率變化">Case 3：TTL 精度與 eviction 行為差異造成命中率變化</h3>
<p><strong>徵兆</strong>：遷到 Redis 後命中率跟 Memcached 時期不一樣（可能更高或更低），cache 行為不如預期。</p>
<p><strong>根因</strong>：Memcached 是 LRU + 秒級 lazy expiration + slab 限制；Redis 有 8 種 eviction policy + ms 級 TTL + 不同記憶體模型。沿用 Memcached 的 TTL 與容量設定不會得到一樣的淘汰行為。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>明確設 Redis 的 <code>maxmemory-policy</code>（純 cache 用 allkeys-lru / allkeys-lfu，見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體與淘汰調校</a>）</li>
<li>不要假設 Memcached 的容量設定直接套用——Redis 記憶體模型不同（無 slab calcification、但有自己的 fragmentation）</li>
<li>觀察 <code>evicted_keys</code> 與命中率，對齊預期 working set</li>
<li>Memcached 的 slab 浪費 vs Redis 的編碼，記憶體佔用會不同，重新算容量</li>
</ol>
<h3 id="case-4以為-redis-一定比-memcached-快--省">Case 4：以為 Redis 一定比 Memcached 快 / 省</h3>
<p><strong>徵兆</strong>：遷到 Redis 後純 string cache 的記憶體佔用或延遲沒有改善，甚至 Redis 單執行緒在高並發純 GET 下不如 Memcached 多執行緒。</p>
<p><strong>根因</strong>：對「純 string KV、高並發」這個 Memcached 的本場，Memcached 的多執行緒可能比 Redis 單執行緒（命令層）更適合。遷 Redis 的收益在 data types / persistence / 生態，不是純 KV 效能。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>釐清遷移動機——是要 data types / persistence（Redis 解）還是純 KV 效能（Memcached 可能更好）</li>
<li>純 KV 高並發要 Redis 的多核走 <a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a> / <a href="/blog/backend/02-cache-redis/vendors/keydb/" data-link-title="KeyDB" data-link-desc="Redis multi-threaded fork、active-replication、Snap 採用">KeyDB</a> 或 Redis I/O threads</li>
<li>純 cache 紀律本來就是 Memcached 的優勢，遷 Redis 要小心別把 cache 用成 database</li>
<li>沒有 data types / persistence 需求的純 KV，留 Memcached 可能更對</li>
</ol>
<h3 id="case-5把可重建的-cache-當成要搬的資料白做遷移工具">Case 5：把可重建的 cache 當成要搬的資料、白做遷移工具</h3>
<p><strong>徵兆</strong>：團隊花時間寫 Memcached → Redis 的資料遷移腳本、做一致性校驗，結果發現 cache 切換後這些資料本來就會被新值覆蓋。</p>
<p><strong>根因</strong>：用一般 migration 的思維（搬資料 + 校驗）處理 cache 遷移，沒意識到 cache 是可重建副本——搬過去的舊值很快被回源的新值取代，搬資料是白工且可能搬到 stale 值。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cache 遷移預設不搬資料、靠 re-warm（這是 cache 類遷移的核心簡化）</li>
<li>只有「重建成本極高的 cache」（昂貴計算結果）才考慮搬，且要評估 stale 風險</li>
<li>把精力放在存取層正確性與 warm-up 控制，不是資料搬遷</li>
<li>對照 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">cache copy boundary</a>：cache 是副本、不是 source-of-truth</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Memcached（source）</th>
          <th>Redis / Valkey（target）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>資料遷移</td>
          <td>—</td>
          <td>不需要（cache 可重建、re-warm）</td>
      </tr>
      <tr>
          <td>data types</td>
          <td>純 string KV</td>
          <td>6 大 + Stream / Geo</td>
      </tr>
      <tr>
          <td>原子操作</td>
          <td>INCR / DECR / CAS</td>
          <td>100+（INCR / HSET / ZADD / Lua）</td>
      </tr>
      <tr>
          <td>persistence</td>
          <td>無</td>
          <td>RDB / AOF（可選）</td>
      </tr>
      <tr>
          <td>多執行緒</td>
          <td>原生多執行緒</td>
          <td>單執行緒命令 + I/O threads</td>
      </tr>
      <tr>
          <td>eviction</td>
          <td>LRU only</td>
          <td>8 種 policy</td>
      </tr>
      <tr>
          <td>純 KV 高並發效能</td>
          <td>多執行緒、本場強</td>
          <td>單執行緒命令可能略遜（要多核走 fork）</td>
      </tr>
      <tr>
          <td>遷移風險</td>
          <td>—</td>
          <td>低（無資料遷移、存取層 + warm-up）</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：要 data types / persistence / 原子操作 → 遷 Redis（兩階段、低風險）；純 KV + 高並發 + 嚴格 cache 紀律 → 留 Memcached。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>Memcached → Redis 是能力升級，它跟 Redis 的調校與選型交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">Redis 記憶體與淘汰調校</a></strong>：遷過去要設對 maxmemory-policy，Redis 記憶體模型跟 Memcached slab 不同。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/connection-pipeline-latency/" data-link-title="Redis 連線與 pipeline：RTT 稅、連線池與一次往返打包多命令" data-link-desc="Redis 單命令通常微秒級執行，但 application 端量到的延遲是毫秒級——差距全在網路往返（RTT）。pipelining 的本質不是『批次發命令』，是把 N 次 RTT 壓成 1 次。本文展開 RTT 會計、連線池配置、pipeline 與 MULTI 的差異、5 個把連線與往返寫成延遲與正確性問題的 production 踩坑，以及連線模型撞牆的邊界">Redis 連線 / pipeline</a></strong>：Memcached multi-get → Redis MGET / pipeline，存取層遷移要保持往返數。</li>
<li><strong>跟反向 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached</a></strong>：反向是 Type E paradigm reduction（downgrade）；本文是能力升級（upgrade），兩者對位看 cache paradigm 的兩個方向。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：要開源 OSI 授權，target 選 Valkey（Redis 相容、BSD），遷移流程一致。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></li>
<li>反向 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-memcached/" data-link-title="Redis → Memcached：Memcached 不是 simpler Redis、是 cache paradigm" data-link-desc="Redis → Memcached 是 Type E paradigm reduction migration — 從 multi-paradigm（KV &#43; 資料結構 &#43; pub/sub &#43; Lua &#43; streams）退到 pure cache；不是「remove Redis features」、是「重新分配 Redis-specific feature 到對應 specialized 服務」；5 個 production 踩雷 &#43; paradigm reduction 路線">Redis → Memcached（Type E paradigm reduction）</a></li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a></li>
</ul>
]]></content:encoded></item><item><title>自管 Redis / Valkey → AWS ElastiCache：engine 不變、變的是誰運維</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/migrate-to-elasticache/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>（source、自管）跟 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>（target、managed）。跑 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit&lt;/a> 對映 &lt;strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid&lt;/strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。&lt;/p>&lt;/blockquote>
&lt;h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維&lt;/h2>
&lt;p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。&lt;/p>
&lt;p>那遷的是什麼？&lt;strong>運維責任的歸屬&lt;/strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚&lt;strong>交出運維的同時、交出了哪些控制權&lt;/strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。&lt;/p>
&lt;p>這對映 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論&lt;/a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。&lt;/p>
&lt;h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>評估&lt;/th>
 &lt;th>等級&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>Schema / API&lt;/td>
 &lt;td>同 engine（Redis/Valkey）、RESP 一致、命令一致&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Operational model&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>自管 → AWS managed（failover/patch/snapshot）&lt;/strong>&lt;/td>
 &lt;td>&lt;strong>High&lt;/strong>&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Abstraction / paradigm&lt;/td>
 &lt;td>完全相同（同 engine）&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Number of components&lt;/td>
 &lt;td>1 → 1&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Application change&lt;/td>
 &lt;td>endpoint 換、client 加 reconnect / TLS、其餘不動&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Data topology&lt;/td>
 &lt;td>cache 可重建（re-warm）或 RDB seed / online 複製&lt;/td>
 &lt;td>Low&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>唯一 High 是 operational model，對映 &lt;strong>Type C operational hybrid&lt;/strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link <a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（source、自管）跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a>（target、managed）。跑 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration-playbook-methodology 6 維 audit</a> 對映 <strong>Operational model = High（自管 → managed）、其他 Low → Type C operational hybrid</strong>。ElastiCache 是 managed SaaS、AWS 操作依官方文件（未本機驗證、引數以官方為準）、最後檢查日 2026-06-16。</p></blockquote>
<h2 id="engine-不變變的是誰運維">engine 不變、變的是誰運維</h2>
<p>多數 vendor 遷移會換掉某個本質的東西——協定、data model、paradigm。自管 Redis/Valkey → ElastiCache 一個都沒換：ElastiCache 跑的就是 Redis 或 Valkey engine，同樣的 RESP 協定、同樣的 data types、同樣的 client library、同樣的命令。application code 幾乎不用動。</p>
<p>那遷的是什麼？<strong>運維責任的歸屬</strong>。自管時要自己部署、自己打 patch、自己設 replication、自己半夜處理 failover。ElastiCache 把這些接走——AWS 做 failover、patching、snapshot、跨 AZ 複製。這個遷移的全部工作量集中在「把運維交出去」這件事上：網路（VPC）、安全（IAM / Security Group）、cutover 的資料連續性，以及想清楚<strong>交出運維的同時、交出了哪些控制權</strong>（不再能 SSH 進機器、不能改任意 config、parameter group 限定可調項）。</p>
<p>這對映 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration 方法論</a> 的 Type C operational hybrid——operational model 是唯一的 High 維度，其他全 Low。本文展開這個「engine 不變、運維轉移」遷移的實際工作與責任邊界。</p>
<h2 id="6-維-diff-dimension-audit">6 維 diff dimension audit</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>評估</th>
          <th>等級</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Schema / API</td>
          <td>同 engine（Redis/Valkey）、RESP 一致、命令一致</td>
          <td>Low</td>
      </tr>
      <tr>
          <td><strong>Operational model</strong></td>
          <td><strong>自管 → AWS managed（failover/patch/snapshot）</strong></td>
          <td><strong>High</strong></td>
      </tr>
      <tr>
          <td>Abstraction / paradigm</td>
          <td>完全相同（同 engine）</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Number of components</td>
          <td>1 → 1</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Application change</td>
          <td>endpoint 換、client 加 reconnect / TLS、其餘不動</td>
          <td>Low</td>
      </tr>
      <tr>
          <td>Data topology</td>
          <td>cache 可重建（re-warm）或 RDB seed / online 複製</td>
          <td>Low</td>
      </tr>
  </tbody>
</table>
<p>唯一 High 是 operational model，對映 <strong>Type C operational hybrid</strong>。Type C 的結構是「operational audit 前置 + drop-in cutover」——因為 engine/API 不變，cutover 本身接近 drop-in（換 endpoint），重點在前置的網路/安全/責任邊界盤點。</p>
<h2 id="operational-auditcutover-前要盤點的">operational audit：cutover 前要盤點的</h2>
<p>ElastiCache 把運維接走，但也劃下新的邊界。cutover 前必盤：</p>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>自管時的負責項</th>
          <th>ElastiCache 後</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署 / patch</td>
          <td>自己裝、自己升級</td>
          <td>AWS 管（失去任意版本控制、跟 AWS 的 engine 版本走）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自己設 Sentinel / 手動切</td>
          <td>Multi-AZ 自動（需確保 client 會重連）</td>
      </tr>
      <tr>
          <td>config</td>
          <td>改任意 redis.conf</td>
          <td>只能改 parameter group 開放的項（部分鎖死）</td>
      </tr>
      <tr>
          <td>網路存取</td>
          <td>自己的網路</td>
          <td>只在 VPC 內可達、要設 subnet group / Security Group</td>
      </tr>
      <tr>
          <td>認證</td>
          <td>AUTH password / 自管 TLS</td>
          <td>IAM auth（Redis 7+）/ ElastiCache 管的 TLS</td>
      </tr>
      <tr>
          <td>監控</td>
          <td>自己的 Prometheus 等</td>
          <td>CloudWatch（指標名與自管不同、dashboard 要改）</td>
      </tr>
  </tbody>
</table>
<p><strong>audit 的關鍵 output</strong>：(1) 目前改了哪些 redis.conf 項、ElastiCache parameter group 是否支援；(2) client 是否有 failover reconnect 邏輯（managed failover 不會代為重連）；(3) 監控要從自管工具搬到 CloudWatch。這三項是 Type C 的核心工作。詳細的 managed 責任邊界見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a>。</p>
<h2 id="cutover資料連續性的兩條路">cutover：資料連續性的兩條路</h2>
<p>因為 engine/API 不變，cutover 接近 drop-in（換 endpoint）。資料連續性有兩條路：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln"> 1</span><span class="cl">路徑 A：re-warm（cache 可重建、最簡單）
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">  1. 建 ElastiCache cluster（空的、選 Valkey / Redis engine、設 parameter group）
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">  2. application 雙寫（自管 + ElastiCache）、讀仍走自管
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">  3. 讀切到 ElastiCache endpoint、cache miss 回源 warm up
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">  4. 命中率追上 → 停寫自管 → 下線自管
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">路徑 B：RDB seed（要 cache 連續性、避免 warm-up origin 衝擊）
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">  1. 自管端 BGSAVE 產生 RDB
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">  2. RDB 上傳 S3、ElastiCache 從 S3 seed 建 cluster（依官方 restore 流程）
</span></span><span class="line"><span class="ln">10</span><span class="cl">  3. application 換 endpoint cutover
</span></span><span class="line"><span class="ln">11</span><span class="cl">  （ElastiCache 也提供 self-managed Redis online migration、見官方文件）</span></span></code></pre></div><p>判讀：</p>
<ul>
<li>純 cache、能接受短暫 warm-up → 路徑 A（最簡單、無資料遷移）</li>
<li>大 dataset、warm-up 會打爆 origin → 路徑 B（RDB seed 保連續性）</li>
<li>AWS CLI 建 cluster 與 restore 細節依 <a href="https://docs.aws.amazon.com/AmazonElastiCache/latest/dg/">ElastiCache 官方文件</a>（未本機驗證）</li>
<li>engine 選 Valkey（AWS default、約低 Redis 20%）除非有 Redis 商業 module 依賴</li>
</ul>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1parameter-group-不支援自管時改的-config">Case 1：parameter group 不支援自管時改的 config</h3>
<p><strong>徵兆</strong>：自管時改了某個 redis.conf 項（例如特定 <code>client-output-buffer-limit</code> 或某個進階參數），遷到 ElastiCache 後該設定無法套用或行為不同。</p>
<p><strong>根因</strong>：ElastiCache 只允許改 parameter group 開放的項，部分 config 被 AWS 鎖死（為了 managed 穩定性）。自管時的任意 config 自由度在 managed 後收窄。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>pre-migration 列出自管端所有非預設 config，逐項對照 ElastiCache parameter group 支援度</li>
<li>不支援的項要評估影響——有些是 AWS 已用更好的方式處理、有些要調整 application 適應</li>
<li>把這個盤點放在 operational audit（cutover 前），不要遷完才發現</li>
<li>高度依賴特殊 config 調校的場景，managed 可能不適合、留自管</li>
</ol>
<h3 id="case-2failover-後-client-不重連managed-不代為重連">Case 2：failover 後 client 不重連（managed 不代為重連）</h3>
<p><strong>徵兆</strong>：ElastiCache Multi-AZ failover 完成，但 application 持續連舊 primary、寫入失敗。</p>
<p><strong>根因</strong>：ElastiCache 接走了 failover（自動晉升 replica），但 application 的 client 重連仍是 application 端的責任——這是 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">managed 責任邊界</a> 的核心：AWS 換 primary，client 要自己跟上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>client 連 primary endpoint（會跟著 failover 更新 DNS）、不寫死 node IP</li>
<li>client 設合理 socket timeout + retry + 縮短 DNS 快取</li>
<li>遷移前就驗證 client 有 failover reconnect 行為（自管 Sentinel 時可能靠不同機制）</li>
<li>對應 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover 時序</a>：自管與 managed 的 failover 機制不同、client 處理要重驗</li>
</ol>
<h3 id="case-3endpoint-只在-vpc-內cutover-後連不上">Case 3：endpoint 只在 VPC 內、cutover 後連不上</h3>
<p><strong>徵兆</strong>：cutover 後 application 完全連不上 ElastiCache、連線逾時。</p>
<p><strong>根因</strong>：ElastiCache endpoint 只在 VPC 內可達、不對公網開放。Security Group 沒開 6379、subnet group 配置錯、或 application 不在同 VPC / 沒有 VPC peering，就連不上。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前確認 Security Group 開 6379 給 application 的來源、subnet group 正確</li>
<li>application 不在同 VPC 要設 peering / Transit Gateway</li>
<li>從 VPC 內 EC2 先 <code>redis-cli -h &lt;endpoint&gt; ping</code> 驗證連通，再切 application</li>
<li>這是自管（自己的網路）→ managed（AWS VPC 模型）最常見的卡點</li>
</ol>
<h3 id="case-4監控斷層自管工具--cloudwatch">Case 4：監控斷層（自管工具 → CloudWatch）</h3>
<p><strong>徵兆</strong>：cutover 後原本的 Prometheus / Grafana dashboard 全空、告警失效。</p>
<p><strong>根因</strong>：自管時用 redis_exporter + Prometheus，ElastiCache 的指標在 CloudWatch、指標名與維度不同。直接搬 dashboard 不會動。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>cutover 前把關鍵告警在 CloudWatch 重建（<code>DatabaseMemoryUsagePercentage</code> / <code>ReplicationLag</code> / <code>CurrConnections</code> 等）</li>
<li>要保留 Grafana 可用 CloudWatch data source 接</li>
<li>把監控遷移納入 operational audit、不要遷完才發現沒監控</li>
<li>核心指標語意相同（記憶體 / 命中 / 連線 / 複製延遲）、只是來源與命名變了</li>
</ol>
<h3 id="case-5以為-managed-就不會-oom--stampede--熱-key">Case 5：以為 managed 就不會 OOM / stampede / 熱 key</h3>
<p><strong>徵兆</strong>：遷到 ElastiCache 後仍然 OOM、cache stampede、熱 key 打爆單 shard。</p>
<p><strong>根因</strong>：ElastiCache 接走的是運維（failover/patch/snapshot），不是 cache 使用方式的問題。記憶體淘汰、stampede、熱 key、key 設計仍是 application 端的責任——managed 不等於 hands-off。</p>
<p><strong>修法</strong>：</p>
<ol>
<li>記憶體 / eviction 調校仍要做（透過 parameter group 設 maxmemory-policy），見 <a href="/blog/backend/02-cache-redis/vendors/redis/memory-eviction-tuning/" data-link-title="Redis 記憶體與淘汰調校：maxmemory-policy、LFU 與碎片化的實戰判讀" data-link-desc="Redis 的記憶體是一條會在半夜爆掉的曲線：maxmemory 設多少、policy 選 LRU 還 LFU、碎片化什麼時候開始吃掉 30% RAM、OOM 時 noeviction 怎麼讓寫入全部失敗。本文展開 Redis 記憶體會計模型、eviction policy 的選型判讀、5 個把記憶體配置寫成 production 事故的踩坑，以及單機記憶體撞牆後該往 cluster 還是 DragonflyDB 走的邊界">記憶體調校</a></li>
<li>stampede / 熱 key 的 application 端防護（jitter / singleflight / 兩層 cache）照舊</li>
<li>釐清 managed 的責任邊界——左欄 AWS 管、右欄 application 端管，見 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">責任邊界 deep article</a></li>
<li>遷 managed 是減運維、不是免設計</li>
</ol>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>自管 Redis / Valkey</th>
          <th>ElastiCache（managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>engine / API</td>
          <td>同（Redis / Valkey）</td>
          <td>同（Redis / Valkey engine）</td>
      </tr>
      <tr>
          <td>運維責任</td>
          <td>全部自己扛</td>
          <td>failover / patch / snapshot 交 AWS</td>
      </tr>
      <tr>
          <td>config 自由度</td>
          <td>任意 redis.conf</td>
          <td>parameter group 開放項（部分鎖死）</td>
      </tr>
      <tr>
          <td>failover</td>
          <td>自設 Sentinel / Cluster</td>
          <td>Multi-AZ 自動（client 要會重連）</td>
      </tr>
      <tr>
          <td>成本</td>
          <td>機器 + 人力運維</td>
          <td>node 費 + managed premium（省人力）</td>
      </tr>
      <tr>
          <td>控制權</td>
          <td>完全</td>
          <td>受 AWS 邊界限制</td>
      </tr>
      <tr>
          <td>適合</td>
          <td>要極致控制 / 跨雲 / 特殊 config</td>
          <td>AWS 生態 / 要減運維 / 可預測 SLA</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：在 AWS 生態、要把運維交出去、能接受 config 自由度收窄 → 遷 ElastiCache（engine 不變、Type C 低風險）；要極致控制 / 跨雲 / 依賴特殊 config → 留自管。engine 選 Valkey 省約 20%。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<p>self-managed → ElastiCache 是運維轉移，它跟 managed 邊界與 engine 調校交織：</p>
<ul>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界 deep article</a></strong>：遷過去後哪些 AWS 管、哪些仍 application 端管，是這個遷移的核心後果。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/redis/sentinel-ha-failover/" data-link-title="Redis Sentinel 與 failover 時序：從 master 死掉到 client 重連的每一段" data-link-desc="Redis Sentinel 的 failover 不是一個瞬間動作，是 down 偵測 → quorum 確認 → 選主 → 提升 → 配置廣播 → client 重連的一條時序鏈，每一段都有自己的延遲與失敗模式。本文展開 Sentinel 的判定模型與這條時序、5 個讓 failover 卡住或丟資料的 production 踩坑，以及 Sentinel 撐不住該往 Cluster 或 managed 走的邊界">Redis Sentinel failover</a></strong>：自管 failover（Sentinel）→ managed failover（Multi-AZ），client 重連邏輯要重驗。</li>
<li><strong>跟 <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></strong>：ElastiCache default engine 是 Valkey，自管 Redis 遷 ElastiCache for Valkey 是「換授權 + 轉 managed」一次到位（見 <a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey 遷移</a>）。</li>
<li><strong>跟<a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">能力級買 vs 建</a></strong>：自管 vs managed 的上層取捨見該章，本文是「決定買（managed）之後」的遷移執行。</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source vendor：<a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a> / <a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a>（自管）</li>
<li>Target vendor：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></li>
<li>對應 deep article：<a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/managed-responsibility-boundary/" data-link-title="AWS ElastiCache 的責任邊界：managed 接手了什麼、又默默留下什麼" data-link-desc="ElastiCache 把 failover、patching、snapshot、跨 AZ 複製接走，但 cache stampede、client 重連、key 設計、eviction policy 還是你的事。本文用 shared responsibility 拆解 managed 的真實邊界、展開 engine 選擇與 cluster mode 配置、5 個把『以為 AWS 全包』寫成事故的 production 踩坑，以及 ElastiCache 到 MemoryDB 的 durability 邊界">ElastiCache 責任邊界</a></li>
<li>相關 migration：<a href="/blog/backend/02-cache-redis/vendors/redis/migrate-to-valkey/" data-link-title="Redis → Valkey：同一份程式碼、不同授權的 drop-in 遷移" data-link-desc="Valkey 是 Redis 7.2.4 的 fork，bit-for-bit 幾乎同源、RDB/AOF 檔案相容、client 一行不改——這是技術上最容易的 cache 遷移。真正的工作不在搬資料，在授權合規驗證與 fork 後分歧（Redis 7.4&#43; 功能、Stack 商業 module）的盤點。本文走 Type B drop-in、相容性 audit 前置、5 個把『最容易的遷移』寫成事故的踩坑">Redis → Valkey</a>（換授權 + 可同時轉 managed）</li>
<li>Methodology：<a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">Migration playbook methodology</a>（Type C operational hybrid）</li>
</ul>
]]></content:encoded></item><item><title>9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/</link><pubDate>Tue, 12 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/</guid><description>&lt;p>這個案例的核心責任是說明「ML feature store 的延遲敏感層」工程選型。即時推薦（首頁 carousel、播放後下一支）需要在 100ms 內生成、ML inference 之前的 feature lookup 通常吃 30-50ms — 把 lookup 壓到 10ms 以下、整個推薦延遲才有預算空間。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Tubi 在 ElastiCache 的關鍵敘述（引自 &lt;a href="https://aws.amazon.com/elasticache/customers/">ElastiCache Customers&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>數字&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>工作負載&lt;/td>
 &lt;td>ML inference feature store&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>p99 延遲&lt;/td>
 &lt;td>&amp;lt; 10 ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>遷移路徑&lt;/td>
 &lt;td>ScyllaDB → ElastiCache for Redis&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>業務場景&lt;/td>
 &lt;td>串流推薦（free streaming service）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Tubi 案例揭露三個 ML feature store 容量設計重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>feature store 是 ML inference 的 critical path&lt;/strong>：每個推薦請求都要查 N 個 feature（user_profile、item_metadata、recent_interactions、similar_users 等）、每個 feature 查詢都吃 latency budget。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a> 的多 stage budget 分解。&lt;/li>
&lt;li>&lt;strong>ScyllaDB → ElastiCache 是「持久 KV → 純 cache」的權衡&lt;/strong>：ScyllaDB 是 Cassandra-compatible 高吞吐 KV、提供 durability；ElastiCache 是 in-memory cache、可以 cache miss。Tubi 選 cache 是判斷「feature 可以重新計算」、durability 不必、純 in-memory 更快。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache vs durable store 選型。&lt;/li>
&lt;li>&lt;strong>p99 才是 ML 系統的容量門檻&lt;/strong>：ML 系統的 user-perceived latency 是 &lt;em>最後完成的 inference&lt;/em>、不是平均。p50 快沒用、p99 慢用戶就看到 loading spinner。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery&lt;/a> 的 latency percentile 分析、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase&lt;/a> 的長尾延遲議題同類。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p>
&lt;ul>
&lt;li>「sub-10ms p99」沒指明 &lt;em>p999 / p9999&lt;/em>。p9999 通常比 p99 高一個量級、會出現在實際 user-perceived 體驗。&lt;/li>
&lt;li>ElastiCache 的 sub-10ms 是 &lt;em>cache hit 路徑&lt;/em> — cache miss 路徑會回到 ScyllaDB 或重新計算、延遲可能 100ms+。容量規劃要考慮 cache hit rate 跟 miss recovery 兩條路徑。&lt;/li>
&lt;/ul>
&lt;h2 id="策略">策略&lt;/h2>
&lt;p>可重用的工程做法：&lt;/p>
&lt;ol>
&lt;li>&lt;strong>ML feature store 用「兩層 cache」設計&lt;/strong>：L1 是 in-process cache（最熱的 features）、L2 是 ElastiCache / Memcached（次熱）、L3 才是持久 store（ScyllaDB / DynamoDB / S3 + Parquet）。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組&lt;/a> 的 cache hierarchy。&lt;/li>
&lt;li>&lt;strong>feature 可重算 → 用 cache、feature 必須持久 → 用 store&lt;/strong>：判斷依據是「重算成本」跟「資料一致性需求」。對應 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary&lt;/a>。&lt;/li>
&lt;li>&lt;strong>p99 / p999 反推單個 stage latency 上限&lt;/strong>：每個 stage（network、cache lookup、feature aggregation、model inference、response serialization）給一個 latency budget、總和等於整體 SLO。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget&lt;/a>、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &amp;#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase&lt;/a> 同樣的反推思維。&lt;/li>
&lt;/ol>
&lt;p>跨平台等效：AWS ElastiCache for Redis / Valkey / MemoryDB、GCP Memorystore for Redis、Azure Cache for Redis 都可實作對等架構。專為 ML feature store 設計的還有 Feast / Tecton / Hopsworks 等開源 + 商業方案、底層常用 Redis-compatible store。&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明「ML feature store 的延遲敏感層」工程選型。即時推薦（首頁 carousel、播放後下一支）需要在 100ms 內生成、ML inference 之前的 feature lookup 通常吃 30-50ms — 把 lookup 壓到 10ms 以下、整個推薦延遲才有預算空間。</p>
<h2 id="觀察">觀察</h2>
<p>Tubi 在 ElastiCache 的關鍵敘述（引自 <a href="https://aws.amazon.com/elasticache/customers/">ElastiCache Customers</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>數字</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>工作負載</td>
          <td>ML inference feature store</td>
      </tr>
      <tr>
          <td>p99 延遲</td>
          <td>&lt; 10 ms</td>
      </tr>
      <tr>
          <td>遷移路徑</td>
          <td>ScyllaDB → ElastiCache for Redis</td>
      </tr>
      <tr>
          <td>業務場景</td>
          <td>串流推薦（free streaming service）</td>
      </tr>
  </tbody>
</table>
<h2 id="判讀">判讀</h2>
<p>Tubi 案例揭露三個 ML feature store 容量設計重點。</p>
<ol>
<li><strong>feature store 是 ML inference 的 critical path</strong>：每個推薦請求都要查 N 個 feature（user_profile、item_metadata、recent_interactions、similar_users 等）、每個 feature 查詢都吃 latency budget。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a> 的多 stage budget 分解。</li>
<li><strong>ScyllaDB → ElastiCache 是「持久 KV → 純 cache」的權衡</strong>：ScyllaDB 是 Cassandra-compatible 高吞吐 KV、提供 durability；ElastiCache 是 in-memory cache、可以 cache miss。Tubi 選 cache 是判斷「feature 可以重新計算」、durability 不必、純 in-memory 更快。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache vs durable store 選型。</li>
<li><strong>p99 才是 ML 系統的容量門檻</strong>：ML 系統的 user-perceived latency 是 <em>最後完成的 inference</em>、不是平均。p50 快沒用、p99 慢用戶就看到 loading spinner。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a> 的 latency percentile 分析、跟 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> 的長尾延遲議題同類。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>「sub-10ms p99」沒指明 <em>p999 / p9999</em>。p9999 通常比 p99 高一個量級、會出現在實際 user-perceived 體驗。</li>
<li>ElastiCache 的 sub-10ms 是 <em>cache hit 路徑</em> — cache miss 路徑會回到 ScyllaDB 或重新計算、延遲可能 100ms+。容量規劃要考慮 cache hit rate 跟 miss recovery 兩條路徑。</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>ML feature store 用「兩層 cache」設計</strong>：L1 是 in-process cache（最熱的 features）、L2 是 ElastiCache / Memcached（次熱）、L3 才是持久 store（ScyllaDB / DynamoDB / S3 + Parquet）。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的 cache hierarchy。</li>
<li><strong>feature 可重算 → 用 cache、feature 必須持久 → 用 store</strong>：判斷依據是「重算成本」跟「資料一致性需求」。對應 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">02.4 cache copy freshness boundary</a>。</li>
<li><strong>p99 / p999 反推單個 stage latency 上限</strong>：每個 stage（network、cache lookup、feature aggregation、model inference、response serialization）給一個 latency budget、總和等於整體 SLO。對應 <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a>、跟 <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> 同樣的反推思維。</li>
</ol>
<p>跨平台等效：AWS ElastiCache for Redis / Valkey / MemoryDB、GCP Memorystore for Redis、Azure Cache for Redis 都可實作對等架構。專為 ML feature store 設計的還有 Feast / Tecton / Hopsworks 等開源 + 商業方案、底層常用 Redis-compatible store。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>想規劃 ML feature store → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.12 SLO 與 Performance Budget</a></li>
<li>想做 p99 / p999 反推 → <a href="/blog/backend/09-performance-capacity/cases/coinbase-ultra-low-latency-exchange-2023/" data-link-title="9.C3 Coinbase International Exchange：超低延遲交易的逆向容量設計" data-link-desc="為什麼 Coinbase 國際交易所選 Cluster Placement Group &#43; z1d 而不是自動擴容 — 延遲敏感型負載的容量取捨">9.C3 Coinbase</a> + <a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.4 Saturation Discovery</a></li>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a>（配對引擎）</li>
<li>想理解 cache hierarchy → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://aws.amazon.com/elasticache/customers/">Amazon ElastiCache Customers</a></li>
<li><a href="https://aws.amazon.com/blogs/database/build-an-ultra-low-latency-online-feature-store-for-real-time-inferencing-using-amazon-elasticache-for-redis/">Build an ultra-low latency online feature store for real-time inferencing using Amazon ElastiCache for Redis</a></li>
</ul>
]]></content:encoded></item><item><title>MongoDB Connection Management and Cache Layer：driver × 部署模型 × cache × predictive scaling</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/connection-management-and-cache-layer/</guid><description>&lt;p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。&lt;/p>
&lt;p>本文不重複 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview&lt;/a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。&lt;/p>
&lt;h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆&lt;/h2>
&lt;p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：&lt;/p>
&lt;p>&lt;strong>Connection ceiling&lt;/strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。&lt;/p>
&lt;p>&lt;strong>Read scaling ceiling&lt;/strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&amp;gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。&lt;/p>
&lt;p>&lt;strong>Scaling reaction lag&lt;/strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。&lt;/p>
&lt;p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。&lt;/p>
&lt;p>讀者徵兆：&lt;/p>
&lt;ul>
&lt;li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限&lt;/li>
&lt;li>p99 read latency 在事件時段集體爬&lt;/li>
&lt;li>Atlas auto-scaling event log 顯示 &lt;em>triggered too late&lt;/em>&lt;/li>
&lt;li>Cache hit rate 跟 read latency 反向相關&lt;/li>
&lt;/ul>
&lt;p>Case anchor：&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &amp;#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase&lt;/a> 是 rich case，含具體數字（deploy 尖峰 &lt;em>connection event rate&lt;/em> ~60K connections / 分鐘 / mongobetween 後 &lt;em>steady-state concurrent connections&lt;/em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a> 雙模式負載敘事（持續 sensor + 緊急事件）、&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes&lt;/a> 媒體爆量形狀。&lt;/p></description><content:encoded><![CDATA[<p>MongoDB 大規模 OLTP 的真實架構不是「一個 driver pool 直連 cluster」、是 driver / proxy 層 + cache + freshness token 層 + scaling trigger 層三層協作。讀者最常的誤解是「Coinbase 用 MongoDB 撐 1.5M reads/sec」— 實際是這個合成架構撐出來的量級、單靠 MongoDB cluster 拿不到那個數字。本文把三層各自議題跟整合操作流程講清楚、並對 mongobetween 的部署模型適用範圍給出明確邊界。</p>
<p>本文不重複 <a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> 的 Atlas / 容量規劃簡介 — 而是 production 部署 + 跨層協作 + 失敗修復的實作層教學。</p>
<h2 id="問題情境大規模-oltp-撞三道牆">問題情境：大規模 OLTP 撞三道牆</h2>
<p>MongoDB 部署規模從中型撐到大規模時、會連環撞三道牆：</p>
<p><strong>Connection ceiling</strong>：應用層 deploy 規模一上來、單一 MongoDB cluster 看到 connection storm。9.C36 Coinbase 揭露具體：Ruby + GVL + blue-green 部署把 instance 數 ×2、連線數隨之 ×2、單一 cluster 看到 60K connections / 分鐘（口徑：Coinbase 特定環境 CRuby + GVL 部署模型）。MongoDB cluster 的 connection limit 撞牆、新 deploy 連不上、線上服務 cascade 失敗。</p>
<p><strong>Read scaling ceiling</strong>：讀者把所有 read 都打 secondary、replica 加到 5-7 仍撐不住 sustained 高 read（&gt;500K reads/sec）。Replication lag 升 + secondary CPU 飽和；單靠 MongoDB cluster 內機制（replica scaling + read preference）拿不到大規模量級。</p>
<p><strong>Scaling reaction lag</strong>：MongoDB cluster 擴容是天級議題、不是即時擴容。9.C36 Coinbase 揭露 reactive scaling 起點到完成 ~70 分鐘（口徑：Coinbase 特定環境、cluster tier / 資料量 / Atlas API 條件下、非 MongoDB 普遍承諾）。Surge 開始時才動來不及、預測性流量必須提前出手。</p>
<p>Surge 形狀又不規則：加密貨幣 surge（隨外部市場波動）/ 媒體爆量（事件驅動）/ IoT 緊急通報（雙模式並存）— 都不適合單純 reactive auto-scaling 接住、必須 predictive + reactive 兩段式。</p>
<p>讀者徵兆：</p>
<ul>
<li>MongoDB Atlas console 看到 connection count 在 deploy 後 spike 到上限</li>
<li>p99 read latency 在事件時段集體爬</li>
<li>Atlas auto-scaling event log 顯示 <em>triggered too late</em></li>
<li>Cache hit rate 跟 read latency 反向相關</li>
</ul>
<p>Case anchor：<a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> 是 rich case，含具體數字（deploy 尖峰 <em>connection event rate</em> ~60K connections / 分鐘 / mongobetween 後 <em>steady-state concurrent connections</em> 由 ~30K 降到 ~2K — 兩者口徑不同、不是同一數字的連續變化；1.5M reads/sec 含 cache / 70 → 25 分鐘擴容）；<a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 雙模式負載敘事（持續 sensor + 緊急事件）、<a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> 媒體爆量形狀。</p>
<h2 id="核心機制三層合成-frame">核心機制：三層合成 frame</h2>
<p>跨案合成 frame（本章合成、case 原文沒這個 frame）：應用層連 MongoDB cluster 在大規模 production 是 <em>三層協作</em>、不是 driver 一個元件：</p>
<table>
  <thead>
      <tr>
          <th>層次</th>
          <th>角色</th>
          <th>9.C36 Coinbase 對應元件</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Driver / Proxy</td>
          <td>連線多工、應用 process 跟 cluster 的橋接</td>
          <td>MongoDB driver + mongobetween proxy</td>
      </tr>
      <tr>
          <td>Cache + freshness token</td>
          <td>read scaling 主路、跨層一致性協議</td>
          <td>Memcached + freshness token + OCC version</td>
      </tr>
      <tr>
          <td>Scaling trigger</td>
          <td>cluster 擴容啟動時機</td>
          <td>ML predictive scaling + reactive fallback</td>
      </tr>
  </tbody>
</table>
<p>三層缺一都會在大規模時撞牆。本文聚焦這三層如何協作、單一層的深度議題（read preference 機制、schema 治理、aggregation pipeline）推到 sibling。</p>
<h3 id="driver--proxy-層">Driver / Proxy 層</h3>
<p>MongoDB driver 原生 connection 模式：driver 在 application process 內維護 connection pool、每個 process 跟 MongoDB cluster 開固定數量 socket。但 driver <strong>沒跨 process pool</strong> — 多個 process 共用同一台機器、每個 process 自己一份 pool、cluster 看到的是 N 倍 connection。跟 PostgreSQL 走 pgbouncer 是同樣需求。</p>
<p>Connection storm 的具體 trigger：</p>
<ul>
<li><strong>部署模型放大 process 數</strong>：CRuby + GVL 強制每 CPU core 一 process、blue-green 部署 instance 數 ×2、連線數隨之 ×2（9.C36 Coinbase 揭露：單 cluster 看到 60K connections/min）</li>
<li><strong>微服務數量多</strong>：50+ microservice 各自連 cluster、每服務 connection 加總後撞上限（9.C37 Forbes 50+ 微服務情境對照）</li>
</ul>
<p>mongobetween proxy（Coinbase 自建）：把多 application process 的連線合成少量到 MongoDB cluster 的連線。9.C36 揭露兩個獨立口徑、不是同一數字的連續變化：deploy 尖峰時 <em>connection event rate</em> 是 ~60K connections / 分鐘（unique connection 事件量、rate）；mongobetween 介入後 <em>steady-state concurrent connection 數</em> 由 ~30K 降到 ~2K（瞬時量、前後對比、一個量級）。引用時把 rate 跟瞬時 concurrent count 分開、不要壓成「60K 收斂到 2K」。</p>
<p><strong>Scope warning（必明示）</strong>：mongobetween 是 Coinbase 為 Ruby + GVL 需求自建、case 自承「Go / Java / Node.js 應用因原生支援連線多工、通常不需要這層 proxy」。寫進設計文件時不可寫成「MongoDB 在大規模都需要 mongobetween」、要寫成「特定部署模型才需要」。</p>
<h3 id="cache--freshness-token-層">Cache + freshness token 層</h3>
<p>直接打 MongoDB 不可能撐 1.5M reads/sec（口徑：users 服務應用層觀察、含 cache、非 MongoDB cluster 純讀取）。Coinbase 在 users 服務前面加 Memcached query cache、單 document query 先查 cache。</p>
<p>跨層一致性問題：write 進 MongoDB primary、cache 還是舊版、user 下次 read 拿到舊資料。</p>
<p><a href="/blog/backend/knowledge-cards/freshness-token/" data-link-title="Freshness Token" data-link-desc="DB write 後返回的版本 token、後續 read 帶 token、保證 read 看到的資料 ≥ token 版本、解 DB &#43; cache 跨層 read-after-write">Freshness Token</a> 機制：</p>
<ol>
<li>Write 成功後給 client token（含 OCC version / clusterTime）</li>
<li>Client read 帶 token</li>
<li>Server 保證返回的資料版本 ≥ token</li>
<li>必要時 bypass cache 直接打 DB</li>
</ol>
<p>跟 DB 層 causal consistency session 對照：causal session 解 MongoDB 內 read-your-own-write、freshness token 解 <em>DB + cache 跨層</em> read-your-own-write。機制細節見 <a href="../replica-set-read-preference/">replica set read preference</a>、本文不重複展開。</p>
<p><strong>Scope warning（必明示）</strong>：1.5M reads/sec 是 <em>users 服務 + cache</em> 合成數字、不是 MongoDB cluster 純讀取 benchmark。寫進設計文件必須明示口徑、避免讀者把 1.5M reads/sec 當成「MongoDB 單獨能撐」。</p>
<h3 id="scaling-trigger-層">Scaling trigger 層</h3>
<p>MongoDB cluster 擴容時間：傳統 reactive scaling 起點到完成 ~70 分鐘（9.C36 Coinbase 揭露口徑：含 instance provisioning + 資料 sync + balancer rebalance、特定 Atlas tier / 資料量條件）。</p>
<p>Reactive 為主撐不住快變流量：CPU / queue 觸發 reactive scaling 在 surge 開始時才動、來不及；surge 已經結束擴容才到位。</p>
<p>Predictive scaling 機制（Coinbase 揭露）：</p>
<ul>
<li>用外部訊號（加密貨幣價格、賽事行程、票務開賣時間）訓練 ML 模型</li>
<li>提前 60 分鐘預測流量</li>
<li>預先擴容</li>
<li>把擴容啟動時間從 70 分鐘壓到 25 分鐘（口徑：trigger 提前、不是擴容本身變快）</li>
</ul>
<p><strong>Scope warning（必明示）</strong>：case 警示「ML 預測有 false positive / false negative、Coinbase 沒揭露準確率、所以仍保留 reactive scaling 作為 safety net」。寫進設計文件要明示兩段式設計、不可寫成「Predictive scaling 取代 reactive scaling」。</p>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/connection-pool/" data-link-title="Connection Pool" data-link-desc="說明連線池如何限制下游資源並影響服務容量">connection-pool</a>、<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/session-consistency/" data-link-title="Session Consistency" data-link-desc="同一使用者工作階段內維持讀寫一致、跨工作階段允許短暫不一致">session-consistency</a>、<a href="/blog/backend/knowledge-cards/hot-partition/" data-link-title="Hot Partition" data-link-desc="說明分散式 KV / OLTP 中、單一 partition 流量遠超其他的容量問題">hot-partition</a>（cache 失效時打穿 DB 的 hot key）。</p>
<h2 id="操作流程">操作流程</h2>
<p><strong>Step 1：connection ceiling audit</strong>。量測現有 deploy 在 peak 的 connection count、推算 deploy ×2 / 微服務新增時 connection 走勢；對照 MongoDB cluster 的 hard limit（Atlas tier 決定、典型 1500-32000）。</p>
<p><strong>Step 2：部署模型判讀</strong>。</p>
<table>
  <thead>
      <tr>
          <th>部署模型</th>
          <th>是否需 proxy 層</th>
          <th>原因</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>CRuby + GVL（process-per-core）</td>
          <td>需要</td>
          <td>每 core 一 process、連線隨 process 線性升</td>
      </tr>
      <tr>
          <td>大量微服務（50+）+ 各自 deploy</td>
          <td>需要</td>
          <td>微服務 connection 加總撞 cluster limit</td>
      </tr>
      <tr>
          <td>Blue-green 部署（雙環境並存）</td>
          <td>需要</td>
          <td>部署期間連線 ×2、容易撞 cluster ceiling</td>
      </tr>
      <tr>
          <td>Go / Java / Node.js 單一 binary + 多 thread</td>
          <td>通常不需要</td>
          <td>原生 driver pool 跨 thread 共用、收斂效率高</td>
      </tr>
  </tbody>
</table>
<p><strong>Step 3：proxy 選型</strong>。Coinbase mongobetween 是參考實作、社群還有 mongoproxy / DocumentDB 內建 connection multiplexer。自建 proxy 是 Coinbase 規模才合理、中型團隊先評估 Atlas tier 升級。</p>
<p><strong>Step 4：cache layer 設計</strong>（read scaling 主路）：</p>
<ul>
<li>前置 Memcached / Redis、cache key = collection + document id + version</li>
<li>Write API 返回 <code>{result, version_token}</code> — token 含 OCC version 或 MongoDB clusterTime</li>
<li>Read API 接受 optional version token、cache lookup 比對 entry version 跟 token、低於就 invalidate + bypass</li>
<li>DB 層 fallback <code>readConcern: &quot;majority&quot;</code> 保證返回 version ≥ token</li>
</ul>
<p><strong>Step 5：predictive scaling 設計</strong>（適用「外部訊號可預測流量」）：</p>
<ul>
<li><strong>識別 driver 訊號</strong>：加密貨幣價格 / 賽事行程 / 票務開賣 / 促銷活動 / IoT 緊急事件預警</li>
<li><strong>訓練 ML</strong>：用歷史流量 vs 訊號 correlation 訓練、輸出未來 30-60 分鐘流量預測</li>
<li><strong>觸發擴容</strong>：預測超 threshold 時主動 trigger Atlas scaling API、不等 reactive metric</li>
<li><strong>保留 reactive safety net</strong>：ML failure 時 reactive scaling 仍會接、不可拿掉</li>
</ul>
<p><strong>Step 6：全鏈路驗證</strong>。Staging 灌入 deploy ×2 模擬 connection storm、灌入 stale cache 驗證 freshness token bypass、放假流量驗證 predictive scaling trigger。</p>
<p>驗證點：</p>
<ul>
<li>Connection count 在 deploy 後不爆 cluster limit</li>
<li>Cache hit rate vs freshness bypass rate 比例正常（cache hit &gt; 90% + bypass &lt; 5% 屬通用工程估算、case 未揭露具體數字）</li>
<li>Predictive scaling 領先窗 ≥ 30 分鐘</li>
<li>Reactive scaling 仍保留作 safety</li>
</ul>
<p>Rollback boundary：</p>
<ul>
<li>Proxy 層可下線（流量改直連 cluster、但短時 connection storm 風險回來）</li>
<li>Cache 層可下線（read 全部打 DB、需 cluster 容量能撐）</li>
<li>Predictive scaling 可下線（退回純 reactive、但快變 surge 接不住）</li>
<li>三層都要設計 graceful degradation、不是全有全無</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<p><strong>Connection storm during deploy</strong>：blue-green 部署 instance 數 ×2、connection 隨之爆、新 deploy 連不上 cluster、cascade 失敗。修法是 proxy 層 + cluster connection limit 預留 headroom（典型留 30% buffer、屬通用工程估算）。</p>
<p><strong>Proxy 變成單點瓶頸</strong>：mongobetween / pgbouncer 風格 proxy 自己變熱點、proxy 故障時下游全死。修法是 proxy 叢集 + health check + 客戶端 retry、跟 application 同 region 共部署降低 proxy ↔ application 的網路 RTT。</p>
<p><strong>Cache hit rate 崩塌</strong>：cache 失效 + 大量 read bypass、DB 突然吃 100% 流量、cluster 飽和。修法是 freshness token 設計時要監控 bypass rate、過高表示 cache invalidation 邏輯有問題、cache 沒在 write 後 update / invalidate。</p>
<p><strong>Freshness token 漏寫</strong>：write 沒帶 token / client 沒帶 token、token silently 失效、user 拿到舊資料。修法是 protocol 強制（middleware 攔截 write / read、自動帶 token）、不能靠 application 自覺。</p>
<p><strong>Predictive scaling false positive 浪費容量</strong>：ML 預測 surge 但實際沒來、cluster 預先擴容後閒置。接受成本、保留 ML model retraining、定期評估 precision / recall。</p>
<p><strong>Predictive scaling false negative 漏接 surge</strong>：ML 沒預測到、cluster 沒提前擴、surge 來時 reactive scaling 開始動但 70 分鐘來不及。修法是 reactive safety net + 服務降級（限流 / 部分 read 降級拿舊資料 + freshness token 告警）。</p>
<p><strong>三層協作脫節</strong>：proxy 擋住 connection storm 但 cluster 內部 read scaling 沒設計、application 仍打爆。三層必須一起設計、不是各自獨立。</p>
<p>Anti-recommendation：</p>
<ul>
<li>中小流量（&lt; 100K reads/sec、單 deploy &lt; 50 instance）不需要這三層；Atlas tier 升級 + cluster 內 replica + 簡單 cache 就夠</li>
<li>mongobetween 風格 proxy 只在 Ruby + GVL / 類似部署模型才必要、Go / Java / Node.js 通常不需要（case 自承）</li>
<li>Predictive scaling 只在外部訊號可預測時有效；無預測訊號的純隨機 surge 還是回 reactive + headroom</li>
<li>大規模 OLTP 不該為了省成本拿掉 cache 層；read scaling 主路就是 cache、單靠 MongoDB cluster 拿不到 1.5M reads/sec 量級</li>
</ul>
<h2 id="容量與觀測">容量與觀測</h2>
<p>關鍵 metric：</p>
<ul>
<li><strong>Connection 層</strong>：cluster connection count / Atlas tier limit / proxy 到 cluster 的 connection multiplex 比、deploy 前後 connection 走勢</li>
<li><strong>Cache 層</strong>：cache hit rate、freshness token bypass rate、cache key collision rate</li>
<li><strong>Scaling 層</strong>：predictive scaling trigger event count / 領先窗、reactive scaling fallback 觸發頻率、實際擴容啟動到完成時間、ML 預測準確率（precision / recall）</li>
</ul>
<p>Mongo / Atlas command：</p>
<ul>
<li><code>db.serverStatus().connections</code>：cluster 當前 connection 統計</li>
<li><code>db.currentOp({})</code>：看 connection 使用</li>
<li>Atlas API：cluster scaling event log</li>
<li>Proxy admin metric：connection multiplex 比、上下游 latency</li>
</ul>
<p>Application observability：APM 看 connection acquire latency、cache hit rate time series、freshness token 流動完整性（write 是否發 token、read 是否帶 token、cache 是否驗 token）。</p>
<p>回到 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 observability evidence</a>：把 connection storm event、cache hit rate / bypass rate、scaling trigger leadtime 列為跨層 evidence 三件套。</p>
<p>回到 <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 bottleneck localization</a>：大規模 OLTP 撞牆時要區分 (a) connection ceiling (b) cache hit rate 下降 (c) cluster 內 replica 飽和 (d) scaling 跟不上。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<p>Sibling deep articles：</p>
<ul>
<li><a href="../replica-set-read-preference/">replica set read preference</a> — DB 層 causal session 機制、freshness token 跨層協議；本文聚焦三層協作、那篇聚焦 DB 層機制</li>
<li><a href="../shard-key-selection/">shard key selection</a> — cluster 擴容是天級議題、是 scaling layer 的 trigger；單 cluster vs 多 cluster 切分</li>
<li><a href="../schema-design-pattern/">schema design pattern</a> — app-layer abstraction 跟本文 cache + freshness token 同層協作、contract layer 三選一</li>
<li><a href="../aggregation-pipeline-optimization/">aggregation pipeline optimization</a> — report dashboard 跑爆 primary 的補位路徑是本文的 cache + read scaling、不是讓 aggregation 自己優化</li>
</ul>
<p>Migration playbook：</p>
<ul>
<li><strong>Federated DB 模式</strong>（9.C36 Coinbase 揭露：MongoDB + DynamoDB）— 不是「全用 MongoDB」、document-shaped 用 MongoDB、access pattern 固定的 KV 用 DynamoDB；對應 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB vendor page</a> 跨 vendor 對照</li>
<li><strong>跨雲 hedging</strong>（9.C37 Forbes 跨雲彈性）— Atlas 跨 AWS / GCP / Azure 是規避未來雲商鎖定的 selection 訊號</li>
</ul>
<p>跟 1.x 互引：</p>
<ul>
<li><a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a> — connection storm 通用模式（pgbouncer / mongobetween 對應）</li>
<li><a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> — 三層架構列為大規模 OLTP 容量規劃必看點</li>
<li><a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a> — predictive scaling 的 ML 訓練紀律</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/mongodb/" data-link-title="MongoDB" data-link-desc="Document database 代表、Atlas managed、跨雲可用、許多大規模平台從 MongoDB 起家">MongoDB vendor overview</a> — 本文是該頁尾「connection management + Atlas scaling」backlog 的深度展開</li>
<li><a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">Vendor 深度技術文章方法論</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/coinbase-mongodb-document-platform/" data-link-title="9.C36 Coinbase：MongoDB 撐 Ruby 單體 &#43; 1.5M reads/sec identity 服務" data-link-desc="Coinbase 以 MongoDB 為主資料層、自建 mongobetween connection proxy、users 服務在加密貨幣 surge 時撐 1.5M reads/sec">9.C36 Coinbase</a> — 三層合成 rich case</li>
<li><a href="/blog/backend/09-performance-capacity/cases/forbes-mongodb-atlas-multi-cloud-migration/" data-link-title="9.C37 Forbes：自管 MongoDB → Atlas on GCP、build 時間 25 → 9 分鐘" data-link-desc="Forbes 把自管 MongoDB 遷到 Atlas on Google Cloud、6 個月完成、build 25 → 9 分鐘、120M 不重複訪客單月承接">9.C37 Forbes</a> — 媒體爆量形狀</li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> — IoT 雙模式負載</li>
<li>官方：<a href="https://www.mongodb.com/docs/manual/reference/connection-string-options/">MongoDB Connection Pool Options</a>、<a href="https://www.mongodb.com/docs/atlas/cluster-autoscaling/">Atlas Auto-Scaling</a>、<a href="https://github.com/coinbase/mongobetween">mongobetween GitHub</a></li>
</ul>
]]></content:encoded></item><item><title>9.C35 Snap：GCP + KeyDB 在 multi-cloud 架構下的低延遲快取</title><link>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/snap-gcp-keydb-cross-cloud/</guid><description>&lt;p>這個案例的核心責任是補強 GCP cache 維度、並揭示 multi-cloud 架構的隱性 latency 議題。Snap（Snapchat 母公司、日活 4 億 +）2011 年從零起就在 GCP 上、是雲原生最早期客戶之一、但近年走 multi-cloud（GCP + AWS）。這個架構引出「跨 cloud cache latency 怎麼處理」的工程議題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Snap 在 GCP 的關鍵敘述（引自 &lt;a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud&lt;/a>、&lt;a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap TPU recommendation&lt;/a>）：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>指標&lt;/th>
 &lt;th>內容&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>用戶基礎&lt;/td>
 &lt;td>4 億 + DAU、年增 18% YoY&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>開始在 GCP 時間&lt;/td>
 &lt;td>2011 年（產品早期）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Multi-cloud cache 方案&lt;/td>
 &lt;td>GCP 上部署 KeyDB cluster 減少 cross-cloud latency&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>ML training&lt;/td>
 &lt;td>TPU（vs GPU 吞吐高 67%、成本低 52%）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>安全框架&lt;/td>
 &lt;td>BeyondCorp Enterprise（Zero Trust）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵架構決策：在 &lt;em>GCP&lt;/em> 上部署 KeyDB（Redis fork、multi-threaded）作為 cache layer、減少 cross-cloud latency。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>Snap 案例揭露三個 multi-cloud 容量設計的工程重點。&lt;/p>
&lt;ol>
&lt;li>&lt;strong>跨 cloud latency 是隱性容量瓶頸&lt;/strong>：當 application 在 AWS、cache 在 GCP（或反之）、每個 cache lookup 都吃跨 cloud 網路 latency（通常 5-30ms、視 region pair 而定）。對 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Snap 這類「每次互動查多個 cache」&lt;/a> 的服務、5ms × 10 cache lookup = 50ms 額外 latency、用戶感受明顯。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget&lt;/a> 的 latency budget 反推。&lt;/li>
&lt;li>&lt;strong>KeyDB 是 Redis 的 multi-threaded 替代&lt;/strong>：Redis 7+ 之前是 single-threaded、單實例吞吐受限。KeyDB（Snap 等大型用戶採用）改成 multi-threaded、單實例 throughput 提升 5-10x、適合超高吞吐 cache 需求。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache&lt;/a> 的 cache layer 設計、但 Snap 規模更大要走專業 fork。&lt;/li>
&lt;li>&lt;strong>TPU vs GPU 是 ML training 的容量成本決策&lt;/strong>：Snap 算過 GPU 的「throughput -67% + cost +52%」就是 TPU 的反向 — TPU 的 throughput 高 67%、cost 低 52% — 對 ML-heavy 公司是巨大決策。對應 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency&lt;/a> 的雲端硬體選型、跟 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &amp;#43; 1.5 億商品、用 GCP Vertex AI Search &amp;#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre Vertex AI&lt;/a> 的 ML 容量規劃同類。&lt;/li>
&lt;/ol>
&lt;p>需要警惕：&lt;/p></description><content:encoded><![CDATA[<p>這個案例的核心責任是補強 GCP cache 維度、並揭示 multi-cloud 架構的隱性 latency 議題。Snap（Snapchat 母公司、日活 4 億 +）2011 年從零起就在 GCP 上、是雲原生最早期客戶之一、但近年走 multi-cloud（GCP + AWS）。這個架構引出「跨 cloud cache latency 怎麼處理」的工程議題。</p>
<h2 id="觀察">觀察</h2>
<p>Snap 在 GCP 的關鍵敘述（引自 <a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud</a>、<a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap TPU recommendation</a>）：</p>
<table>
  <thead>
      <tr>
          <th>指標</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶基礎</td>
          <td>4 億 + DAU、年增 18% YoY</td>
      </tr>
      <tr>
          <td>開始在 GCP 時間</td>
          <td>2011 年（產品早期）</td>
      </tr>
      <tr>
          <td>Multi-cloud cache 方案</td>
          <td>GCP 上部署 KeyDB cluster 減少 cross-cloud latency</td>
      </tr>
      <tr>
          <td>ML training</td>
          <td>TPU（vs GPU 吞吐高 67%、成本低 52%）</td>
      </tr>
      <tr>
          <td>安全框架</td>
          <td>BeyondCorp Enterprise（Zero Trust）</td>
      </tr>
  </tbody>
</table>
<p>關鍵架構決策：在 <em>GCP</em> 上部署 KeyDB（Redis fork、multi-threaded）作為 cache layer、減少 cross-cloud latency。</p>
<h2 id="判讀">判讀</h2>
<p>Snap 案例揭露三個 multi-cloud 容量設計的工程重點。</p>
<ol>
<li><strong>跨 cloud latency 是隱性容量瓶頸</strong>：當 application 在 AWS、cache 在 GCP（或反之）、每個 cache lookup 都吃跨 cloud 網路 latency（通常 5-30ms、視 region pair 而定）。對 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">Snap 這類「每次互動查多個 cache」</a> 的服務、5ms × 10 cache lookup = 50ms 額外 latency、用戶感受明顯。對應 <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a> 的 latency budget 反推。</li>
<li><strong>KeyDB 是 Redis 的 multi-threaded 替代</strong>：Redis 7+ 之前是 single-threaded、單實例吞吐受限。KeyDB（Snap 等大型用戶採用）改成 multi-threaded、單實例 throughput 提升 5-10x、適合超高吞吐 cache 需求。對應 <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> 的 cache layer 設計、但 Snap 規模更大要走專業 fork。</li>
<li><strong>TPU vs GPU 是 ML training 的容量成本決策</strong>：Snap 算過 GPU 的「throughput -67% + cost +52%」就是 TPU 的反向 — TPU 的 throughput 高 67%、cost 低 52% — 對 ML-heavy 公司是巨大決策。對應 <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界與 efficiency</a> 的雲端硬體選型、跟 <a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre Vertex AI</a> 的 ML 容量規劃同類。</li>
</ol>
<p>需要警惕：</p>
<ul>
<li>KeyDB 是 <em>fork-based</em> 軟體、有 vendor lock-in 風險（Snap 大規模採用後、KeyDB 公司被收購、未來 fork 走向不確定）</li>
<li>TPU 是 <em>Google 專屬硬體</em>、不能在其他 cloud 用、是 vendor lock-in 來源</li>
<li>「年增 18%」是用戶數、不是流量。流量成長通常超過用戶成長（per-user engagement 上升）</li>
</ul>
<h2 id="策略">策略</h2>
<p>可重用的工程做法：</p>
<ol>
<li><strong>Multi-cloud 架構優先把 cache 跟 application 放同一 cloud</strong>：跨 cloud 的不該是 cache lookup（高頻、低 latency 容忍）、應該是 batch sync（低頻、高 latency 容忍）。對應 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 的部署策略。</li>
<li><strong>Redis 規模化遇到 single-threaded 限制時的選項</strong>：
<ul>
<li>拆 cluster（多個 Redis instance）— 應用層分散 key</li>
<li>換 KeyDB / Dragonfly（multi-threaded fork）</li>
<li>換 Redis 7+ I/O thread（保留 protocol）</li>
<li>換 Memcached（multi-threaded、但功能少）</li>
</ul>
</li>
<li><strong>ML training infrastructure 選型按 throughput / cost 而非品牌</strong>：GPU vs TPU vs Trainium 不是「哪家好」、是「在 <em>本 workload</em> 上哪個划算」。要實測 benchmark、不是看 vendor marketing。</li>
<li><strong>跨 cloud 部署的「資料引力」</strong>：data 在哪、application 通常會被 data 吸過去。Snap 把 cache 放 GCP 是因為 production data 在 GCP — 想搬 cache 到 AWS 同時要搬 data、成本高。</li>
</ol>
<p>跨平台等效：AWS ElastiCache + Cassandra / DynamoDB Global Tables、Azure Cache for Redis + Cosmos DB 都可實作 multi-region cache 但 single-cloud 內。multi-cloud cache 通常要自管（自管 KeyDB / Dragonfly / Redis Cluster）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>對照其他 cache 案例 → <a href="/blog/backend/09-performance-capacity/cases/tinder-elasticache-valkey-matching/" data-link-title="9.C6 Tinder：ElastiCache for Valkey 撐 4700 萬月活的配對引擎" data-link-desc="Tinder 用 Amazon ElastiCache for Valkey 提供配對引擎所需的次毫秒延遲快取層">9.C6 Tinder ElastiCache</a> / <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi ML feature store</a></li>
<li>想設計 multi-cloud cache → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> + <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>想做 ML training 容量規劃 → <a href="/blog/backend/09-performance-capacity/cost-engineering/" data-link-title="9.7 成本邊界與 efficiency" data-link-desc="cost per request、cost curve、降級成本、over-provisioning trade-off">9.7 成本邊界</a> + <a href="/blog/backend/09-performance-capacity/cases/mercado-libre-latam-bigquery-vertex/" data-link-title="9.C31 Mercado Libre：LatAm 電商在 GCP 上用 Vertex AI 搜尋 1.5 億商品" data-link-desc="Mercado Libre 1 億客戶 &#43; 1.5 億商品、用 GCP Vertex AI Search &#43; BigQuery 提供近即時搜尋與分析">9.C31 Mercado Libre</a></li>
<li>想理解 cross-cloud latency → <a href="/blog/backend/09-performance-capacity/slo-performance-budget/" data-link-title="9.12 SLO 與 Performance Budget" data-link-desc="performance budget 跟 SLO / error budget 的對接">9.12 SLO 與 Performance Budget</a></li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://cloud.google.com/blog/products/application-modernization/snap-deploys-keydb-on-google-cloud-to-reduce-cross-cloud-latency">Snap deploys KeyDB on Google Cloud to reduce cross-cloud latency</a></li>
<li><a href="https://cloud.google.com/blog/products/ai-machine-learning/snap-inc-uses-google-cloud-tpu-for-deep-learning-recommendation-models">Snap Inc. uses Google Cloud TPU for deep learning recommendation models</a></li>
<li><a href="https://cloud.google.com/blog/products/gcp/snap-maintains-uptime-with-mcs-from-google-cloud/">Snap maintains uptime with MCS from Google Cloud</a></li>
<li><a href="https://cloud.google.com/blog/products/identity-security/why-snap-chose-beyondcorp-enterprise-to-build-a-durable-zero-trust-framework">Why Snap chose BeyondCorp Enterprise</a></li>
</ul>
]]></content:encoded></item><item><title>DynamoDB DAX 快取策略：cluster 架構、item/query cache、write-through 與 invalidation 邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/dax-caching-strategy/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>DAX 觸發條件 SSoT&lt;/strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段&lt;/a>、含 &lt;code>9.C29 Lemino&lt;/code> case fact 跟 &lt;code>9.C19 Capcom&lt;/code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取&lt;/h2>
&lt;p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。&lt;/p>
&lt;p>&lt;strong>cluster 拓樸&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica&lt;/li>
&lt;li>跨多 AZ 部署、primary 故障時 replica 接手&lt;/li>
&lt;li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>兩種快取、不同生命週期&lt;/strong>：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>熱門節目首播時段、application 對同一批 metadata item 的讀取 latency p99 從 5ms 尖到 40ms、下游 timeout 連鎖。team 加了 DAX、p99 壓回個位數毫秒。三個月後另一個 service 也「照抄」加 DAX、結果 cost 上升、latency 沒降 — 那個 service 是寫密集、每次讀的 key 都不同、cache hit rate 不到 20%。同一個工具、在一個 workload 壓回 p99 延遲、在另一個只增加成本卻不降延遲。DAX 的價值取決於 read pattern 跟一致性需求是否匹配。本文展開 DAX 的 cluster 架構、兩種快取的不同失效語意、以及 write-through 跟 strongly consistent read 的邊界。</p>
<blockquote>
<p><strong>DAX 觸發條件 SSoT</strong>：DAX 「該不該存在」的觸發條件（讀峰值持續高 / cache hit rate 可預期 / read:write ratio 高）主寫於 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design 的 DAX 段</a>、含 <code>9.C29 Lemino</code> case fact 跟 <code>9.C19 Capcom</code> derive 分層。本文承接「已決定要用 DAX」之後的機制、配置與失效邊界、不重複展開觸發判讀。</p></blockquote>
<h2 id="核心機制dax-cluster-與兩種快取">核心機制：DAX cluster 與兩種快取</h2>
<p>DAX（DynamoDB Accelerator）是 DynamoDB 前面的 in-memory write-through cache、提供 microsecond 級讀取（DynamoDB 本身是 single-digit ms）。它 API 相容 — application 把 DynamoDB client 換成 DAX client、API call 不變、讀寫自動經過 cache 層。</p>
<p><strong>cluster 拓樸</strong>：</p>
<ul>
<li>一個 DAX cluster 由多個 node 組成、一個 primary（接受寫）+ 多個 read replica</li>
<li>跨多 AZ 部署、primary 故障時 replica 接手</li>
<li>application 透過 DAX endpoint 連 cluster、SDK 自動分散讀取到 replica</li>
</ul>
<p><strong>兩種快取、不同生命週期</strong>：</p>
<table>
  <thead>
      <tr>
          <th>快取類型</th>
          <th>內容</th>
          <th>寫入如何影響</th>
          <th>失效方式</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Item cache</td>
          <td><code>GetItem</code> / <code>BatchGetItem</code> 的單筆結果</td>
          <td>write-through 寫入時同步更新對應 item</td>
          <td>item TTL + write-through</td>
      </tr>
      <tr>
          <td>Query cache</td>
          <td><code>Query</code> / <code>Scan</code> 的結果集</td>
          <td>單筆 write <em>不會</em> 失效對應 query 結果集</td>
          <td>只靠 query TTL</td>
      </tr>
  </tbody>
</table>
<p>這張表的第二列是 DAX 最常被誤解的點：<strong>query cache 不會因為底層某筆 item 被改而失效</strong>。item cache 走 write-through、寫入時會更新；但 query cache 存的是「整個結果集」、DAX 無法知道某筆新寫入是否該進某個已快取的 query 結果、所以 query cache 只靠 TTL 過期。這代表 query 結果可能 stale 到一個 TTL 週期。</p>
<blockquote>
<p><strong>Scope warning</strong>：「item cache 預設 TTL 5 分鐘」、「query cache 預設 TTL 5 分鐘」這些預設值屬 AWS vendor 規格、可在 cluster 設定調整、實作時 cross-verify 官方 doc。本文不含 production case 揭露的 DAX TTL 配置數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/cache-invalidation/" data-link-title="Cache Invalidation" data-link-desc="說明快取資料何時更新、刪除或重建，以及失效策略如何影響一致性">cache-invalidation</a>、<a href="/blog/backend/knowledge-cards/write-through-cache/" data-link-title="Write-Through Cache" data-link-desc="說明寫入時同步更新快取與正式來源的策略">write-through-cache</a>、<a href="/blog/backend/knowledge-cards/ttl/" data-link-title="TTL" data-link-desc="說明資料過期時間如何影響快取新鮮度、成本與一致性">ttl</a>、<a href="/blog/backend/knowledge-cards/cache-hit-rate/" data-link-title="Cache Hit Rate" data-link-desc="說明快取命中比例如何衡量加速效果與下游保護">cache-hit-rate</a>。</p>
<h2 id="一致性與-invalidation-邊界">一致性與 invalidation 邊界</h2>
<p>DAX 的一致性語意是它跟「一般 cache-aside」最大的差別、也是踩雷集中區。</p>
<p><strong>write-through 的保證範圍</strong>：</p>
<p>寫入經過 DAX 時、DAX 先寫 DynamoDB、成功後更新自己的 item cache。所以「寫完馬上用 <code>GetItem</code> 讀同一筆」、在 <em>同一個 DAX node</em> 上能讀到新值。但這不是 strong consistency — 多 node cluster 下、寫入只更新 primary 與被路由到的 node、其他 read replica 的 item cache 仍可能 stale 到 TTL。</p>
<p><strong>strongly consistent read 繞過 cache</strong>：</p>
<p>DAX 只服務 eventually consistent read。application 若要求 strongly consistent read（<code>ConsistentRead=True</code>）、DAX 直接 pass through 到 DynamoDB、不經 cache、也享受不到 microsecond latency。這是設計上的取捨 — DAX 換 latency 的代價是放棄 strong consistency。read-your-write 嚴格場景不能靠 DAX。</p>
<p><strong>query cache stale 的真實後果</strong>：</p>
<p>application 用 <code>Query</code> 列「某 user 的 active order」、結果被 query cache 快取；user 新建一筆 order、item cache 更新了該筆 item、但 <em>列表 query 的 cache 沒失效</em>、user 重整頁面在 TTL 內看不到新訂單。修法不是調 DAX、是判斷「這個 query 能不能接受 TTL 內 stale」— 不能接受的、該 query 不要走 DAX（直接打 DynamoDB）、或縮短該類 query 的 TTL。</p>
<blockquote>
<p><strong>Scope warning</strong>：上述一致性語意屬 DAX vendor 規格 + 通用 cache 工程知識、非 production case 揭露；實際 staleness 視 cluster node 數、TTL 配置與讀寫分布而定。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從 read pattern 評估到上線的 6 步流程。</p>
<h4 id="step-1確認-read-pattern-適配">Step 1：確認 read pattern 適配</h4>
<p>在加 DAX 前、用 CloudWatch 看目標 table 的 read:write ratio 跟 read 的 key 重複度：</p>
<ul>
<li>read:write 高（讀遠多於寫）+ 重複讀同一組 key → 適合</li>
<li>寫密集 / 每次讀不同 key / 大量 strongly consistent read → 不適合（回頭看 <a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/#dax-%e4%bd%9c%e7%82%ba%e8%ae%80%e5%b3%b0%e5%80%bc%e8%a3%9c%e4%bd%8d" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design DAX 觸發條件</a>）</li>
</ul>
<h4 id="step-2cluster-sizing">Step 2：cluster sizing</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">node 數 = 讀峰值 throughput / 單 node 容量 + 1（容錯餘量）
</span></span><span class="line"><span class="ln">2</span><span class="cl">node class = 依 working set 大小選（cache 要能裝下熱資料）</span></span></code></pre></div><p>跨至少 2 個 AZ、確保 primary 故障有 replica 接手。</p>
<h4 id="step-3application-切換-client">Step 3：application 切換 client</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="kn">import</span> <span class="nn">amazondax</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 原本：dynamodb = boto3.resource(&#34;dynamodb&#34;)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">dax</span> <span class="o">=</span> <span class="n">amazondax</span><span class="o">.</span><span class="n">AmazonDaxClient</span><span class="o">.</span><span class="n">resource</span><span class="p">(</span><span class="n">endpoint_url</span><span class="o">=</span><span class="s2">&#34;dax://my-cluster.xxx.dax-clusters.region.amazonaws.com&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">table</span> <span class="o">=</span> <span class="n">dax</span><span class="o">.</span><span class="n">Table</span><span class="p">(</span><span class="s2">&#34;orders&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"># API 不變、讀寫自動經過 DAX</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="s2">&#34;ORDER#123&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">})</span></span></span></code></pre></div><h4 id="step-4分流-strongly-consistent-read">Step 4：分流 strongly consistent read</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 需要 strong 的讀直接走 DynamoDB、不要走 DAX</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="n">ddb_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">,</span> <span class="n">ConsistentRead</span><span class="o">=</span><span class="kc">True</span><span class="p">)</span>   <span class="c1"># 繞過 cache</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 可接受 eventual 的讀走 DAX</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="n">dax_table</span><span class="o">.</span><span class="n">get_item</span><span class="p">(</span><span class="n">Key</span><span class="o">=...</span><span class="p">)</span>                          <span class="c1"># 走 cache</span></span></span></code></pre></div><p>application 要明確區分哪些讀路徑能接受 stale、哪些不能；不能接受的不走 DAX。</p>
<h4 id="step-5設定-ttl-與監控-hit-rate">Step 5：設定 TTL 與監控 hit rate</h4>
<p>依資料變動頻率設 item / query cache TTL：變動慢的 metadata 可設長 TTL、變動快的設短或不快取。上線後盯 <code>CacheHitRate</code>。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 驗證 hit rate 達預期、確認 DAX 真的減少 DynamoDB 讀</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># CloudWatch: DAX CacheHits / (CacheHits + CacheMisses)</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"># 同時看 DynamoDB ConsumedReadCapacityUnits 是否下降</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：DAX 可隨時 detach — application 端把 DAX endpoint 換回 DynamoDB endpoint 即可、無資料遷移；DAX 只是讀路徑加速層、不持有唯一資料。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1把-dax-當預設配置">Case 1：把 DAX 當預設配置</h4>
<p>寫密集 / 低 hit rate workload 加 DAX、invalidation 開銷 + cluster 成本 &gt; cache 收益。修法：先確認 read pattern 適配（Step 1）、DAX 是讀峰值補位不是預設（觸發條件 SSoT 在 gsi-lsi-design）。</p>
<h4 id="case-2以為-query-cache-會即時反映寫入">Case 2：以為 query cache 會即時反映寫入</h4>
<p>寫入後列表 query 在 TTL 內看不到新資料、被當成 bug 長時間誤查。修法：理解 query cache 只靠 TTL 失效（不是 bug 是設計）；強一致列表需求的 query 不走 DAX、或縮短 TTL。</p>
<h4 id="case-3strongly-consistent-read-全走-dax-還抱怨不快">Case 3：strongly consistent read 全走 DAX 還抱怨不快</h4>
<p>application 全程 <code>ConsistentRead=True</code>、DAX 全部 pass through、等於沒裝 DAX 還多付 cluster 錢。修法：分流 — strong read 直接打 DynamoDB、eventual read 才走 DAX。</p>
<h4 id="case-4cluster-單-az--單-node">Case 4：cluster 單 AZ / 單 node</h4>
<p>省成本只開單 node、primary 故障時讀路徑整個失效、回退到 DynamoDB 瞬間流量尖峰。修法：跨 2+ AZ、primary + replica；DAX 故障的 fallback 路徑（直連 DynamoDB）要先測過。這個 Case 的失敗代價跟其他 Case 不對稱 — 其餘 Case 多是成本浪費或延遲沒降、detach DAX 即可回復；單 AZ / 單 node 故障是讀路徑硬中斷、回退瞬間把原本被 cache 吸收的讀峰值全打回 DynamoDB、若 base table 的 RCU 或 on-demand burst 餘量沒預留、會引發 throttling 連鎖。回退路徑要按「DAX 全失效時的讀峰值」預估 DynamoDB 側容量、而非平時被 cache 削減後的讀量。</p>
<h4 id="case-5working-set-超過-cache-容量">Case 5：working set 超過 cache 容量</h4>
<p>熱資料超過 node memory、cache 不斷 evict、hit rate 掉到沒意義。修法：依 working set 選 node class、或縮小快取範圍（只快取真正熱的 access pattern）。</p>
<p><strong>Anti-recommendation</strong>：read:write ratio 低、或 cache hit rate 預期 &lt; 50% 的 workload、不要上 DAX；application 端的 request-level cache 或根本不快取可能更划算。DAX 是 cluster 常駐成本（instance-hour 計）、只在讀峰值持續高才回本。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>CacheHits</code> / <code>CacheMisses</code> / 算出 <code>CacheHitRate</code> — 核心健康指標</li>
<li><code>ItemCacheHits</code> / <code>QueryCacheHits</code> — 分辨兩種快取各自的命中</li>
<li><code>CPUUtilization</code> / <code>EvictedSize</code> — node 是否過載、cache 是否頻繁 evict</li>
<li>DynamoDB 端 <code>ConsumedReadCapacityUnits</code> — 確認 DAX 真的削減了 base 讀取</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>CacheHitRate</code> &lt; 70% — 重新評估 DAX 是否該存在、或快取範圍是否該收窄</li>
<li><code>EvictedSize</code> 持續高 — working set 超過 cache 容量、要加大 node class</li>
<li>DynamoDB read capacity 沒因 DAX 下降 — read pattern 不適配、DAX 沒發揮作用</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「70% hit rate 閾值」屬通用工程估算、非 case 揭露；實際閾值依 cost 結構與 latency 目標調整。</p></blockquote>
<p>接回 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>、<a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="dax-vs-application-side-cache-vs-elasticache">DAX vs application-side cache vs ElastiCache</h3>
<p>DAX 不是唯一的 DynamoDB 讀加速方案。三者責任不同：</p>
<ul>
<li><strong>DAX</strong>：DynamoDB 專屬、API 相容、write-through、零 application cache 邏輯；綁 DynamoDB</li>
<li><strong>application-side cache</strong>（如 in-process LRU）：最低延遲、但每個 instance 各自一份、一致性難管</li>
<li><strong>ElastiCache（Redis / Valkey）</strong>：通用 cache、可跨資料源、但要自己寫 cache-aside 邏輯與 invalidation</li>
</ul>
<p>當快取需求超出單一 DynamoDB table（跨資料源聚合 / 需要 Redis 資料結構如 sorted set leaderboard）、回 <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> 評估 ElastiCache；DAX 最適配的情境是「純 DynamoDB 讀加速、且不想自行維護 cache 邏輯」。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/gsi-lsi-design/" data-link-title="DynamoDB GSI 與 LSI 設計：access pattern 補位、projection、consistency 跟 DAX 補位" data-link-desc="GSI / LSI 是 single-table 沒覆蓋的 access pattern 補位、不是萬靈丹；本文涵蓋 projection 三型選擇、sparse index、GSI 自己會 hot partition、DAX 讀峰值補位的觸發條件（含 Capcom 是 derive vs Lemino 是 case fact 的分層）">gsi-lsi-design</a> — DAX 觸發條件 SSoT（讀峰值補位 / Lemino case fact / Capcom derive）在該篇、本篇承接機制層</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand-vs-provisioned</a> — DAX 削減 base 讀取後、provisioned RCU 規劃要重算</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — strongly consistent read 繞過 DAX、對應 read 一致性軸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — DAX 不解 hot partition、寫熱點仍打到 DynamoDB</li>
<li>替代路由：跨資料源快取 / Redis 資料結構需求 → <a href="/blog/backend/02-cache-redis/" data-link-title="模組二：快取與 Redis" data-link-desc="整理快取策略、Redis 資料型別與分散式狀態輔助能力">02 快取模組</a> ElastiCache</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/ntt-docomo-lemino-japanese-streaming/" data-link-title="9.C29 NTT DOCOMO Lemino：3 個月達 500 萬 MAU 的串流後端" data-link-desc="Lemino 用 DynamoDB &#43; AWS Media Services 撐 30 channels live &#43; 5M MAU、工程工時下降 90%">Lemino 9.C29</a> 互引：DAX 讀峰值補位的 case fact</li>
</ul>
]]></content:encoded></item><item><title>模組二案例正文</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/</link><pubDate>Thu, 07 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/cases/</guid><description>&lt;p>這個資料夾的核心責任是把快取與 Redis 的轉換壓力寫成可回寫正文，而不是只列工具名稱。&lt;/p>
&lt;h2 id="案例列表">案例列表&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>章節&lt;/th>
 &lt;th>主題&lt;/th>
 &lt;th>核心責任&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1&lt;/a>&lt;/td>
 &lt;td>Meta cache 一致性升級&lt;/td>
 &lt;td>把 invalidation 不一致問題轉成可觀測與可治理流程&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2&lt;/a>&lt;/td>
 &lt;td>Meta mcrouter 快取路由&lt;/td>
 &lt;td>把單叢集快取演進到跨區路由與失效隔離&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3&lt;/a>&lt;/td>
 &lt;td>Shopify 快取序列化遷移&lt;/td>
 &lt;td>把快取 payload 格式遷移做成雙軌相容與回退&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4&lt;/a>&lt;/td>
 &lt;td>Meta CacheLib 分層快取&lt;/td>
 &lt;td>把 DRAM-only 快取演進到記憶體/快閃分層架構&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5&lt;/a>&lt;/td>
 &lt;td>Shopify write-through&lt;/td>
 &lt;td>把 read-heavy 路徑轉成寫入同步快取策略&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6&lt;/a>&lt;/td>
 &lt;td>Netflix EVCache&lt;/td>
 &lt;td>把本地快取演進成跨區分散式快取層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7&lt;/a>&lt;/td>
 &lt;td>Cloudflare Cache Reserve&lt;/td>
 &lt;td>把邊緣快取延伸到持久層降低回源壓力&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8&lt;/a>&lt;/td>
 &lt;td>Meta TAO&lt;/td>
 &lt;td>把 graph cache 演進成可擴展的一致性資料層&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9&lt;/a>&lt;/td>
 &lt;td>反例：快取切換失敗&lt;/td>
 &lt;td>快取策略切換若無防線會觸發 stampede 與回源雪崩&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10&lt;/a>&lt;/td>
 &lt;td>對照：規模差異下快取策略&lt;/td>
 &lt;td>小中大型服務用同一快取策略會造成不同失敗型態&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table></description><content:encoded><![CDATA[<p>這個資料夾的核心責任是把快取與 Redis 的轉換壓力寫成可回寫正文，而不是只列工具名稱。</p>
<h2 id="案例列表">案例列表</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>主題</th>
          <th>核心責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cache-consistency-upgrade/" data-link-title="2.C1 Meta：Cache Consistency 升級" data-link-desc="快取 invalidation 一致性如何從常見錯誤演進到高可信治理。">2.C1</a></td>
          <td>Meta cache 一致性升級</td>
          <td>把 invalidation 不一致問題轉成可觀測與可治理流程</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-mcrouter-global-cache-routing/" data-link-title="2.C2 Meta：mcrouter 與跨區快取路由" data-link-desc="快取從單點最佳化演進到分散式路由層的案例。">2.C2</a></td>
          <td>Meta mcrouter 快取路由</td>
          <td>把單叢集快取演進到跨區路由與失效隔離</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-cache-serialization-migration/" data-link-title="2.C3 Shopify：快取序列化格式遷移" data-link-desc="快取 payload 從 Marshal 轉 MessagePack 的遷移策略。">2.C3</a></td>
          <td>Shopify 快取序列化遷移</td>
          <td>把快取 payload 格式遷移做成雙軌相容與回退</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-cachelib-kangaroo-tiered-cache/" data-link-title="2.C4 Meta：CacheLib / Kangaroo 分層快取" data-link-desc="快取從 DRAM-only 轉向分層快取架構的實務案例。">2.C4</a></td>
          <td>Meta CacheLib 分層快取</td>
          <td>把 DRAM-only 快取演進到記憶體/快閃分層架構</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/shopify-write-through-cache-at-scale/" data-link-title="2.C5 Shopify：Write-through Cache 在高讀流量的實作" data-link-desc="read-heavy 服務如何轉向 write-through 快取模型。">2.C5</a></td>
          <td>Shopify write-through</td>
          <td>把 read-heavy 路徑轉成寫入同步快取策略</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/netflix-evcache-global-cache-layer/" data-link-title="2.C6 Netflix：EVCache 全域快取層" data-link-desc="快取從本地層演進為跨區分散式能力的案例。">2.C6</a></td>
          <td>Netflix EVCache</td>
          <td>把本地快取演進成跨區分散式快取層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/cloudflare-cache-reserve-tiered-storage/" data-link-title="2.C7 Cloudflare：Cache Reserve 分層儲存快取" data-link-desc="邊緣快取延伸到持久層以降低回源壓力的案例。">2.C7</a></td>
          <td>Cloudflare Cache Reserve</td>
          <td>把邊緣快取延伸到持久層降低回源壓力</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/meta-tao-social-graph-cache-evolution/" data-link-title="2.C8 Meta：TAO 社交圖快取演進" data-link-desc="社交圖查詢在規模化下如何把快取做成資料層能力。">2.C8</a></td>
          <td>Meta TAO</td>
          <td>把 graph cache 演進成可擴展的一致性資料層</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/failure-cache-stampede-rollout-regression/" data-link-title="2.C9 反例：快取切換引發 Stampede 回歸" data-link-desc="快取策略切換若缺乏保護，會導致回源壓力與錯誤率連鎖上升。">2.C9</a></td>
          <td>反例：快取切換失敗</td>
          <td>快取策略切換若無防線會觸發 stampede 與回源雪崩</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/cases/contrast-cache-strategy-by-scale/" data-link-title="2.C10 對照：規模差異下的快取策略" data-link-desc="同一快取策略在小中大型服務下會產生不同風險。">2.C10</a></td>
          <td>對照：規模差異下快取策略</td>
          <td>小中大型服務用同一快取策略會造成不同失敗型態</td>
      </tr>
  </tbody>
</table>
]]></content:encoded></item><item><title>快取 Vendor 清單</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/</link><pubDate>Fri, 01 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/</guid><description>&lt;p>快取 Vendor 清單的核心責任是把 cache 服務名稱放回副本語意、資料新鮮度、回源保護與操作成本的判斷。每個服務頁先回答它承擔哪種暫存責任，再討論資料型別、失效策略、容量模型、HA / managed 邊界與案例回寫。在挑單一服務之前先有一個更上層的判斷：這塊快取能力該自管 Redis、用 managed cache（ElastiCache、MemoryDB）、還是用 serverless cache（Upstash）或含 cache 的 BaaS bundle — 逐能力的買 vs 建判讀見 &lt;a href="https://tarrragon.github.io/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建&lt;/a>。&lt;/p>
&lt;h2 id="讀法">讀法&lt;/h2>
&lt;p>快取服務要從資料生命週期進入。讀者如果要保護資料庫讀取壓力，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside&lt;/a>；如果要判斷 TTL 與淘汰，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction&lt;/a>；如果服務已經把 cache 當主要 serving layer，先回到 &lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary&lt;/a>。&lt;/p>
&lt;h2 id="教學順序同步">教學順序同步&lt;/h2>
&lt;p>快取服務頁的教學順序是先建立 Redis / Valkey baseline，再比較 Memcached、DragonflyDB 與 managed cache。這個順序對齊 checkout E2：讀者先理解可重建副本、新鮮度與回源保護，再比較同類服務如何改變相容性、memory model、failover 與 managed operation。&lt;/p>
&lt;h2 id="t1-服務頁大綱">T1 服務頁大綱&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>服務&lt;/th>
 &lt;th>類型&lt;/th>
 &lt;th>頁面要回答的核心問題&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis&lt;/a>&lt;/td>
 &lt;td>Data structure cache&lt;/td>
 &lt;td>data types、persistence、cluster 與授權變動如何影響選型&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey&lt;/a>&lt;/td>
 &lt;td>Redis-compatible&lt;/td>
 &lt;td>Redis 相容性、開源治理與 managed ecosystem 如何取捨&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached&lt;/a>&lt;/td>
 &lt;td>Simple KV cache&lt;/td>
 &lt;td>純快取、低語意與水平擴張如何降低操作成本&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB&lt;/a>&lt;/td>
 &lt;td>Redis-compatible&lt;/td>
 &lt;td>多核心架構、相容性與高吞吐 cache workload 如何評估&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;a href="https://tarrragon.github.io/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache&lt;/a>&lt;/td>
 &lt;td>Managed cache&lt;/td>
 &lt;td>managed Redis / Valkey / Memcached 如何轉移維運責任&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h2 id="內容覆蓋進度">內容覆蓋進度&lt;/h2>
&lt;p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板&lt;/a>）跟 migration playbook（跨 vendor 遷移流程、走 &lt;a href="https://tarrragon.github.io/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構&lt;/a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。&lt;/p></description><content:encoded><![CDATA[<p>快取 Vendor 清單的核心責任是把 cache 服務名稱放回副本語意、資料新鮮度、回源保護與操作成本的判斷。每個服務頁先回答它承擔哪種暫存責任，再討論資料型別、失效策略、容量模型、HA / managed 邊界與案例回寫。在挑單一服務之前先有一個更上層的判斷：這塊快取能力該自管 Redis、用 managed cache（ElastiCache、MemoryDB）、還是用 serverless cache（Upstash）或含 cache 的 BaaS bundle — 逐能力的買 vs 建判讀見 <a href="/blog/backend/00-service-selection/capability-buy-vs-build/" data-link-title="0.22 能力級買 vs 建：feature-as-a-service 與 BaaS bundle 選型" data-link-desc="在交付形態決定整個系統要不要自建之後、逐能力判斷該外包還是自建：辨識 managed 基礎設施、feature SaaS 與 BaaS bundle 三種外包深度、no-code 到 dev-tool 的服務光譜、買 vs 建判準與權重浮動、整合接縫與遷出代價">0.22 能力級買 vs 建</a>。</p>
<h2 id="讀法">讀法</h2>
<p>快取服務要從資料生命週期進入。讀者如果要保護資料庫讀取壓力，先回到 <a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a>；如果要判斷 TTL 與淘汰，先回到 <a href="/blog/backend/02-cache-redis/ttl-eviction/" data-link-title="2.3 TTL 與 eviction" data-link-desc="整理過期策略、容量控制與熱點資料">2.3 TTL 與 eviction</a>；如果服務已經把 cache 當主要 serving layer，先回到 <a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary</a>。</p>
<h2 id="教學順序同步">教學順序同步</h2>
<p>快取服務頁的教學順序是先建立 Redis / Valkey baseline，再比較 Memcached、DragonflyDB 與 managed cache。這個順序對齊 checkout E2：讀者先理解可重建副本、新鮮度與回源保護，再比較同類服務如何改變相容性、memory model、failover 與 managed operation。</p>
<h2 id="t1-服務頁大綱">T1 服務頁大綱</h2>
<table>
  <thead>
      <tr>
          <th>服務</th>
          <th>類型</th>
          <th>頁面要回答的核心問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/redis/" data-link-title="Redis" data-link-desc="OSS in-memory data structure store、cache 主流">Redis</a></td>
          <td>Data structure cache</td>
          <td>data types、persistence、cluster 與授權變動如何影響選型</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/valkey/" data-link-title="Valkey" data-link-desc="Redis fork、Linux Foundation 託管、BSD 授權">Valkey</a></td>
          <td>Redis-compatible</td>
          <td>Redis 相容性、開源治理與 managed ecosystem 如何取捨</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/memcached/" data-link-title="Memcached" data-link-desc="純記憶體 key-value cache、無持久化">Memcached</a></td>
          <td>Simple KV cache</td>
          <td>純快取、低語意與水平擴張如何降低操作成本</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/dragonflydb/" data-link-title="DragonflyDB" data-link-desc="高效能 Redis / Memcached 相容替代、多核架構">DragonflyDB</a></td>
          <td>Redis-compatible</td>
          <td>多核心架構、相容性與高吞吐 cache workload 如何評估</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/02-cache-redis/vendors/aws-elasticache/" data-link-title="AWS ElastiCache" data-link-desc="AWS managed Redis / Valkey / Memcached">AWS ElastiCache</a></td>
          <td>Managed cache</td>
          <td>managed Redis / Valkey / Memcached 如何轉移維運責任</td>
      </tr>
  </tbody>
</table>
<h2 id="內容覆蓋進度">內容覆蓋進度</h2>
<p>每個 vendor 服務頁下會擴充兩類文章：deep article（vendor 自身的配置、故障、容量、走 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">6-section 模板</a>）跟 migration playbook（跨 vendor 遷移流程、走 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">6-type 結構</a>）。「→ X」代表遷移到 X 的 playbook、其他形式代表 same-vendor 的 topology / version / config 變動。</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>Deep article</th>
          <th>Migration playbook</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="redis/">Redis</a></td>
          <td><a href="redis/memory-eviction-tuning/">memory-eviction-tuning</a> / <a href="redis/persistence-fork-latency/">persistence-fork-latency</a> / <a href="redis/sentinel-ha-failover/">sentinel-ha-failover</a> / <a href="redis/connection-pipeline-latency/">connection-pipeline-latency</a> / <a href="redis/cluster-resharding/">cluster-resharding</a></td>
          <td><a href="redis/migrate-to-valkey/">→ Valkey</a> / <a href="redis/migrate-to-dragonflydb/">→ DragonflyDB</a> / <a href="redis/migrate-to-memcached/">→ Memcached</a> / <a href="redis/migrate-to-elasticache/">→ ElastiCache</a></td>
      </tr>
      <tr>
          <td><a href="valkey/">Valkey</a></td>
          <td><a href="valkey/redis-compatibility-and-io-threads/">redis-compatibility-and-io-threads</a></td>
          <td>（沿用 Redis → ElastiCache：自管 Valkey 同路徑）</td>
      </tr>
      <tr>
          <td><a href="memcached/">Memcached</a></td>
          <td><a href="memcached/slab-allocator-memory-economics/">slab-allocator-memory-economics</a></td>
          <td><a href="memcached/migrate-to-redis/">→ Redis</a></td>
      </tr>
      <tr>
          <td><a href="dragonflydb/">DragonflyDB</a></td>
          <td><a href="dragonflydb/shared-nothing-multicore-architecture/">shared-nothing-multicore-architecture</a></td>
          <td><a href="dragonflydb/migrate-to-redis/">→ Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="aws-elasticache/">AWS ElastiCache</a></td>
          <td><a href="aws-elasticache/managed-responsibility-boundary/">managed-responsibility-boundary</a></td>
          <td><a href="aws-elasticache/migrate-to-self-managed/">→ 自管 Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="keydb/">KeyDB</a></td>
          <td><a href="keydb/active-active-replication/">active-active-replication</a></td>
          <td><a href="keydb/migrate-to-redis/">→ Redis/Valkey</a></td>
      </tr>
      <tr>
          <td><a href="momento/">Momento</a></td>
          <td>overview-only（見下方註）</td>
          <td>—</td>
      </tr>
      <tr>
          <td><a href="caffeine/">Caffeine</a></td>
          <td><a href="caffeine/two-tier-cache-invalidation/">two-tier-cache-invalidation</a></td>
          <td>—</td>
      </tr>
  </tbody>
</table>
<p>備註：<a href="redis/cluster-resharding/">cluster-resharding</a> 是同 cluster 的 topology 重劃（5 type migration 漏類驗證、形式上歸在 deep article 欄、不是跨 vendor 遷移）。</p>
<p>Momento overview-only 的理由：Momento 是 serverless cache、實作面（無 server 參數、無容量規劃、無 cluster topology）相對薄；本 blog case 庫無 Momento production case、且 SaaS 無法本機驗證。依 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">deep article 方法論</a> 反向判準（無 production 經驗 / case 支撐的純 spec 復述不該寫 deep article），Momento 維持 overview-only、待有 case 或 serverless cost 實證再評估。</p>
<p>進度（2026-06-22）：8 個 vendor 的 deep-article 層收尾完成。Migration playbook 覆蓋更新：ElastiCache → 自管 Redis/Valkey、DragonflyDB → Redis/Valkey、KeyDB → Redis/Valkey 三條回退路徑已補齊。目前只剩 Momento（overview-only、無 migration 需求）跟 Caffeine（local cache、無跨 vendor migration 概念）的 migration 欄為空白、屬設計決策。剩餘獨立 track：各 T1 vendor 進階主題的更多 deep article（Redis distributed lock / modules、Memcached CAS、ElastiCache Global Datastore DR）、後續候選的 Garnet / Hazelcast / Aerospike / Varnish edge cache。</p>
<h2 id="服務頁撰寫欄位">服務頁撰寫欄位</h2>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>快取服務頁要保留的問題</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務責任</td>
          <td>它承擔 cache copy、data structure、presence、counter 還是 managed operation</td>
      </tr>
      <tr>
          <td>適用壓力</td>
          <td>hot key、read QPS、origin cost、latency、multi-region、memory cost 哪個壓力最明顯</td>
      </tr>
      <tr>
          <td>替代邊界</td>
          <td>同類 Redis 相容服務、Memcached、managed cache、local cache 的機會成本</td>
      </tr>
      <tr>
          <td>操作成本</td>
          <td>memory sizing、eviction、backup、failover、cluster upgrade、client compatibility</td>
      </tr>
      <tr>
          <td>Evidence</td>
          <td>hit rate、miss rate、origin QPS、stale read、eviction、hot key、replication lag</td>
      </tr>
      <tr>
          <td>案例回寫</td>
          <td>Meta、Shopify、Netflix、Cloudflare、Tinder、Tubi、Snap 案例如何提供判準</td>
      </tr>
  </tbody>
</table>
<p>服務責任段要先分辨副本與正式狀態。Redis、Valkey、DragonflyDB 與 ElastiCache 都可能承擔 cache serving layer，但資料是否可重建、stale window 多長、回源壓力是否受控，才是選型判斷的起點。</p>
<p>適用壓力段要保留 workload 語言。商品詳情、session、presence、rate limit、leaderboard、ML feature store 與 edge cache 的資料形狀不同，服務頁要用各自的 freshness、memory、QPS 與回退條件寫。</p>
<h2 id="服務頁標準章節">服務頁標準章節</h2>
<table>
  <thead>
      <tr>
          <th>章節</th>
          <th>快取服務頁要補的內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>服務定位</td>
          <td>它是 data structure cache、simple KV、managed cache、local cache 還是 HTTP cache</td>
      </tr>
      <tr>
          <td>本章目標</td>
          <td>讀者能判斷資料是否可重建、stale window、origin protection 與 memory cost</td>
      </tr>
      <tr>
          <td>最短判讀路徑</td>
          <td>用「miss 打回 origin 是否可承受」快速判斷是否能引入或擴大快取</td>
      </tr>
      <tr>
          <td>日常操作與決策形狀</td>
          <td>key design、TTL、eviction、warmup、failover、client timeout</td>
      </tr>
      <tr>
          <td>核心取捨表</td>
          <td>Redis 相容服務、Memcached、managed cache、local cache 的機會成本</td>
      </tr>
      <tr>
          <td>進階主題</td>
          <td>cluster、persistence、multi-region、serverless cache、module / data type</td>
      </tr>
      <tr>
          <td>排錯與失敗快速判讀</td>
          <td>hit rate、miss rate、origin QPS、hot key、eviction、replication lag</td>
      </tr>
      <tr>
          <td>何時改走其他服務</td>
          <td>durable workflow 轉 queue、正式狀態轉 database、全文查詢轉 search</td>
      </tr>
      <tr>
          <td>不在本頁內的主題</td>
          <td>Redis command 百科、語言 client API 細節、完整調參手冊</td>
      </tr>
      <tr>
          <td>案例回寫與下一步路由</td>
          <td>回到 2.C cases、9.C cache capacity cases、4.20 evidence package</td>
      </tr>
  </tbody>
</table>
<h2 id="跨-vendor-議題對照">跨 vendor 議題對照</h2>
<p>橫向議題在不同 vendor 用不同 mechanism 達成。本表列同一議題在 5 個 vendor 的對應位置、確保大綱不缺漏、讀者跨 vendor 查找時有索引。</p>
<table>
  <thead>
      <tr>
          <th>議題</th>
          <th>Redis</th>
          <th>Valkey</th>
          <th>Memcached</th>
          <th>DragonflyDB</th>
          <th>AWS ElastiCache</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Redis API 相容度</td>
          <td>原生（最高）</td>
          <td>100%（fork 7.2.4）</td>
          <td>不相容（純 KV）</td>
          <td>高（少數 commands 不支援）</td>
          <td>Engine 決定（Redis/Valkey 100%、Memcached 不適用）</td>
      </tr>
      <tr>
          <td>Data types</td>
          <td>6 大 + Stream / Geo</td>
          <td>跟 Redis 一致</td>
          <td>純 string KV</td>
          <td>跟 Redis 一致</td>
          <td>跟 engine 一致</td>
      </tr>
      <tr>
          <td>多核 / 多執行緒</td>
          <td>I/O threads（main 仍單線）</td>
          <td>Valkey 8 強化 async I/O threading（超出 Redis）</td>
          <td>原生多執行緒</td>
          <td>完全 shared-nothing 多核</td>
          <td>跟 engine 一致</td>
      </tr>
      <tr>
          <td>Cluster mode</td>
          <td>Cluster + Sentinel</td>
          <td>跟 Redis 一致</td>
          <td>Client-side ketama hashing</td>
          <td>Single instance scale-up（無 Cluster）</td>
          <td>Cluster mode enabled/disabled</td>
      </tr>
      <tr>
          <td>持久化策略</td>
          <td>AOF + RDB</td>
          <td>跟 Redis 一致</td>
          <td>無持久化</td>
          <td>Fork-less snapshot</td>
          <td>Automatic + manual snapshot</td>
      </tr>
      <tr>
          <td>跨 AZ / 多 region</td>
          <td>Sentinel + replication / Cluster geo</td>
          <td>跟 Redis 一致</td>
          <td>需 Mcrouter / EVCache 等代理</td>
          <td>Replica 模式</td>
          <td>Multi-AZ + Global Datastore</td>
      </tr>
      <tr>
          <td>授權模式</td>
          <td>RSALv2 / SSPL（非 OSI）</td>
          <td>BSD 3-clause（OSI）</td>
          <td>BSD（OSI）</td>
          <td>BSL（4 年後轉 Apache 2.0）</td>
          <td>AWS managed pricing</td>
      </tr>
      <tr>
          <td>Managed level</td>
          <td>自管</td>
          <td>自管 / managed Valkey 可選</td>
          <td>自管</td>
          <td>自管（無 Dragonfly managed）</td>
          <td>Fully managed</td>
      </tr>
      <tr>
          <td>主討論案例</td>
          <td>2.C1-C8（跨 Meta / Netflix / Shopify）</td>
          <td>待補（fork 較新）</td>
          <td>2.C4 Mcrouter / 2.C5 EVCache</td>
          <td>待補（採用較新）</td>
          <td>2.C5 EVCache / 2.C8 Shopify</td>
      </tr>
  </tbody>
</table>
<p>對照表的用途有三：</p>
<ul>
<li>寫某 vendor 頁時、檢查橫向議題是否都有對應的進階主題子段</li>
<li>讀者在 vendor 間遷移時、知道對應 mechanism 在另一個 vendor 叫什麼</li>
<li>評估遷移風險：相容度 + 授權 + 生態三維度合併判讀</li>
</ul>
<p>下面 8 段把對照表的每行展開、避免裸表格成為終點。</p>
<h3 id="redis-api-相容度">Redis API 相容度</h3>
<p>API 相容度決定 client / 工具 / module 是否能直接遷移。<strong>Redis</strong> 是 reference 實作；<strong>Valkey</strong> 100% 相容（直接 drop-in、所有 client library 可用）；<strong>DragonflyDB</strong> 相容核心 commands 但部分 module / Lua 行為差異、不支援 Redis Cluster mode；<strong>Memcached</strong> 跟 Redis 完全不相容（protocol 不同、無 data types）；<strong>ElastiCache</strong> 取決於 engine（Redis / Valkey 100%、Memcached 是另一條線）。</p>
<p>選型判讀：既有 Redis 部署遷移 → Valkey 最低風險；要 scale up single instance → DragonflyDB 可評估但確認 module 跟 Cluster mode 影響；純 KV 從 Redis 改 Memcached → 等同重寫（不是相容問題、是 capability 差異）。</p>
<h3 id="data-types">Data types</h3>
<p>Data types 影響可用場景。<strong>Redis / Valkey</strong> 提供 string / hash / list / set / sorted set / stream / hyperloglog / geo — leaderboard / session / counter / distributed lock 等都有原生支援；<strong>Memcached</strong> 純 string KV — 任何複雜結構要在 application 層自己處理（serialize JSON 等）；<strong>DragonflyDB</strong> 跟 Redis 一致；<strong>ElastiCache</strong> 取決於 engine。</p>
<p>選型判讀：需要 sorted set / streams / hash → Redis 系列；純 cache GET/SET → Memcached 更輕；想用 Redis data types 但要極高 throughput → DragonflyDB。</p>
<h3 id="多核--多執行緒">多核 / 多執行緒</h3>
<p>多核利用度差異大。<strong>Redis</strong> 主執行緒 + I/O threads（Redis 6+）— main thread 仍處理所有 command；<strong>Valkey</strong> 8.x 強化 async I/O threading、把更多 I/O 路徑非同步化、多核吞吐超出 Redis（這是 Valkey fork 後第一個實質技術分歧、見 <a href="/blog/backend/02-cache-redis/vendors/valkey/redis-compatibility-and-io-threads/" data-link-title="Valkey 相容性驗證與 io-threads 調校：drop-in 切換與多執行緒的實機判讀" data-link-desc="Valkey 跟 Redis 100% 相容這句話要怎麼驗證、切換才敢上線。本文用 INFO server 的雙版本回報拆解相容性的真實邊界、展開 Valkey 8 的 io-threads 多執行緒調校、5 個把 drop-in 切換或執行緒配置寫成事故的 production 踩坑，以及相容性撞牆該怎麼判斷的邊界">Valkey deep article</a>）；<strong>Memcached</strong> 原生 multi-threaded（<code>-t</code> 指定 thread 數）— 適合多核機器；<strong>DragonflyDB</strong> 完全 shared-nothing 多核 — 官方宣稱比 Redis 高 25× throughput（依 workload、以官方 benchmark 為準）；<strong>ElastiCache</strong> 取決於 engine、不能改變。</p>
<p>選型判讀：單 instance 想充分利用 16+ core → DragonflyDB / Memcached；4-8 core 中等場景 → Redis 加 I/O threads 已夠；需要 Redis API + 高 throughput → DragonflyDB 是 sweet spot。</p>
<h3 id="cluster-mode">Cluster mode</h3>
<p>擴展拓樸不同。<strong>Redis</strong> Cluster mode（16384 hash slot、可加減 shard）跟 Sentinel mode（HA 無 sharding）；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 沒有 server-side cluster、靠 client-side consistent hashing（ketama）；<strong>DragonflyDB</strong> 完全沒有 Cluster mode — 哲學是「single instance 撐到很大規模」；<strong>ElastiCache</strong> 提供 Cluster mode enabled / disabled 兩選項、disabled 上限 ~340GB。</p>
<p>選型判讀：超 single instance 容量 → Redis Cluster / ElastiCache Cluster enabled；HA 但容量在單 master → Redis Sentinel / ElastiCache disabled；scale up 機制 → DragonflyDB；極簡 client-side sharding → Memcached。</p>
<h3 id="持久化策略">持久化策略</h3>
<p>cache 是否需要持久化、view 不一。<strong>Redis</strong> AOF（append-only）+ RDB（snapshot）+ 混合模式；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 無持久化 — 重啟即 cold cache（嚴格 cache 哲學）；<strong>DragonflyDB</strong> fork-less snapshot（大記憶體場景比 Redis fork 高效）；<strong>ElastiCache</strong> 自動 snapshot + manual snapshot、跨 region 複製。</p>
<p>選型判讀：cache warmup 後不想全失 → Redis AOF / Valkey；純 cache 接受 cold start → Memcached；大記憶體 + snapshot 頻繁 → DragonflyDB fork-less；managed snapshot 不想處理 → ElastiCache。</p>
<h3 id="跨-az--多-region">跨 AZ / 多 region</h3>
<p>HA 拓樸三類。<strong>Redis</strong> Sentinel + replication（單 region 多 AZ）/ Cluster geo replication（規劃中）；<strong>Valkey</strong> 跟 Redis 一致；<strong>Memcached</strong> 沒有原生 — 靠 Mcrouter / EVCache 等代理做跨 AZ；<strong>DragonflyDB</strong> Replica 模式（primary-replica）跨 AZ 可行、跨 region 需自建；<strong>ElastiCache</strong> Multi-AZ replica（內建）+ Global Datastore（跨 region active-passive）。</p>
<p>選型判讀：自管跨 AZ → Redis Sentinel / Valkey；自管跨 region → Mcrouter 或自建；不想處理跨區 → ElastiCache Multi-AZ + Global Datastore。</p>
<h3 id="授權模式">授權模式</h3>
<p>授權直接影響商業使用權利。<strong>Redis</strong> 2024 起 RSALv2 / SSPL（非 OSI 認可）— SaaS 提供 Redis-as-service 受限；<strong>Valkey</strong> BSD 3-clause（OSI 認可）— 商業使用無限制；<strong>Memcached</strong> BSD（OSI）— 開源無限制；<strong>DragonflyDB</strong> BSL（Business Source License）— 4 年後轉 Apache 2.0、目前商業 managed service 提供受限；<strong>ElastiCache</strong> AWS managed pricing — 跟 license 無關（你付的是 AWS 服務費）。</p>
<p>選型判讀：開源合規敏感（公部門 / 企業政策）→ Valkey / Memcached；新部署不在乎 license → Redis / DragonflyDB；不想處理 license → ElastiCache（AWS 處理）。</p>
<h3 id="managed-level">Managed level</h3>
<p>運維責任轉移程度。<strong>Redis / Valkey</strong> 自管或選 managed（ElastiCache / Memorystore / Azure Cache）；<strong>Memcached</strong> 自管或 ElastiCache；<strong>DragonflyDB</strong> 目前只能自管（無 fully managed offering）；<strong>ElastiCache</strong> 完全 managed（auto failover / snapshot / patching）— 付 managed premium。</p>
<p>選型判讀：team 沒運維 Redis 經驗 → managed（ElastiCache / Memorystore）；要極致控制 → 自管；DragonflyDB 必自管（無 managed）。</p>
<h2 id="撰寫批次">撰寫批次</h2>
<table>
  <thead>
      <tr>
          <th>批次</th>
          <th>服務頁</th>
          <th>撰寫目的</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>C1</td>
          <td>Redis / Valkey</td>
          <td>建立 Redis baseline、開源治理與相容性判準</td>
      </tr>
      <tr>
          <td>C2</td>
          <td>Memcached</td>
          <td>建立 simple KV cache、低語意副本與水平擴張邊界</td>
      </tr>
      <tr>
          <td>C3</td>
          <td>DragonflyDB / AWS ElastiCache</td>
          <td>建立高吞吐 Redis-compatible 與 managed cache 的操作取捨</td>
      </tr>
      <tr>
          <td>C4</td>
          <td>KeyDB / Momento / Caffeine</td>
          <td>補 multi-threaded fork、serverless cache、local cache 對照（overview 完成 2026-06-16）</td>
      </tr>
  </tbody>
</table>
<h2 id="後續候選">後續候選</h2>
<p>C4 已建立 <a href="keydb/">KeyDB</a> / <a href="momento/">Momento</a> / <a href="caffeine/">Caffeine</a> overview。剩餘候選：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>候選服務</th>
          <th>寫作重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Redis fork / compat</td>
          <td>Garnet（Microsoft）</td>
          <td>相容性、multi-threading、client behavior</td>
      </tr>
      <tr>
          <td>Managed cache</td>
          <td>Azure Cache for Redis、GCP Memorystore</td>
          <td>managed SLA、vendor boundary</td>
      </tr>
      <tr>
          <td>Distributed cache</td>
          <td>Hazelcast、Aerospike</td>
          <td>cluster memory、near-cache、durability boundary</td>
      </tr>
      <tr>
          <td>Local cache</td>
          <td>Guava Cache、Ehcache（off-heap）</td>
          <td>process-local cache、invalidation、memory pressure</td>
      </tr>
      <tr>
          <td>HTTP / edge cache</td>
          <td>Varnish、Cloudflare Cache、Fastly、CloudFront</td>
          <td>edge TTL、origin protection、purge workflow</td>
      </tr>
  </tbody>
</table>
<p>主流覆蓋檢查的重點是把 cache 分成 process-local、service-local、distributed 與 edge 四層。Redis 系列 / KeyDB / DragonflyDB 解 service-local / distributed data structure cache；<a href="caffeine/">Caffeine</a> 解 process-local、<a href="momento/">Momento</a> 解 serverless cache；Varnish、Cloudflare、Fastly、CloudFront 解 HTTP / edge cache；Hazelcast、Aerospike 解更重的 distributed data / cache 邊界。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>上游：<a href="/blog/backend/02-cache-redis/cache-aside/" data-link-title="2.2 cache aside 與失效策略" data-link-desc="整理 read-through 思路、cache miss 與 invalidation">2.2 Cache Aside</a></li>
<li>上游：<a href="/blog/backend/02-cache-redis/cache-copy-freshness-boundary/" data-link-title="2.7 Cache Copy Boundary 與 Freshness" data-link-desc="說明快取何時只是可重建副本，何時會影響交易、權限或配額正確性。">2.7 Cache Copy Boundary</a></li>
<li>案例：<a href="/blog/backend/02-cache-redis/cases/" data-link-title="模組二案例正文" data-link-desc="快取策略與快取平台演進案例入口。">2.C 快取案例正文</a></li>
<li>服務路徑：<a href="/blog/backend/02-cache-redis/cache-migration-stampede-rollback/" data-link-title="2.9 Cache Migration 與 Stampede Rollback（實作示範）" data-link-desc="以商品詳情與價格快取示範 cache migration 如何交付 evidence package、release gate 與 incident decision log。">2.9 Cache Migration 與 Stampede Rollback</a></li>
</ul>
]]></content:encoded></item></channel></rss>