<?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>Redis-Streams on Tarragon</title><link>https://tarrragon.github.io/blog/tags/redis-streams/</link><description>Recent content in Redis-Streams on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Sat, 20 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/redis-streams/index.xml" rel="self" type="application/rss+xml"/><item><title>Queue 緩衝</title><link>https://tarrragon.github.io/blog/devops/07-burst-traffic/queue-buffering/</link><pubDate>Sat, 20 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/devops/07-burst-traffic/queue-buffering/</guid><description>&lt;p>Message queue 放在 ingestion（接收事件）和 processing（寫入 storage）之間，把兩者解耦。Ingestion 只負責驗證和寫入 queue，processing 按自己的速度從 queue 消費。Queue 做 burst 的時間緩衝 — 高峰時 queue 積壓、低峰時 worker 追上。&lt;/p>
&lt;h2 id="為什麼不直接寫-db">為什麼不直接寫 DB&lt;/h2>
&lt;p>直接寫 DB（SQLite / PostgreSQL）的問題是 ingestion 速度被 DB 寫入速度限制。DB 寫入慢（鎖定、WAL flush、索引更新）時，HTTP handler 的 goroutine 等在 &lt;code>Storage.Store()&lt;/code> 上 — goroutine 積壓 → 記憶體上升 → 最終 OOM 或 response timeout。&lt;/p>
&lt;p>Queue 的解決方式是把「接收」和「寫入」分開：接收端只做 JSON 驗證 + 寫入 queue（微秒級），處理端從 queue 讀取 + 寫入 DB（毫秒級）。接收端的吞吐量不再受 DB 限制。&lt;/p>
&lt;h3 id="取捨">取捨&lt;/h3>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>直接寫 DB&lt;/th>
 &lt;th>經過 Queue&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>延遲&lt;/td>
 &lt;td>事件寫完 DB 即可查詢&lt;/td>
 &lt;td>事件要等 worker 消費後才可查詢&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>吞吐&lt;/td>
 &lt;td>受 DB 寫入速度限制&lt;/td>
 &lt;td>受 queue 寫入速度限制（通常遠高於 DB）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>複雜度&lt;/td>
 &lt;td>一個元件&lt;/td>
 &lt;td>三個元件（collector + queue + worker）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>故障模式&lt;/td>
 &lt;td>DB 掛了事件丟失（除非有背壓）&lt;/td>
 &lt;td>Queue 做持久化，DB 掛了事件在 queue 等待&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>自用工具場景不需要 queue — 單 collector + SQLite 的直接寫入足夠。Queue 的引入條件是「直接寫 DB 的背壓開始頻繁觸發」。&lt;/p>
&lt;h2 id="候選類型">候選類型&lt;/h2>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>Queue&lt;/th>
 &lt;th>特點&lt;/th>
 &lt;th>適用場景&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>&lt;strong>Kafka&lt;/strong>&lt;/td>
 &lt;td>高吞吐、持久化、消費者群組&lt;/td>
 &lt;td>大規模（&amp;gt; 10 萬 events/sec）、多消費者&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>NATS JetStream&lt;/strong>&lt;/td>
 &lt;td>輕量、低延遲、Go 原生&lt;/td>
 &lt;td>中型（千 ~ 萬 events/sec）、Go 生態&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>&lt;strong>Redis Streams&lt;/strong>&lt;/td>
 &lt;td>用既有 Redis、XADD/XREAD API&lt;/td>
 &lt;td>中型、已有 Redis 基礎設施&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;h3 id="選型判斷">選型判斷&lt;/h3>
&lt;p>已有 Redis → 先用 Redis Streams（零新增元件）。Go 為主的技術棧 → NATS JetStream（Go 原生 client、單 binary 部署）。需要跨消費者群組或日誌級持久化 → Kafka。&lt;/p>
&lt;h3 id="引入條件">引入條件&lt;/h3>
&lt;p>Queue 的引入是架構複雜度的顯著上升（一個元件變三個）。明確的觸發條件：&lt;/p>
&lt;ul>
&lt;li>背壓（429 回應）頻繁觸發（每天 &amp;gt; 100 次）且持續（不只是瞬間 burst）&lt;/li>
&lt;li>寫入延遲的 P95 超過 500ms（DB 成為瓶頸）&lt;/li>
&lt;li>需要多個 consumer（同一批事件要送到不同的下游 — analytics DB、alert engine、archive）&lt;/li>
&lt;/ul>
&lt;h2 id="監控系統的-queue-架構">監控系統的 Queue 架構&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">SDK ──→ Collector (ingestion only)
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">2&lt;/span>&lt;span class="cl"> │
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">3&lt;/span>&lt;span class="cl"> ├─ 驗證 JSON Schema
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">4&lt;/span>&lt;span class="cl"> ├─ Redaction
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">5&lt;/span>&lt;span class="cl"> └─ 寫入 Queue
&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"> ├── Worker A → PostgreSQL（主 storage）
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">8&lt;/span>&lt;span class="cl"> ├── Worker B → 降採樣 → Summary tables
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">9&lt;/span>&lt;span class="cl"> └── Worker C → Rule engine → Alert&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>Collector 瘦身為 ingestion-only — 只做接收、驗證、redaction 和寫入 queue。Storage 寫入、降採樣、rule engine 都移到 worker 群。Collector 的吞吐瓶頸從 DB 寫入變成 queue 寫入（queue 的寫入吞吐通常是 DB 的 10-100 倍）。&lt;/p></description><content:encoded><![CDATA[<p>Message queue 放在 ingestion（接收事件）和 processing（寫入 storage）之間，把兩者解耦。Ingestion 只負責驗證和寫入 queue，processing 按自己的速度從 queue 消費。Queue 做 burst 的時間緩衝 — 高峰時 queue 積壓、低峰時 worker 追上。</p>
<h2 id="為什麼不直接寫-db">為什麼不直接寫 DB</h2>
<p>直接寫 DB（SQLite / PostgreSQL）的問題是 ingestion 速度被 DB 寫入速度限制。DB 寫入慢（鎖定、WAL flush、索引更新）時，HTTP handler 的 goroutine 等在 <code>Storage.Store()</code> 上 — goroutine 積壓 → 記憶體上升 → 最終 OOM 或 response timeout。</p>
<p>Queue 的解決方式是把「接收」和「寫入」分開：接收端只做 JSON 驗證 + 寫入 queue（微秒級），處理端從 queue 讀取 + 寫入 DB（毫秒級）。接收端的吞吐量不再受 DB 限制。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>直接寫 DB</th>
          <th>經過 Queue</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>延遲</td>
          <td>事件寫完 DB 即可查詢</td>
          <td>事件要等 worker 消費後才可查詢</td>
      </tr>
      <tr>
          <td>吞吐</td>
          <td>受 DB 寫入速度限制</td>
          <td>受 queue 寫入速度限制（通常遠高於 DB）</td>
      </tr>
      <tr>
          <td>複雜度</td>
          <td>一個元件</td>
          <td>三個元件（collector + queue + worker）</td>
      </tr>
      <tr>
          <td>故障模式</td>
          <td>DB 掛了事件丟失（除非有背壓）</td>
          <td>Queue 做持久化，DB 掛了事件在 queue 等待</td>
      </tr>
  </tbody>
</table>
<p>自用工具場景不需要 queue — 單 collector + SQLite 的直接寫入足夠。Queue 的引入條件是「直接寫 DB 的背壓開始頻繁觸發」。</p>
<h2 id="候選類型">候選類型</h2>
<table>
  <thead>
      <tr>
          <th>Queue</th>
          <th>特點</th>
          <th>適用場景</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><strong>Kafka</strong></td>
          <td>高吞吐、持久化、消費者群組</td>
          <td>大規模（&gt; 10 萬 events/sec）、多消費者</td>
      </tr>
      <tr>
          <td><strong>NATS JetStream</strong></td>
          <td>輕量、低延遲、Go 原生</td>
          <td>中型（千 ~ 萬 events/sec）、Go 生態</td>
      </tr>
      <tr>
          <td><strong>Redis Streams</strong></td>
          <td>用既有 Redis、XADD/XREAD API</td>
          <td>中型、已有 Redis 基礎設施</td>
      </tr>
  </tbody>
</table>
<h3 id="選型判斷">選型判斷</h3>
<p>已有 Redis → 先用 Redis Streams（零新增元件）。Go 為主的技術棧 → NATS JetStream（Go 原生 client、單 binary 部署）。需要跨消費者群組或日誌級持久化 → Kafka。</p>
<h3 id="引入條件">引入條件</h3>
<p>Queue 的引入是架構複雜度的顯著上升（一個元件變三個）。明確的觸發條件：</p>
<ul>
<li>背壓（429 回應）頻繁觸發（每天 &gt; 100 次）且持續（不只是瞬間 burst）</li>
<li>寫入延遲的 P95 超過 500ms（DB 成為瓶頸）</li>
<li>需要多個 consumer（同一批事件要送到不同的下游 — analytics DB、alert engine、archive）</li>
</ul>
<h2 id="監控系統的-queue-架構">監控系統的 Queue 架構</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">SDK ──→ Collector (ingestion only)
</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">           ├─ 驗證 JSON Schema
</span></span><span class="line"><span class="ln">4</span><span class="cl">           ├─ Redaction
</span></span><span class="line"><span class="ln">5</span><span class="cl">           └─ 寫入 Queue
</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">                 ├── Worker A → PostgreSQL（主 storage）
</span></span><span class="line"><span class="ln">8</span><span class="cl">                 ├── Worker B → 降採樣 → Summary tables
</span></span><span class="line"><span class="ln">9</span><span class="cl">                 └── Worker C → Rule engine → Alert</span></span></code></pre></div><p>Collector 瘦身為 ingestion-only — 只做接收、驗證、redaction 和寫入 queue。Storage 寫入、降採樣、rule engine 都移到 worker 群。Collector 的吞吐瓶頸從 DB 寫入變成 queue 寫入（queue 的寫入吞吐通常是 DB 的 10-100 倍）。</p>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>突發流量的分類 → <a href="/blog/devops/07-burst-traffic/burst-classification/" data-link-title="突發流量的分類" data-link-desc="可預期 vs 不可預期的突發流量 — 不同來源、持續時間和倍率決定不同的應對策略">突發流量的分類</a></li>
<li>降級策略 → <a href="/blog/devops/07-burst-traffic/degradation-strategy/" data-link-title="降級策略" data-link-desc="系統超載時犧牲什麼保住什麼 — 動態取樣、事件優先級、功能降級、聚合前移四種策略">降級策略</a></li>
<li>規模分級的完整應對 → <a href="/blog/devops/07-burst-traffic/scale-tier-response/" data-link-title="規模分級應對表" data-link-desc="自用級 → 中型 → 大型 → 商業網站級的四級應對方案 — 每級的觸發條件、架構組成和成本">規模分級應對表</a></li>
<li>Queue 的選型和操作實務 → <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">backend 非同步佇列</a></li>
</ul>
]]></content:encoded></item><item><title>Redis Streams → Kafka：從 embedded stream 長成 dedicated event streaming</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/migrate-to-kafka/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/migrate-to-kafka/</guid><description>&lt;blockquote>
&lt;p>本文是跨 vendor migration playbook、cross-link 到 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams&lt;/a> 跟 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka&lt;/a>。對位 &lt;a href="https://tarrragon.github.io/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）、&amp;#39;migration&amp;#39; 字面上不成立；本文釐清兩家 paradigm 邊界、什麼情境真的能換、application 模式重設計的 5 個踩雷（consumer offset 觀念差 / retention model / exactly-once 假設 / schema registry 缺位 / fan-out 模式差）、跟 JetStream 對位 &amp;#43; 混合架構">Kafka ↔ NATS&lt;/a> 的 &lt;em>paradigm shift&lt;/em> 模板 — 兩端不是同類產品的不同實作、是不同抽象層的系統：一個是 Redis 行程內的 append-only log data structure、一個是專用的 distributed event streaming platform。&lt;/p>&lt;/blockquote>
&lt;h2 id="redis-streams-跟-kafka-是不同抽象層的東西">Redis Streams 跟 Kafka 是不同抽象層的東西&lt;/h2>
&lt;p>Redis Streams 是 Redis 行程內的一個 data structure、Kafka 是一整套獨立的 distributed event streaming platform。這個區別決定整趟遷移的性質：要把 messaging 能力從「既有 Redis 行程的一塊記憶體」搬到「自成一格、要獨立運維的多節點叢集」，遠超過換個相容 broker 的工作量。&lt;/p>
&lt;p>Redis Streams 的責任邊界是「在已經跑著的 Redis 裡多一個 append-only log」。它共用 Redis 的記憶體、持久化（AOF / RDB）、failover（Sentinel / Cluster）跟運維團隊。寫入用 &lt;code>XADD&lt;/code>、消費用 &lt;code>XREADGROUP&lt;/code>，consumer group 跟 pending entries list（PEL）都活在同一個 Redis 行程。它的設計取捨偏向「低延遲、低運維增量、跟 Redis 生命週期綁定」。&lt;/p>
&lt;p>Kafka 的責任邊界是「成為跨系統的事件總線」。它把訊息寫成 partition 化的 log、落在獨立 broker 的磁碟、用 replication 保護、用 consumer group offset 追蹤各 consumer 進度，可長期保留並隨意 replay。它的設計取捨偏向「寫入即承諾、磁碟級長期保留、多 consumer 各自重播、水平擴展吞吐」。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>維度&lt;/th>
 &lt;th>Redis Streams&lt;/th>
 &lt;th>Kafka&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>部署形態&lt;/td>
 &lt;td>Redis 行程內的 data structure&lt;/td>
 &lt;td>獨立 broker 叢集（3-5 broker + KRaft）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>儲存後端&lt;/td>
 &lt;td>RAM-bound（受 &lt;code>maxmemory&lt;/code> 限制）&lt;/td>
 &lt;td>Broker 本地磁碟（可加 tiered storage to S3）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>拓樸單位&lt;/td>
 &lt;td>單一 stream key（綁單一 shard）&lt;/td>
 &lt;td>Topic + 多 partition（跨 broker 分布）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Retention 機制&lt;/td>
 &lt;td>&lt;code>MAXLEN&lt;/code> / &lt;code>MINID&lt;/code>、application 主動 trim&lt;/td>
 &lt;td>Broker 端 retention policy（time / size）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>消費進度&lt;/td>
 &lt;td>PEL + &lt;code>XACK&lt;/code>（broker 維護待 ack 集合）&lt;/td>
 &lt;td>Consumer offset commit（per partition）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>失敗接管&lt;/td>
 &lt;td>&lt;code>XCLAIM&lt;/code> / &lt;code>XAUTOCLAIM&lt;/code>（手動 / 半自動）&lt;/td>
 &lt;td>Rebalance protocol（broker 協調自動分配）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Replay&lt;/td>
 &lt;td>從 entry ID 重讀（受 retention 內資料限制）&lt;/td>
 &lt;td>從任意 offset 重讀（受磁碟 retention 限制）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>延遲&lt;/td>
 &lt;td>亞毫秒（記憶體操作）&lt;/td>
 &lt;td>5-50ms&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>運維增量&lt;/td>
 &lt;td>近乎零（沿用 Redis）&lt;/td>
 &lt;td>顯著（多養一套叢集 + schema / connect 生態）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>關鍵在「拓樸單位」這列。Redis Streams 的一個 stream key 只能落在單一 shard、沒有 partition 概念，吞吐與資料量受單 shard 的記憶體與單執行緒處理能力封頂。Kafka 的 topic 天然切成多 partition、分散到多 broker，這是兩者在規模上的分水嶺，也是後面所有對位與故障演練的根。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是跨 vendor migration playbook、cross-link 到 <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> 跟 <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/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 shift</em> 模板 — 兩端不是同類產品的不同實作、是不同抽象層的系統：一個是 Redis 行程內的 append-only log data structure、一個是專用的 distributed event streaming platform。</p></blockquote>
<h2 id="redis-streams-跟-kafka-是不同抽象層的東西">Redis Streams 跟 Kafka 是不同抽象層的東西</h2>
<p>Redis Streams 是 Redis 行程內的一個 data structure、Kafka 是一整套獨立的 distributed event streaming platform。這個區別決定整趟遷移的性質：要把 messaging 能力從「既有 Redis 行程的一塊記憶體」搬到「自成一格、要獨立運維的多節點叢集」，遠超過換個相容 broker 的工作量。</p>
<p>Redis Streams 的責任邊界是「在已經跑著的 Redis 裡多一個 append-only log」。它共用 Redis 的記憶體、持久化（AOF / RDB）、failover（Sentinel / Cluster）跟運維團隊。寫入用 <code>XADD</code>、消費用 <code>XREADGROUP</code>，consumer group 跟 pending entries list（PEL）都活在同一個 Redis 行程。它的設計取捨偏向「低延遲、低運維增量、跟 Redis 生命週期綁定」。</p>
<p>Kafka 的責任邊界是「成為跨系統的事件總線」。它把訊息寫成 partition 化的 log、落在獨立 broker 的磁碟、用 replication 保護、用 consumer group offset 追蹤各 consumer 進度，可長期保留並隨意 replay。它的設計取捨偏向「寫入即承諾、磁碟級長期保留、多 consumer 各自重播、水平擴展吞吐」。</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis Streams</th>
          <th>Kafka</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署形態</td>
          <td>Redis 行程內的 data structure</td>
          <td>獨立 broker 叢集（3-5 broker + KRaft）</td>
      </tr>
      <tr>
          <td>儲存後端</td>
          <td>RAM-bound（受 <code>maxmemory</code> 限制）</td>
          <td>Broker 本地磁碟（可加 tiered storage to S3）</td>
      </tr>
      <tr>
          <td>拓樸單位</td>
          <td>單一 stream key（綁單一 shard）</td>
          <td>Topic + 多 partition（跨 broker 分布）</td>
      </tr>
      <tr>
          <td>Retention 機制</td>
          <td><code>MAXLEN</code> / <code>MINID</code>、application 主動 trim</td>
          <td>Broker 端 retention policy（time / size）</td>
      </tr>
      <tr>
          <td>消費進度</td>
          <td>PEL + <code>XACK</code>（broker 維護待 ack 集合）</td>
          <td>Consumer offset commit（per partition）</td>
      </tr>
      <tr>
          <td>失敗接管</td>
          <td><code>XCLAIM</code> / <code>XAUTOCLAIM</code>（手動 / 半自動）</td>
          <td>Rebalance protocol（broker 協調自動分配）</td>
      </tr>
      <tr>
          <td>Replay</td>
          <td>從 entry ID 重讀（受 retention 內資料限制）</td>
          <td>從任意 offset 重讀（受磁碟 retention 限制）</td>
      </tr>
      <tr>
          <td>延遲</td>
          <td>亞毫秒（記憶體操作）</td>
          <td>5-50ms</td>
      </tr>
      <tr>
          <td>運維增量</td>
          <td>近乎零（沿用 Redis）</td>
          <td>顯著（多養一套叢集 + schema / connect 生態）</td>
      </tr>
  </tbody>
</table>
<p>關鍵在「拓樸單位」這列。Redis Streams 的一個 stream key 只能落在單一 shard、沒有 partition 概念，吞吐與資料量受單 shard 的記憶體與單執行緒處理能力封頂。Kafka 的 topic 天然切成多 partition、分散到多 broker，這是兩者在規模上的分水嶺，也是後面所有對位與故障演練的根。</p>
<h2 id="先確認是不是真的該遷多數中小規模不該遷">先確認是不是真的該遷：多數中小規模不該遷</h2>
<p>決定遷移前先做反向確認：在中小規模、且團隊已熟 Redis 的情境，Redis Streams 往往已經夠用，把它換成 Kafka 多半是引入運維負擔而非解決問題。遷移的正當理由來自規模或保留需求真的超出 Redis Streams 的能力邊界，而不是 Kafka 更主流。</p>
<p><a href="/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">Arcjet</a> 的方向恰好相反、值得當反向參照。Arcjet 的 security / bot detection 平台需要低延遲請求處理，原本評估 Kafka，發現 managed Kafka 要六位數美元年費、自管運維難度也高；他們把既有的 Redis cache 層升級成 Streams，總成本掉到約一千美元年費。代價是 Redis Streams 沒有自動 retention，他們自寫一個 Janitor process，依約每分鐘一百則的實際處理速度監測 stream 長度跟 consumer group 狀態、selectively trim。</p>
<p>Arcjet 的判讀對遷移方向的啟示：當 workload 是低延遲、資料量留在記憶體可承受的範圍、團隊本來就在跑 Redis，Redis Streams 是務實且便宜的選擇；願意自寫 retention 工具就能補上它缺的治理能力。這條路成立時，遷去 Kafka 是用六位數年費跟一整套叢集運維，去換一個現有方案已能覆蓋的需求。</p>
<p><a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso</a> 是另一個 Redis Streams 站得住的高壓案例。Bitso 的撮合引擎微服務要扛每秒上千則訊息、亞毫秒延遲、撐住 BTC 價格暴動的尖峰；他們先後評估 Kafka（延遲不符）跟 SQS（vendor lock-in + 延遲）後選 Redis Streams，自建一層 Reliable Streams 抽象封裝 PEL + retry + DLQ，走 idempotent processing 接受重複勝過遺失。Bitso 揭露 Redis Streams 是「資料結構」而非「broker 系統」，可靠性責任在 application 層；但在亞毫秒延遲是硬指標的撮合場景，這個取捨反而讓 Redis Streams 勝過 Kafka。</p>
<p>兩個案例共同點：當延遲是硬指標、資料量在 RAM 可承受範圍、團隊能自建缺的治理層，Redis Streams 就站得住。遷去 Kafka 的決策該建立在這些前提不再成立之上，而不是建立在 Kafka 更有名之上。</p>
<h2 id="真正該遷的訊號">真正該遷的訊號</h2>
<p>決定遷移的依據是 Redis Streams 的三個能力邊界被實際 workload 突破：retention 需求超出 RAM 的成本曲線、需要長期 replay、consumer group 或 partition 規模超出單一 Redis 行程。三個訊號中任一個被觸發、且自建工具補不回來時，遷去 Kafka 才划算。</p>
<p>第一個訊號是 retention 超出 RAM 的成本翻轉。Redis Streams 的資料活在記憶體，保留越久、stream 越長、佔的 RAM 越多，而 RAM 是 Redis 叢集裡最貴的資源。當 retention 需求從「幾小時的緩衝」長到「數天到數週的事件保留」，把這些資料留在 RAM 的成本會快速超過 Kafka 把同樣資料留在 broker 磁碟（甚至 tiered storage 到 S3）的成本。<a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 退場案例</a>就是這條線被突破的反例 — 把 Redis 當長期事件儲存（Stream 是其中一塊），事件量每週以 GB 成長、AOF fsync 與 EBS I/O 變成 latency 痛點，最終退回 PostgreSQL。成本曲線翻轉是最常見、也最該觸發遷移的訊號。</p>
<p>第二個訊號是需要長期 replay。事件溯源（event sourcing）或合規稽核場景，需要保留並重播數週、數月甚至數年的歷史事件。Redis Streams 的 replay 只能重讀 retention 內還在的資料，而 retention 受 RAM 限制無法拉得很長；Kafka 的磁碟保留加 tiered storage 讓長期 replay 變成 first-class 能力。當 replay 視窗的需求超出 RAM 能承受的 retention，這個訊號成立。</p>
<p>第三個訊號是 consumer group 或 partition 規模超出單一 Redis。Redis Streams 的單一 stream key 綁在單一 shard，吞吐受單 shard 封頂、沒有 partition 可以水平拆分並行度；要跨 shard 只能手動用 hash tag 切成多個獨立 stream，application 自己路由。當單一邏輯 stream 的吞吐需求、或 consumer 並行度需求超過單 shard 能給的，且手動切 stream 的複雜度已經失控，Kafka 的原生 partition 才值得換。</p>
<p>這三個訊號之外，還有一個放大條件：是否需要 Kafka 生態（Schema Registry、Connect / Debezium CDC、Streams 流處理）。如果遷移同時要接上 CDC pipeline 或 schema 強制治理，那 Kafka 帶來的不只是 retention 跟 partition、而是整套生態，這會讓遷移的價值天平更傾向 Kafka。但若只是想要更長 retention、生態用不到，先評估 Redis tiered 方案或自建 Janitor 是否更便宜。</p>
<h2 id="概念對位xaddxreadgroupxackmaxlenxclaim">概念對位：XADD/XREADGROUP/XACK/MAXLEN/XCLAIM</h2>
<p>遷移的核心工作是把 Redis Streams 的五個核心操作對應到 Kafka 的等價概念、並理解每個對位背後語意的偏移，這比換 SDK 重得多。直接照字面搬會在 retention、消費進度、失敗接管三處踩雷，這三處正是後面故障演練的來源。</p>
<table>
  <thead>
      <tr>
          <th>Redis Streams 操作</th>
          <th>Kafka 等價</th>
          <th>語意偏移</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>XADD stream * field val</code></td>
          <td><code>producer.send(topic, key, val)</code></td>
          <td>Kafka 用 key 決定 partition、Redis 單 stream 無 partition</td>
      </tr>
      <tr>
          <td><code>XREADGROUP GROUP g c</code></td>
          <td>consumer group + <code>poll()</code></td>
          <td>Kafka rebalance 自動分配 partition、Redis 要手動 <code>XCLAIM</code></td>
      </tr>
      <tr>
          <td><code>XACK stream g id</code></td>
          <td>offset commit</td>
          <td>PEL 是逐則待 ack 集合、offset 是單調位移、語意不同</td>
      </tr>
      <tr>
          <td><code>MAXLEN</code> / <code>MINID</code> / <code>XTRIM</code></td>
          <td>retention policy（time / size）</td>
          <td>application 主動 trim → broker 端被動 retention</td>
      </tr>
      <tr>
          <td><code>XCLAIM</code> / <code>XAUTOCLAIM</code></td>
          <td>rebalance protocol</td>
          <td>手動 / 半自動接管 → broker 協調自動 reassign</td>
      </tr>
  </tbody>
</table>
<p><code>XADD</code> 對 <code>producer.send</code> 的最大偏移是 partition key。Redis 的單一 stream key 沒有 partition，所有 entry 都在同一條序列上嚴格有序；Kafka 把訊息依 key 雜湊分到不同 partition，只有同一 partition 內保證有序。遷移時要決定哪個欄位當 partition key、這個決定同時決定了 ordering 的範圍跟 hot partition 的風險。</p>
<p><code>XREADGROUP</code> 對 consumer group 的偏移在 rebalance。Redis consumer group 沒有自動 rebalance，consumer 掛掉後它名下未 ack 的訊息留在 PEL，要靠其他 consumer 主動 <code>XCLAIM</code> 接管；Kafka 的 consumer group 有 rebalance protocol，consumer 加入或離開時 broker 自動把 partition 重新分配。從手動接管搬到自動 rebalance，application 端負責接管的那段邏輯可以刪掉、但要改成理解 rebalance 行為。</p>
<p><code>XACK</code> 對 offset commit 是最容易誤用的一處，獨立成下一節的故障演練。<code>MAXLEN</code> 對 retention policy 是成本模型翻轉的核心，也獨立成故障演練。</p>
<h2 id="production-故障演練">Production 故障演練</h2>
<h3 id="case-1retention-模型從-ram-限制翻成-log-成本磁碟與成本失準">Case 1：Retention 模型從 RAM 限制翻成 log 成本，磁碟與成本失準</h3>
<p><strong>徵兆</strong>：團隊把 Redis Streams 的 <code>MAXLEN 100000</code>（保留最近十萬則、控制 RAM）習慣直接對映成 Kafka 的某個數字，結果 cutover 後不是 broker 磁碟暴漲超出預期、就是資料保留遠短於業務需要、replay 視窗對不上。</p>
<p><strong>根因</strong>：Redis Streams 的 <code>MAXLEN</code> 是 application 在每次 <code>XADD</code> 主動修剪的「條數上限」，目的是壓住 RAM 佔用，是一個 count-based 的記憶體預算旋鈕。Kafka 的 retention 是 broker 端被動執行的 policy、預設是 time-based（<code>retention.ms</code>）或 size-based（<code>retention.bytes</code>），目的是控制磁碟保留窗，而磁碟比 RAM 便宜一到兩個數量級。兩者的單位、執行主體、成本曲線都不同 — 把「保留十萬則以省 RAM」直接搬成 Kafka 設定，會錯估磁碟用量，也會把 Redis 時代「為了省 RAM 而被迫短保留」的限制錯誤地帶進一個本來就能長保留的系統。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>從業務需求重算 retention、不沿用 Redis 的 RAM 預算</strong>：Redis 的 <code>MAXLEN</code> 數字是 RAM 成本的妥協、不是業務的真實保留需求；遷移時回到「業務需要 replay 多久」重新算 <code>retention.ms</code>，這正是遷移要解鎖的能力。</li>
<li><strong>改用 time-based 為主、size-based 當保險絲</strong>：Kafka 設 <code>retention.ms</code> 對齊業務 replay 窗、再設 <code>retention.bytes</code> 防單 partition 磁碟失控。</li>
<li><strong>長保留接 tiered storage</strong>：retention 需求拉到數週數月時，把冷資料分層到 S3、熱資料留本地磁碟，成本曲線進一步壓平，而這在 Redis 的 RAM 模型下做不到。</li>
</ol>
<h3 id="case-2pel-觀念被帶進-offset造成重複或漏消費">Case 2：PEL 觀念被帶進 offset，造成重複或漏消費</h3>
<p><strong>徵兆</strong>：遷移後 consumer 出現「明明處理過的訊息又被重新消費」或「某些訊息整批沒被處理」；團隊照 Redis 時代「逐則 <code>XACK</code>」的心智模型管理 Kafka offset commit，結果對不上。</p>
<p><strong>根因</strong>：PEL 跟 offset 是兩個不同的進度模型。Redis Streams 的 PEL 是 broker 維護的「逐則待 ack 集合」，每則訊息獨立追蹤是否已 ack，consumer 可以亂序 ack 某幾則、其他留在 PEL；<code>XACK</code> 是針對特定 entry ID 的點狀確認。Kafka 的 offset 是 per partition 的單調位移、代表「這個位置之前都算消費完」，commit offset N 意味著 0 到 N-1 全部視為已處理。把 PEL 的逐則語意套到 offset 上會出兩種錯：一是處理完亂序的訊息後 commit 了較大的 offset，中間沒處理完的訊息被當成已消費而漏掉；二是 commit 時機錯置（auto-commit 在處理前就 commit），crash 後從錯誤位置重讀造成重複。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>理解 offset 是區間承諾、不是逐則確認</strong>：commit offset 前確保該 offset 之前的訊息都已處理完、不要對亂序處理的批次 commit 最大 offset。</li>
<li><strong>關 auto-commit、改 manual commit 在處理之後</strong>：<code>enable.auto.commit=false</code>，處理完一批再 commit，對齊 at-least-once。</li>
<li><strong>保留 application 端 idempotency</strong>：這點從 Redis 時代就該有、遷到 Kafka 仍成立 — at-least-once 下重複難免，用 message ID + dedup store 顯式去重，對位 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>跟 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 的 idempotent processing</a>。</li>
</ol>
<h3 id="case-3單-stream-key-換成多-partitionordering-假設破裂">Case 3：單 stream key 換成多 partition，ordering 假設破裂</h3>
<p><strong>徵兆</strong>：遷移前所有事件在單一 Redis stream 上嚴格有序、downstream 依賴這個順序（例如同一筆訂單的 created → paid → shipped）；切到 Kafka 多 partition 後，同一筆訂單的事件被分到不同 partition、處理順序錯亂。</p>
<p><strong>根因</strong>：Redis Streams 的單一 stream key 綁單一 shard、所有 entry 在一條序列上全域有序，application 不需要思考 ordering 範圍就免費得到全序。Kafka 把 topic 切成多 partition 來換取水平吞吐，代價是只保證 <em>同一 partition 內</em> 有序、partition 之間無序。遷移時若沒指定 partition key、訊息會被 round-robin 或依預設雜湊散開，同一個業務實體（訂單、帳戶、裝置）的事件落到不同 partition，全序假設就破了。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>用業務實體當 partition key</strong>：把需要保序的實體 ID（訂單 ID、帳戶 ID）當 Kafka message key，同 key 雜湊到同 partition、partition 內保序，把「全域有序」收斂成「per-entity 有序」這個多數業務真正需要的粒度。</li>
<li><strong>辨識哪些流真的需要全序</strong>：若某條流真的需要全域嚴格有序且無法拆成 per-entity，設單 partition topic（犧牲該 topic 的水平吞吐）；這也是個訊號 — 若大量流都需要全序，遷 Kafka 的吞吐優勢用不上、該重新評估遷移。</li>
<li><strong>規劃 partition 數對齊並行度跟 hot key</strong>：partition 數決定 consumer 並行上限，同時注意熱門 key 造成的 hot partition，對位 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka topic 設計</a>的 key 策略段。</li>
</ol>
<h3 id="case-4redis-既有低延遲被-kafka-吞吐換掉延遲敏感路徑受傷">Case 4：Redis 既有低延遲被 Kafka 吞吐換掉，延遲敏感路徑受傷</h3>
<p><strong>徵兆</strong>：遷移後某些原本靠 Redis Streams 亞毫秒延遲的路徑（即時風控判斷、撮合前置）延遲跳到數十毫秒，下游 SLA 破線。</p>
<p><strong>根因</strong>：Redis Streams 的亞毫秒延遲來自記憶體操作 + 行程內 data structure；Kafka 為了長期保留跟高吞吐，訊息要落磁碟、過 replication、走網路到獨立 broker，單則訊息延遲落在 5-50ms 區間，這是它換吞吐跟持久性付出的代價。把延遲敏感路徑無差別搬上 Kafka，等於用一個為吞吐優化的系統去服務一個為延遲優化的需求。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>按延遲需求分流、不要全遷</strong>：把延遲敏感的即時路徑留在 Redis Streams（或 Redis 其他結構）、把需要長保留 / 高吞吐 / replay 的事件流遷到 Kafka，這正是 Bitso 在撮合場景堅持 Redis Streams 的理由。</li>
<li><strong>接受混合架構是常態</strong>：Redis Streams 跟 Kafka 共存、各自服務適配的 workload，不追求「全部統一到 Kafka」；對位 <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 的混合架構是 long-term default</a> 思路。</li>
<li><strong>若 Kafka 延遲必須壓低</strong>：調 producer <code>linger.ms=0</code> + <code>acks=1</code>、consumer <code>fetch.min.bytes=1</code> 換取較低延遲，但這會犧牲吞吐與部分可靠性、是 trade-off 不是免費午餐。</li>
</ol>
<h2 id="migration-結構漸進-cutover--長期混合">Migration 結構：漸進 cutover + 長期混合</h2>
<p>這趟遷移的結構是漸進拆分而非一次性切換：先按 workload 性質分流、再對需要遷的事件流做 dual-write 並行、逐流 cutover、最終留下 Redis Streams 跟 Kafka 共存的混合架構。一次性把所有 stream 搬上 Kafka 既無必要、也會把延遲敏感路徑拖下水。</p>
<ol>
<li><strong>Phase 0：scope 分流</strong> — 對每條 stream 跑前面三個訊號的判讀，分成「該遷 Kafka」（retention / replay / 規模超界）跟「留 Redis Streams」（延遲敏感 / 規模在範圍內）兩類。這一步直接決定後續工作量、也避免無差別遷移。</li>
<li><strong>Phase 1：Kafka 叢集與 topic 設計</strong> — 建 broker 叢集、依 Case 3 的 partition key 設計建 topic、依 Case 1 的業務需求設 retention，這時做的是基礎設施準備、還沒碰流量。</li>
<li><strong>Phase 2：dual-write 並行</strong> — producer 同時寫 Redis Streams 跟 Kafka、新 consumer 接 Kafka 驗證正確性、舊 consumer 持續吃 Redis Streams，這是可逆階段、出問題退回只讀 Redis 即可。</li>
<li><strong>Phase 3：逐流 cutover</strong> — 逐條 stream 把流量切到 Kafka、確認 consumer 進度（offset）跟 idempotency 都對、再停掉該 stream 的 Redis 端寫入；cutover 以 stream 為單位、不是整批。</li>
<li><strong>Phase 4：長期混合</strong> — 留在 Redis Streams 的延遲敏感流跟遷到 Kafka 的事件流共存、各自運維；需要時用 bridge（消費 Redis Streams 寫入 Kafka、或反向）同步必要資料。</li>
</ol>
<p>dual-write 階段的可逆性是這個結構的安全邊界：在 Phase 2 之前一切可退回純 Redis、Phase 3 逐流 cutover 把不可逆動作（停 Redis 寫入）切到最小粒度，單條 stream 出問題不影響其他流。</p>
<h2 id="capacity--cost-對照">Capacity / cost 對照</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>Redis Streams（既有 Redis 內）</th>
          <th>Kafka（self-managed）</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>部署增量</td>
          <td>近乎零（沿用 Redis 行程）</td>
          <td>3-5 broker + KRaft、獨立叢集</td>
      </tr>
      <tr>
          <td>儲存成本曲線</td>
          <td>RAM-bound（最貴的資源）</td>
          <td>磁碟為主（便宜 1-2 數量級）+ tiered to S3</td>
      </tr>
      <tr>
          <td>Retention 上限</td>
          <td>受 <code>maxmemory</code> 限制、實務數小時到數天</td>
          <td>數週到數月（磁碟）、數年（tiered storage）</td>
      </tr>
      <tr>
          <td>吞吐 / 單邏輯 stream</td>
          <td>受單 shard 封頂</td>
          <td>多 partition 水平擴展</td>
      </tr>
      <tr>
          <td>延遲</td>
          <td>亞毫秒</td>
          <td>5-50ms</td>
      </tr>
      <tr>
          <td>運維 FTE 增量</td>
          <td>近乎零</td>
          <td>0.5-2 FTE（含 schema / connect 生態）</td>
      </tr>
      <tr>
          <td>Replay 能力</td>
          <td>retention 內重讀（受 RAM 限制）</td>
          <td>任意 offset 重讀（受磁碟 retention 限制）</td>
      </tr>
      <tr>
          <td>生態</td>
          <td>Redis 工具鏈</td>
          <td>Schema Registry / Connect / Streams</td>
      </tr>
  </tbody>
</table>
<p><strong>判讀</strong>：成本的核心翻轉在「儲存成本曲線」這列。Redis Streams 把資料壓在最貴的 RAM、retention 越長越貴，所以實務上被迫短保留；Kafka 把資料攤到便宜的磁碟、再分層到 S3，讓長保留變得可負擔。但這個翻轉只在「retention 需求真的長」時成立 — 若 retention 只需數小時、資料量小，Redis Streams 沒有獨立叢集跟 0.5-2 FTE 的運維增量，總成本反而低，這正是 Arcjet 的處境。遷移划不划算取決於 retention 跟規模需求落在這條曲線的哪一段。</p>
<h2 id="整合--下一步">整合 / 下一步</h2>
<h3 id="混合架構是常見終態">混合架構是常見終態</h3>
<p>多數從 Redis Streams 起步、因規模長出 Kafka 需求的系統，終態是兩者共存而非取代：</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">[延遲敏感即時路徑]                    [長保留 / replay / 高吞吐事件流]
</span></span><span class="line"><span class="ln">2</span><span class="cl">   Redis Streams                              Kafka
</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">        └──────────── Bridge（雙向同步）────────┘</span></span></code></pre></div><p>Redis Streams 服務亞毫秒延遲的即時路徑（風控、撮合前置）、Kafka 服務需要長保留與 replay 的事件流；需要打通時寫一段 bridge 同步必要 stream。這跟 <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 的混合架構是 long-term default</a> 是同一個 paradigm shift 結論的兩個實例。</p>
<h3 id="接上-kafka-生態">接上 Kafka 生態</h3>
<p>遷到 Kafka 後可解鎖 Redis Streams 沒有的生態能力：</p>
<ul>
<li>Schema 治理：用 <a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Schema Registry</a> 強制 producer / consumer 契約，補上 Redis Streams 缺的 schema enforcement（對位 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 自建抽象層</a>的紀律性責任）。</li>
<li>CDC pipeline：接 <a href="/blog/backend/01-database/vendors/postgresql/logical-replication-debezium/" data-link-title="PostgreSQL Logical Replication &#43; Debezium CDC：replication slot × failure × recovery 對照" data-link-desc="PostgreSQL logical replication slot 跟 Debezium CDC 的失效模式對照表：slot lag 撐爆 primary disk / schema change 斷流 / 初始 COPY 鎖表 / zombie slot 不釋放 / replay storm 後 offset reset；publication / subscription / pgoutput 配置、跟 Kafka outbox pattern 整合">Debezium</a> 把資料庫變更流進 Kafka topic，做事件溯源主軸。</li>
<li>長期 replay：tiered storage 把冷事件分層到 S3、支援數年 replay。</li>
</ul>
<h3 id="反向確認的-tripwire">反向確認的 tripwire</h3>
<p>遷移後若觀察到：延遲敏感路徑 SLA 破線、Kafka 叢集運維成本超出省下的 RAM 成本、實際 retention 需求遠短於規劃 — 這些是「該遷的訊號其實不成立」的回溯訊號，應重新評估該 stream 是否該退回 Redis Streams，對位 <a href="/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">Arcjet</a> 的成本判讀。</p>
<h2 id="相關連結">相關連結</h2>
<ul>
<li>Source / target vendor：<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> / <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></li>
<li>反向案例：<a href="/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/" data-link-title="3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $" data-link-desc="Arcjet security 平台、Kafka managed 6 位數 $/yr、用 Redis Streams 約 $1k/yr、自寫 Janitor 監控 retention。">Arcjet Redis Streams 取代 Kafka</a> / <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso Reliable Streams</a> / <a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 退場</a></li>
<li>平行 migration playbook（同 paradigm shift）：<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></li>
<li>平行 vendor：<a href="/blog/backend/03-message-queue/vendors/rabbitmq/" data-link-title="RabbitMQ" data-link-desc="Classic message broker、AMQP routing 為主">RabbitMQ</a> / <a href="/blog/backend/03-message-queue/vendors/nats/" data-link-title="NATS" data-link-desc="Lightweight messaging、JetStream 加持久化與 streams">NATS</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 寫作方法論</a>（Type E paradigm shift）</li>
</ul>
]]></content:encoded></item><item><title>Redis Streams XCLAIM / PEL 失敗接管與 Cluster 影響</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/xclaim-pel-recovery/</link><pubDate>Tue, 16 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/xclaim-pel-recovery/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams&lt;/a> overview 的 implementation-layer deep article。Overview 給選型與最短路徑、本文聚焦「consumer crash 之後、卡在 PEL 的訊息怎麼回到處理流程」這條 implementation flow。實機輸出來自 &lt;code>redis:7&lt;/code>（7.4.9）單節點。&lt;/p>&lt;/blockquote>
&lt;h2 id="consumer-crash-後訊息卡在哪裡">consumer crash 後、訊息卡在哪裡&lt;/h2>
&lt;p>Redis Streams 的 consumer group 設計是「先投遞、後 ack」：&lt;code>XREADGROUP&lt;/code> 把 entry 投給某個 consumer 的同時、entry 進入該 group 的 &lt;strong>PEL（Pending Entries List）&lt;/strong>、標記為「已投遞、未確認」。consumer 處理完才呼叫 &lt;code>XACK&lt;/code> 把 entry 移出 PEL。這一段「已投遞未 ack」的視窗、是 Redis Streams 提供 at-least-once 的全部依據。&lt;/p>
&lt;p>問題在於 consumer crash 時機落在這個視窗內。consumer 已經拿到訊息、PEL 已經記了它的名字、但它在 ack 之前就死了。Redis 沒有 broker 級的「重新投遞」背景程序——不像 RabbitMQ consumer 斷線後 unacked 訊息自動 requeue。Redis 把這筆訊息留在 PEL、owner 仍是那個死掉的 consumer、然後什麼都不做。要讓這筆訊息回到處理流程、只有 application 主動呼叫 &lt;code>XCLAIM&lt;/code> 或 &lt;code>XAUTOCLAIM&lt;/code> 改寫 owner。&lt;/p>
&lt;p>這就是 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &amp;#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &amp;#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &amp;#43; retry &amp;#43; DLQ、idempotent processing。">Bitso 自建 Reliable Streams 抽象&lt;/a> 揭露的核心事實：Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。本文展開的就是這個責任的具體形狀——PEL 怎麼累積、怎麼判讀、接管機制怎麼運作、以及哪些操作會讓接管失效。&lt;/p>
&lt;h2 id="pel-機制xreadgroup-進xack-出">PEL 機制：XREADGROUP 進、XACK 出&lt;/h2>
&lt;p>PEL 是 per-group 的結構、記錄每個 entry 的四個欄位：entry ID、目前 owner consumer、idle time（距上次投遞的毫秒數）、delivery count（被投遞過幾次）。先用實機輸出建立基礎。寫入 5 筆、建 group、兩個 consumer 各讀一部分：&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 XADD mystream &lt;span class="s1">&amp;#39;*&amp;#39;&lt;/span> event order_1 amount &lt;span class="m">100&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 2&lt;/span>&lt;span class="cl">1781584105202-0
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 3&lt;/span>&lt;span class="cl">&lt;span class="c1"># ... order_2 ~ order_5、各得遞增 entry ID&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">$ redis-cli XGROUP CREATE mystream g1 &lt;span class="m">0&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln"> 6&lt;/span>&lt;span class="cl">OK
&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">$ redis-cli XREADGROUP GROUP g1 c1 COUNT &lt;span class="m">3&lt;/span> STREAMS mystream &lt;span class="s1">&amp;#39;&amp;gt;&amp;#39;&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"># c1 拿到 order_1 / order_2 / order_3&lt;/span>
&lt;/span>&lt;/span>&lt;span class="line">&lt;span class="ln">10&lt;/span>&lt;span class="cl">$ redis-cli XREADGROUP GROUP g1 c2 COUNT &lt;span class="m">10&lt;/span> STREAMS mystream &lt;span class="s1">&amp;#39;&amp;gt;&amp;#39;&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"># c2 拿到 order_4 / order_5&lt;/span>&lt;/span>&lt;/span>&lt;/code>&lt;/pre>&lt;/div>&lt;p>&lt;code>'&amp;gt;'&lt;/code> 代表「只取從未投遞給本 group 的新訊息」。投遞後這 5 筆全進 PEL。&lt;code>XPENDING&lt;/code> 的 summary 形式給總覽：&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <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> overview 的 implementation-layer deep article。Overview 給選型與最短路徑、本文聚焦「consumer crash 之後、卡在 PEL 的訊息怎麼回到處理流程」這條 implementation flow。實機輸出來自 <code>redis:7</code>（7.4.9）單節點。</p></blockquote>
<h2 id="consumer-crash-後訊息卡在哪裡">consumer crash 後、訊息卡在哪裡</h2>
<p>Redis Streams 的 consumer group 設計是「先投遞、後 ack」：<code>XREADGROUP</code> 把 entry 投給某個 consumer 的同時、entry 進入該 group 的 <strong>PEL（Pending Entries List）</strong>、標記為「已投遞、未確認」。consumer 處理完才呼叫 <code>XACK</code> 把 entry 移出 PEL。這一段「已投遞未 ack」的視窗、是 Redis Streams 提供 at-least-once 的全部依據。</p>
<p>問題在於 consumer crash 時機落在這個視窗內。consumer 已經拿到訊息、PEL 已經記了它的名字、但它在 ack 之前就死了。Redis 沒有 broker 級的「重新投遞」背景程序——不像 RabbitMQ consumer 斷線後 unacked 訊息自動 requeue。Redis 把這筆訊息留在 PEL、owner 仍是那個死掉的 consumer、然後什麼都不做。要讓這筆訊息回到處理流程、只有 application 主動呼叫 <code>XCLAIM</code> 或 <code>XAUTOCLAIM</code> 改寫 owner。</p>
<p>這就是 <a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso 自建 Reliable Streams 抽象</a> 揭露的核心事實：Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。本文展開的就是這個責任的具體形狀——PEL 怎麼累積、怎麼判讀、接管機制怎麼運作、以及哪些操作會讓接管失效。</p>
<h2 id="pel-機制xreadgroup-進xack-出">PEL 機制：XREADGROUP 進、XACK 出</h2>
<p>PEL 是 per-group 的結構、記錄每個 entry 的四個欄位：entry ID、目前 owner consumer、idle time（距上次投遞的毫秒數）、delivery count（被投遞過幾次）。先用實機輸出建立基礎。寫入 5 筆、建 group、兩個 consumer 各讀一部分：</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 XADD mystream <span class="s1">&#39;*&#39;</span> event order_1 amount <span class="m">100</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">1781584105202-0
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="c1"># ... order_2 ~ order_5、各得遞增 entry ID</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">$ redis-cli XGROUP CREATE mystream g1 <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">OK
</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-cli XREADGROUP GROUP g1 c1 COUNT <span class="m">3</span> STREAMS mystream <span class="s1">&#39;&gt;&#39;</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="c1"># c1 拿到 order_1 / order_2 / order_3</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">$ redis-cli XREADGROUP GROUP g1 c2 COUNT <span class="m">10</span> STREAMS mystream <span class="s1">&#39;&gt;&#39;</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="c1"># c2 拿到 order_4 / order_5</span></span></span></code></pre></div><p><code>'&gt;'</code> 代表「只取從未投遞給本 group 的新訊息」。投遞後這 5 筆全進 PEL。<code>XPENDING</code> 的 summary 形式給總覽：</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 XPENDING mystream g1
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">5</span>                  <span class="c1"># PEL 總數</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">1781584105202-0    <span class="c1"># 最小 pending ID</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584105578-0    <span class="c1"># 最大 pending ID</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">c1                 <span class="c1"># 各 consumer 的 pending 數</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="m">3</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">c2
</span></span><span class="line"><span class="ln">8</span><span class="cl"><span class="m">2</span></span></span></code></pre></div><p>5 筆全在 PEL、c1 扛 3 筆、c2 扛 2 筆。展開形式 <code>XPENDING &lt;key&gt; &lt;group&gt; - + &lt;count&gt;</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">1781584105202-0  c1  <span class="m">6318</span>  <span class="m">1</span>    <span class="c1"># entry ID / owner / idle ms / delivery count</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">1781584105278-0  c1  <span class="m">6318</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584105373-0  c1  <span class="m">6318</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">1781584105466-0  c2  <span class="m">6224</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">1781584105578-0  c2  <span class="m">6224</span>  <span class="m">1</span></span></span></code></pre></div><p><code>idle</code> 是 6318ms（距投遞已過 6.3 秒）、<code>delivery count</code> 都是 1（只投過一次）。這兩個數字是後面接管決策的核心輸入：idle 判斷「owner 是不是死了」、delivery count 判斷「這筆是不是 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message</a>」。</p>
<p><code>XACK</code> 把處理完的 entry 移出 PEL：</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 XACK mystream g1 1781584105202-0
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="m">1</span>                  <span class="c1"># 成功移除 1 筆</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">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="m">4</span>                  <span class="c1"># PEL 剩 4 筆</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">1781584105578-0
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">c1
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">2</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">c2
</span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="m">2</span></span></span></code></pre></div><p>PEL 從 5 降到 4。判讀原則固定：<strong>PEL 持續成長就是 consumer 健康訊號異常</strong>——不是 crash 沒 ack、就是處理速度跟不上、再不然是 ACK 程式碼漏寫。三者用 idle time 區分：crash 的 entry idle 會單調成長、處理慢的 idle 在 timeout 附近震盪、漏 ACK 的 entry delivery count 停在 1 但 idle 無上限成長。</p>
<h2 id="xclaim-與-xautoclaim改寫-owner-的兩條路">XCLAIM 與 XAUTOCLAIM：改寫 owner 的兩條路</h2>
<p>接管的本質是把 PEL entry 的 owner 從死掉的 consumer 改成活著的 consumer。<code>XCLAIM</code> 是手動指定 entry ID 接管、<code>XAUTOCLAIM</code> 是自動掃 idle 超過門檻的 entry 批次接管。兩者都接受 min-idle-time 參數當安全閥。</p>
<p><code>XCLAIM &lt;key&gt; &lt;group&gt; &lt;new-consumer&gt; &lt;min-idle-time&gt; &lt;id...&gt;</code>：把指定 entry 改判給新 consumer、條件是該 entry 的 idle 已達 min-idle-time。下面用 min-idle-time 0（無條件接管）把 c1 的一筆轉給 c3：</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 XCLAIM mystream g1 c3 <span class="m">0</span> 1781584105278-0
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">event
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">order_2
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">amount
</span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="m">200</span>               <span class="c1"># 回傳被接管 entry 的完整內容</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">1781584105278-0  c3  <span class="m">66</span>     <span class="m">2</span>    <span class="c1"># owner 變 c3、idle 歸零(66ms)、delivery count 升到 2</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">1781584105373-0  c1  <span class="m">14590</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">1781584105466-0  c2  <span class="m">14496</span>  <span class="m">1</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105578-0  c2  <span class="m">14496</span>  <span class="m">1</span></span></span></code></pre></div><p>接管後三件事同時發生：owner 改成 c3、idle 重置（剛 claim、66ms）、<strong>delivery count 從 1 升到 2</strong>。delivery count 自增是接管機制留下的審計軌跡——一筆訊息 delivery count 累積到 5、10、代表它反覆被接管又反覆沒處理完、這就是 poison message 的訊號、該路由到隔離區（見 <a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">recovery semantics</a> 與 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>）。</p>
<p><code>XAUTOCLAIM &lt;key&gt; &lt;group&gt; &lt;new-consumer&gt; &lt;min-idle-time&gt; &lt;start-id&gt;</code>（Redis 6.2+）省掉「先 XPENDING 找 ID、再逐筆 XCLAIM」兩步、一次掃描接管：</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 XAUTOCLAIM mystream g1 c3 <span class="m">0</span> <span class="m">0</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">0-0                          <span class="c1"># 下次掃描的 cursor（0-0 代表掃完一輪）</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">1781584105278-0 ...          <span class="c1"># 接管的 entry 內容（order_2）</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">1781584105373-0 ...          <span class="c1"># order_3</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">1781584105466-0 ...          <span class="c1"># order_4</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">1781584105578-0 ...          <span class="c1"># order_5</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>                <span class="c1"># 第三個回傳值：已從 stream 刪除的 entry ID 清單</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">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="m">4</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">1781584105278-0
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105578-0
</span></span><span class="line"><span class="ln">13</span><span class="cl">c3                           <span class="c1"># 全部 4 筆 owner 變 c3</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="m">4</span></span></span></code></pre></div><p>一次呼叫把整個 group 的 idle 訊息全歸到 c3。<code>XAUTOCLAIM</code> 是 consumer crash 後接管的主力——consumer 在啟動或處理迴圈裡固定跑一輪 <code>XAUTOCLAIM</code>、把孤兒訊息撿回來。回傳的 cursor 支援分批（一次掃不完時帶 cursor 續掃）、第三個回傳值（被刪 entry 清單）對應後面 MAXLEN 修剪的故障。</p>
<h2 id="min-idle-time防止活-consumer-被搶單">min-idle-time：防止活 consumer 被搶單</h2>
<p>min-idle-time 不是裝飾參數、是接管機制的安全閥：它要求「只有 idle 超過門檻的 entry 才能被接管」。沒有這個門檻、兩個 consumer 會互相搶對方正在處理的訊息。</p>
<p>驗證搶單防護——剛被 c3 claim 的訊息 idle 很低、用 60 秒門檻去 claim 會落空：</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 XCLAIM mystream g1 c4 <span class="m">60000</span> 1781584105278-0
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>               <span class="c1"># 回空：該 entry idle 未達 60000ms、c4 搶不到</span></span></span></code></pre></div><p>回空陣列代表 claim 失敗、owner 不變、訊息留在 c3 手上。這就是 min-idle-time 的作用：<strong>門檻 = 我願意相信 owner consumer 還活著的最長時間</strong>。</p>
<p>門檻設定是接管設計的核心取捨、沒有通用值、由訊息處理時間分佈決定。門檻設太短、正常處理中的訊息被當成孤兒搶走、變成多 consumer 重複處理同一筆。門檻設太長、真正 crash 的訊息要等很久才有人接管、recovery 延遲拉高。<a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness 的 event-driven 案例</a> 正是用 XAUTOCLAIM 重派來解 head-of-line blocking（慢訊息阻塞 consumer 進度）、並自設 redelivery 策略避免上述反覆搶單。實務基準是「門檻 &gt; p99 處理時間 + 安全係數」：若單筆處理 p99 是 2 秒、門檻設 30-60 秒、確保只有真的死掉（遠超正常處理時間）的 owner 才被接管。</p>
<p>接管後仍需 application 層去重。XCLAIM 改寫 owner、不代表原 consumer 真的沒處理完——它可能正在 ack 的瞬間被 claim、結果兩邊都處理一次。at-least-once 的去重責任永遠在 application、靠 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 兜底、這跟接管門檻設多準無關。</p>
<h2 id="memory-與-retentionmaxlen--xtrim-的取捨">Memory 與 retention：MAXLEN / XTRIM 的取捨</h2>
<p>Stream 是 append-only、不主動丟資料、佔用的 Redis 記憶體單調成長。retention 的唯一旋鈕是修剪：<code>MAXLEN</code>（保留最近 N 筆）或 <code>MINID</code>（保留 ID 大於某值的 entry）。可以在 <code>XADD</code> 寫入時順帶修剪、也可以用 <code>XTRIM</code> 獨立執行。</p>
<p>精確修剪 <code>MAXLEN =</code> 跟近似修剪 <code>MAXLEN ~</code> 的差別在性能。stream 內部是 radix tree of macro-nodes（每個 node 打包多筆 entry）。精確修剪要拆 node 才能剛好留 N 筆、近似修剪只刪「整個可以丟掉的 node」、留下的筆數會略多於 N、但省掉拆 node 的開銷。<code>~</code> 是 production 預設、<code>=</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">$ redis-cli XADD mystream MAXLEN <span class="s1">&#39;~&#39;</span> <span class="m">1000</span> <span class="s1">&#39;*&#39;</span> event order_6 amount <span class="m">600</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">1781584152570-0             <span class="c1"># 近似修剪：超過 ~1000 才整 node 刪</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli XADD mystream MAXLEN <span class="s1">&#39;=&#39;</span> <span class="m">3</span> <span class="s1">&#39;*&#39;</span> event order_7 amount <span class="m">700</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">1781584152871-0
</span></span><span class="line"><span class="ln">5</span><span class="cl">$ redis-cli XLEN mystream
</span></span><span class="line"><span class="ln">6</span><span class="cl"><span class="m">3</span>                           <span class="c1"># 精確修剪到剛好 3 筆</span></span></span></code></pre></div><p>stream 不受 <code>maxmemory-policy</code> eviction 管理——一般 key 在記憶體壓力下會被 evict、stream entry 不會。這代表 stream 是「只進不出、除非主動修剪」的記憶體成長源。<a href="/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/" data-link-title="3.C46 Learning.com：Redis 事件源退場（反例）" data-link-desc="Learning.com 把 microservice event store 放 Redis、1 年累積 GB/週、AOF&#43;EBS 變 latency 痛點、退到 PostgreSQL。">Learning.com 把 Redis 當長期事件儲存、最終因成本與延遲退場</a> 就是沒設修剪上限的反例（該案例涵蓋 Redis 事件儲存整體、Stream 是其中一塊）：事件量每週以 GB 成長、AOF fsync 與 EBS I/O 變成 latency 痛點、最終退回 PostgreSQL。判讀訊號是 <code>MEMORY USAGE mystream</code> 對比實例 <code>maxmemory</code>、超過預算就調低 MAXLEN。</p>
<h2 id="故障演練">故障演練</h2>
<h3 id="case-1consumer-crash-後-pel-訊息卡死沒人接">Case 1：consumer crash 後 PEL 訊息卡死沒人接</h3>
<p><strong>徵兆</strong>：<code>XPENDING</code> 總數持續成長、某個 consumer 的 pending 數停在固定值不降、那些 entry 的 idle time 單調往上爬（幾分鐘、幾小時）、業務端對應的訊息「進了 stream 但沒被處理」。</p>
<p><strong>根因</strong>：consumer 進程 crash（OOM kill / 部署滾動 / panic）、留下的 PEL entry owner 仍是死掉的 consumer。Redis 不會自動重投——沒有任何背景程序會碰這些 entry。它們會永遠卡在 PEL、直到有人主動接管。新啟動的 consumer 用 <code>XREADGROUP ... '&gt;'</code> 只會拿到「從未投遞」的新訊息、不會碰到前任留下的孤兒。</p>
<p><strong>修法</strong>：consumer 啟動時跟處理迴圈裡固定跑 <code>XAUTOCLAIM</code>、把超過 idle 門檻的孤兒撿回來：</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"># 每個 consumer 週期性執行、min-idle-time 設 60s</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">$ redis-cli XAUTOCLAIM mystream g1 self_consumer_id <span class="m">60000</span> <span class="m">0</span></span></span></code></pre></div><ol>
<li><strong>min-idle-time 設成 &gt; p99 處理時間 + 安全係數</strong>：避免把處理中的訊息誤判成孤兒（接 Case 2）。</li>
<li><strong>用回傳 cursor 分批掃</strong>：PEL 大時一次 <code>XAUTOCLAIM</code> 不掃完、帶 cursor 續掃、避免單次 block 太久。</li>
<li><strong>接管後檢查 delivery count</strong>：超過閾值（如 5）的 entry 不再處理、路由到 DLQ（Redis Streams 沒原生 DLQ、Bitso 自建一個 stream 當 DLQ）。</li>
<li><strong>監控 PEL 最大 idle</strong>：alert 設在「最老 pending entry 的 idle 超過 N 倍接管門檻」、代表接管機制本身停了。</li>
</ol>
<h3 id="case-2min-idle-time-設太短活-consumer-被搶單">Case 2：min-idle-time 設太短、活 consumer 被搶單</h3>
<p><strong>徵兆</strong>：同一筆訊息被多個 consumer 處理、下游出現重複副作用（重複扣款、重複發信）；<code>XPENDING</code> 展開看到某些 entry 的 delivery count 異常高（5、10+）但 stream 流量正常、沒有 consumer crash。</p>
<p><strong>根因</strong>：接管門檻低於正常處理時間。consumer A 拿到一筆要處理 10 秒的訊息、門檻設了 5 秒、consumer B 跑 <code>XAUTOCLAIM</code> 時這筆 idle 已過 5 秒、B 把還在 A 手上處理的訊息搶走、兩邊都處理一次。這是接管門檻設計的通用競態——一筆慢訊息被反覆搶、delivery count 暴衝、卻沒人真正完成。（<a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness 案例</a> 用 XAUTOCLAIM 重派解 head-of-line blocking 時、正是靠門檻與 redelivery 策略避開這種搶單。）</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>量測真實處理時間分佈、門檻設 &gt; p99</strong>：先用 metric 抓單筆處理 p50 / p99、門檻設 p99 的數倍。</li>
<li><strong>delivery count 當搶單偵測器</strong>：同一 entry delivery count 快速成長、代表它在被搶來搶去、調高門檻或隔離該訊息。</li>
<li><strong>idempotency 兜底</strong>：門檻再準也防不了「ack 瞬間被 claim」的競態、application 層去重是最後防線、不可省（見 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>）。</li>
</ol>
<h3 id="case-3maxlen-修剪掉-pel-內還沒-ack-的訊息">Case 3：MAXLEN 修剪掉 PEL 內還沒 ack 的訊息</h3>
<p><strong>徵兆</strong>：<code>XPENDING</code> 顯示某些 entry 仍 pending、但 <code>XCLAIM</code> 接管它時拿不到內容；consumer 接手後發現訊息 body 是空的、無法處理、又無法判斷該不該 ack。</p>
<p><strong>根因</strong>：<strong>修剪只看 entry ID 的新舊、不看它在不在 PEL</strong>。<code>XTRIM MAXLEN</code> 把最舊的 entry 從 stream 物理刪除、即使這些 entry 還在某個 group 的 PEL 裡等 ack。PEL 只記 entry ID、不存 body；body 存在 stream 本體。entry 被 trim 掉、PEL 還記得這個 ID、但 body 已經不存在了。實機驗證——4 筆全在 PEL、把 stream 修剪到剩 2 筆：</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 XLEN mystream
</span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="m">5</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">$ redis-cli XPENDING mystream g1
</span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="m">4</span>                           <span class="c1"># 4 筆未 ack 在 PEL</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">$ redis-cli XTRIM mystream MAXLEN <span class="m">2</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="m">3</span>                           <span class="c1"># 刪掉 3 筆（含 PEL 內的未 ack entry）</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">$ redis-cli XLEN mystream
</span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="m">2</span>
</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">$ redis-cli XPENDING mystream g1 - + <span class="m">10</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">1781584105278-0  c3  <span class="m">19307</span>  <span class="m">3</span>   <span class="c1"># PEL 還記得這些 ID</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">1781584105373-0  c3  <span class="m">19307</span>  <span class="m">2</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">1781584105466-0  c3  <span class="m">19307</span>  <span class="m">2</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">1781584105578-0  c3  <span class="m">19307</span>  <span class="m">2</span>
</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-cli XCLAIM mystream g1 c5 <span class="m">0</span> 1781584105278-0
</span></span><span class="line"><span class="ln">18</span><span class="cl"><span class="o">(</span>empty array<span class="o">)</span>               <span class="c1"># 接管成功改 owner、但 entry body 已被 trim、拿不到內容</span></span></span></code></pre></div><p>PEL 還有 4 筆記錄、但對應的 body 已從 stream 消失。<code>XCLAIM</code> 接管這種 entry、改得了 owner、拿不到 body——這是訊息靜默遺失。</p>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>修剪上限要 &gt; 處理 backlog 深度</strong>：MAXLEN / 流入速率 = 訊息在被修剪前的最長存活時間、這個時間要遠大於「最慢 consumer 清空 backlog 的時間」。</li>
<li><strong>修剪前檢查 PEL 最舊 ID</strong>：自動修剪前比對 <code>XPENDING</code> 的最小 pending ID、確保不會修到還在 PEL 的 entry。</li>
<li><strong>慢 consumer 監控優先於積極修剪</strong>：先解決 consumer 處理太慢導致 PEL 積壓的根因、再談用小 MAXLEN 壓記憶體；倒過來只會修掉未 ack 訊息。</li>
<li><strong>MINID 修剪比 MAXLEN 安全</strong>：MINID 用時間/業務邊界（如「保留 24 小時內」）、比 MAXLEN 的「保留 N 筆」更容易保證涵蓋未 ack 視窗。</li>
</ol>
<h3 id="case-4redis-cluster-對單-stream-的-shard-限制">Case 4：Redis Cluster 對單 stream 的 shard 限制</h3>
<p><strong>徵兆</strong>：stream 流量成長到單 node 容量上限、想像 Kafka 那樣「加 partition 分流」、卻發現 Redis Cluster 沒有這個機制；單一 stream key 的全部讀寫永遠打在同一個 node。</p>
<p><strong>根因</strong>：Redis Cluster 用 <code>CRC16(key) % 16384</code> 把 key 映射到 slot、slot 分佈在 node 上。<strong>一個 stream 是一個 key、永遠落在單一 slot、永遠在單一 shard</strong>。Streams 沒有 Kafka partition 那種「同一 topic 切多片、分散到多 broker」的概念。單 stream 的吞吐天花板就是單 node 的天花板。</p>
<p>實機驗證 keyslot 計算（cluster-enabled 節點）：</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 CLUSTER KEYSLOT stream:orders
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">6139</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli CLUSTER KEYSLOT stream:payments
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="m">3696</span>                        <span class="c1"># 不同 key 落不同 slot、可能在不同 shard</span></span></span></code></pre></div><p><strong>修法</strong>：要分流就在 application 層切多個 stream key（<code>stream:orders:0</code>、<code>stream:orders:1</code> &hellip;）、自己做 partition 路由。若需要某幾個 stream 保證落同一 shard（為了跨 stream 的原子操作或 co-located 處理）、用 hash tag——只有 <code>{}</code> 內的部分參與 CRC16：</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 CLUSTER KEYSLOT <span class="s1">&#39;{shard1}:stream:orders&#39;</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="m">10271</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">$ redis-cli CLUSTER KEYSLOT <span class="s1">&#39;{shard1}:stream:payments&#39;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="m">10271</span>                       <span class="c1"># 同 hash tag、強制落同 slot</span></span></span></code></pre></div><p>兩個不同 key 因為共用 <code>{shard1}</code> hash tag、CRC16 算出同一個 slot 10271、保證在同一 shard。判讀邊界：需要真正的 partition + replication + 跨節點水平擴展、Redis Streams 不是答案、改走 <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>。Redis Streams 的定位是中等規模、單 shard 容量內、不跨節點分片。</p>
<blockquote>
<p>Cluster 多節點分片下的端到端行為（resharding 期間 stream key 隨 slot 搬移、client topology cache）需要多節點環境、本文未實機驗證；slot migration 機制與踩雷見 <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>。</p></blockquote>
<h3 id="case-5failover-後-pel-狀態不一致">Case 5：failover 後 PEL 狀態不一致</h3>
<p><strong>徵兆</strong>：Sentinel / Cluster failover 後（replica 升 primary）、原本在 PEL 的部分訊息「消失」或「重複投遞」；<code>XPENDING</code> 數字跟 failover 前對不上；consumer 接管邏輯撿到不該撿的訊息、或漏撿該撿的。</p>
<p><strong>根因</strong>：Redis 的 replication 是非同步的。primary 上的 <code>XADD</code> / <code>XACK</code> / <code>XCLAIM</code> 先在本地生效、再非同步傳給 replica。failover 那一刻、replica 的 PEL 狀態落後 primary 一個 replication lag 的視窗。新 primary 從它當下的（落後的）PEL 狀態接手：lag 視窗內已 ack 的訊息在新 primary 上仍 pending（重複投遞）、lag 視窗內剛 claim 的 owner 改寫可能丟失（接管邏輯錯亂）。AOF / RDB 持久化只保證單機重啟的恢復、不改變跨 replica 的非同步本質。</p>
<blockquote>
<p>failover 對 PEL 一致性的影響需要多節點 Sentinel / Cluster 環境跨節點觀測、本文未實機驗證；以下依官方 replication 語義與案例敘述判讀。</p></blockquote>
<p><strong>修法</strong>：</p>
<ol>
<li><strong>接受 at-least-once、靠 idempotency 收斂</strong>：failover 造成的重複投遞跟正常的重複投遞同一性質、application 去重邏輯本來就要處理（見 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency 卡</a>）。</li>
<li><strong>failover 後主動全量 XAUTOCLAIM 對帳</strong>：failover 偵測到後、consumer 跑一輪低門檻 <code>XAUTOCLAIM</code> 重新接管、用 application 端的處理紀錄判斷哪些真的沒處理。</li>
<li><strong>降低 replication lag</strong>：lag 越小、failover 視窗的 PEL 偏差越小；監控 <code>master_repl_offset</code> 與 replica offset 差。</li>
<li><strong>語義誤配風險</strong>：把 Redis Streams 當「不丟訊息的 broker」用、在 failover 邊界會破功——這是 <a href="/blog/backend/03-message-queue/cases/failure-queue-semantics-mismatch-cutover/" data-link-title="3.C9 反例：Queue 語義切換誤配" data-link-desc="at-least-once / exactly-once 語義誤配導致資料重複與遺漏。">3.C9 語義誤配</a> 的思路、選型時就要認清 Redis Streams 的一致性等級。</li>
</ol>
<h2 id="capacity-與判讀路由">Capacity 與判讀路由</h2>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>判讀訊號</th>
          <th>邊界</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>PEL 深度</td>
          <td><code>XPENDING</code> 總數持續成長</td>
          <td>成長不停 = consumer 健康問題、不是調 MAXLEN 能解</td>
      </tr>
      <tr>
          <td>接管門檻</td>
          <td>delivery count 異常高（搶單）/ 最老 idle 不收斂</td>
          <td>門檻 &gt; p99 處理時間 + 安全係數</td>
      </tr>
      <tr>
          <td>Stream 記憶體</td>
          <td><code>MEMORY USAGE</code> 對比 <code>maxmemory</code></td>
          <td>stream 不被 eviction、唯一旋鈕是 MAXLEN / MINID 修剪</td>
      </tr>
      <tr>
          <td>修剪 vs 未 ack 視窗</td>
          <td>修剪上限 / 流入速率 &lt; backlog 清空時間</td>
          <td>違反就會修掉 PEL 內未 ack 訊息（Case 3）</td>
      </tr>
      <tr>
          <td>單 stream 吞吐</td>
          <td>單 node CPU / memory 打滿、無法加 partition</td>
          <td>達單 shard 天花板 = 該評估 Kafka</td>
      </tr>
  </tbody>
</table>
<p>判讀路由固定三層：先看 PEL 是「整 group 成長」（流入 &gt; 處理、擴 consumer）還是「單 consumer 卡住」（crash、要接管）；接管時先確認 min-idle-time 對得上處理時間分佈、再看 delivery count 篩 poison message；retention 調整前先確認修剪上限涵蓋 PEL 未 ack 視窗。</p>
<h2 id="整合與下一步">整合與下一步</h2>
<p>接管機制是 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">consumer 設計</a> 在 Redis Streams 上的具體落地——consumer 不只是讀訊息的迴圈、還要承擔「撿前任孤兒」的責任。設計 consumer 時把 <code>XAUTOCLAIM</code> 排進處理迴圈、跟 <code>XREADGROUP '&gt;'</code> 並列、不是事後補丁。</p>
<p>知識卡對位：delivery count 超閾值的訊息對應 <a href="/blog/backend/knowledge-cards/poison-message-quarantine/" data-link-title="Poison-Message Quarantine" data-link-desc="說明把毒訊息從主處理路徑隔離出來的機制，讓正常訊息繼續前進">poison message quarantine</a>（Redis Streams 沒原生 DLQ、自建一個 stream 當隔離區）；接管後的去重對應 <a href="/blog/backend/knowledge-cards/recovery-semantics/" data-link-title="Recovery Semantics" data-link-desc="說明事件處理失敗後能否透過 replay、checkpoint 與補償重建正確狀態並驗證">recovery semantics</a> 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>（at-least-once 的收斂責任在 application）。</p>
<p>案例延伸：<a href="/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/" data-link-title="3.C42 Bitso：Reliable Redis Streams 抽象 &#43; 自建 DLQ" data-link-desc="Bitso 加密交易所、千 msg/sec/stream &#43; 亞毫秒延遲、自建 Reliable Streams 封裝 PEL &#43; retry &#43; DLQ、idempotent processing。">Bitso</a> 把本文這些機制封裝成 Reliable Streams 抽象層 + 自建 DLQ、是「application 層補可靠性」的完整實作參考；<a href="/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/" data-link-title="3.C45 Klaxit：Rust &#43; Redis Streams 處理 Heroku Logplex" data-link-desc="Klaxit carpool 用 Redis Streams 處理 Heroku Logplex 匯流、自動偵測修復平台 perf 問題、6 個月 production Rust。">Klaxit Rust + Logplex</a> 是高吞吐 log ingestion 下 consumer group 分流長時間穩定運轉的範例；接管門檻搶單的反面教訓在 <a href="/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/" data-link-title="3.C44 Harness：CD 微服務 async state transfer" data-link-desc="Harness CD 平台用 Redis Streams 解 brittle HTTP、揭露監控缺口 / MAXLEN truncation / head-of-line blocking 三類問題。">Harness event-driven</a>。</p>
<p>選型回路：單 stream 撞到單 shard 天花板、或 failover 一致性要求超出 at-least-once、回 <a href="/blog/backend/03-message-queue/vendors/redis-streams/#%e4%bd%95%e6%99%82%e6%94%b9%e8%b5%b0%e5%85%b6%e4%bb%96%e6%9c%8d%e5%8b%99" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &#43; consumer group">Redis Streams overview 的「何時改走其他服務」</a>、評估 <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>（partition + replication）。Cluster 層的 slot / topology 行為見 <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>。</p>
]]></content:encoded></item><item><title>3.C42 Bitso：Reliable Redis Streams 抽象 + 自建 DLQ</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-bitso-reliable-streams/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 沒有原生 DLQ、要在 application 層自建抽象。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Bitso 的 Order Engine 微服務需要 thousands of messages/sec/stream + 亞毫秒延遲、撐住 BTC 價格暴動的流量尖峰；先後評估 Kafka（latency）跟 SQS（vendor lock-in + latency）後選 Redis Streams、團隊本來就熟 Redis、已在 mission-critical service 跑超過半年。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>自建 &amp;ldquo;Reliable Redis Streams&amp;rdquo; 抽象層（StreamRedisOperations adapter / ReliableStream interface / MessageReadingLoop）封裝 readMessages + readPendingMessages、加上 Redis Streams 沒有原生支援的 DLQ（N 次 retry 後路由）、走 idempotent processing 接受重複勝過遺失。揭露 Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Sentinel + Cluster 可靠性。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/bitso-engineering/the-redis-streams-we-have-known-and-loved-e9e596d49a22">The Redis Streams We Have Known and Loved&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 沒有原生 DLQ、要在 application 層自建抽象。</p>
<h2 id="觀察">觀察</h2>
<p>Bitso 的 Order Engine 微服務需要 thousands of messages/sec/stream + 亞毫秒延遲、撐住 BTC 價格暴動的流量尖峰；先後評估 Kafka（latency）跟 SQS（vendor lock-in + latency）後選 Redis Streams、團隊本來就熟 Redis、已在 mission-critical service 跑超過半年。</p>
<h2 id="判讀">判讀</h2>
<p>自建 &ldquo;Reliable Redis Streams&rdquo; 抽象層（StreamRedisOperations adapter / ReliableStream interface / MessageReadingLoop）封裝 readMessages + readPendingMessages、加上 Redis Streams 沒有原生支援的 DLQ（N 次 retry 後路由）、走 idempotent processing 接受重複勝過遺失。揭露 Redis Streams 是「資料結構」、不是「broker 系統」、可靠性責任在 application 層。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Sentinel + Cluster 可靠性。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 vendor 頁</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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/bitso-engineering/the-redis-streams-we-have-known-and-loved-e9e596d49a22">The Redis Streams We Have Known and Loved</a></li>
</ul>
]]></content:encoded></item><item><title>3.C43 Arcjet：Redis Streams 取代 Kafka 省 6 位數 $</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-arcjet-replace-kafka/</guid><description>&lt;p>Arcjet 用 Redis Streams 取代 Kafka 的案例揭露了中小規模場景下「Kafka 的 managed 成本 vs Redis Streams 的運維成本」的具體取捨 — 省下六位數年費的代價是自寫 retention 治理跟監控工具。&lt;/p>
&lt;h2 id="業務背景">業務背景&lt;/h2>
&lt;p>Arcjet 是 security / bot detection 平台，處理每個 HTTP request 的安全判斷。核心需求是 low-latency 的請求處理 — 安全判斷要在幾毫秒內完成，不能拖慢使用者的 request。&lt;/p>
&lt;p>系統架構中有一段 event-driven pipeline 負責把安全事件從 detection layer 傳遞到 analytics 跟 alerting。原本評估用 Kafka 做這段 pipeline，但 managed Kafka 的年費落在六位數美金 — 對 Arcjet 的流量規模跟業務階段，這個成本不合理。&lt;/p>
&lt;p>Arcjet 的基礎設施已經有 Redis 做 cache。把 Redis 從純 cache 升級到 cache + Streams，利用既有的 Redis infrastructure 承擔 event pipeline，總成本約 $1k/year。&lt;/p>
&lt;h2 id="技術挑戰">技術挑戰&lt;/h2>
&lt;h3 id="redis-streams-沒有自動-retention">Redis Streams 沒有自動 retention&lt;/h3>
&lt;p>Kafka 的 retention 是內建功能 — 設定 &lt;code>log.retention.hours&lt;/code> 後 broker 自動刪除到期資料。Redis Streams 沒有內建的自動 retention — stream 資料會持續累積，直到手動 &lt;code>XTRIM&lt;/code> 或 &lt;code>XDEL&lt;/code>。&lt;/p>
&lt;p>在生產環境下，不處理 retention 意味著 Redis 的記憶體持續成長，最終觸發 eviction policy 或 OOM。對 Arcjet 來說 Redis 同時做 cache 跟 Streams，Streams 的記憶體成長會擠壓 cache 的可用空間。&lt;/p>
&lt;h3 id="consumer-group-進度追蹤">Consumer group 進度追蹤&lt;/h3>
&lt;p>Redis Streams 的 consumer group 會追蹤每個 consumer 的讀取進度（last delivered ID）。做 &lt;code>XTRIM&lt;/code> 時需要確保不刪除尚未被所有 consumer group 確認的訊息 — 否則 consumer 會丟失未處理的事件。&lt;/p>
&lt;p>Kafka 的 log compaction 跟 retention 自動處理這個問題（consumer offset 以前的 segment 才會被清理）。Redis Streams 需要 application 自己確認所有 consumer group 的進度，再決定 trim 的位置。&lt;/p>
&lt;h3 id="單機-redis-的可靠性邊界">單機 Redis 的可靠性邊界&lt;/h3>
&lt;p>Redis 的持久化機制（RDB snapshot + AOF）提供的是 best-effort 的持久性，跟 Kafka 的 replication-based 持久化保證不同。Redis crash + restart 時，AOF 的最後幾筆寫入可能遺失（取決於 &lt;code>appendfsync&lt;/code> 設定）。&lt;/p>
&lt;p>對 Arcjet 的安全事件場景，偶爾丟失幾筆事件可以接受（security detection 的結果是即時判斷，事後的 analytics 容忍小量遺失）。如果場景是金融交易或 audit log，這個可靠性邊界就不夠。&lt;/p>
&lt;h2 id="解法與取捨">解法與取捨&lt;/h2>
&lt;h3 id="自建-janitor-process">自建 Janitor process&lt;/h3>
&lt;p>Arcjet 自寫了一個 Janitor process 處理 Redis Streams 的 retention：&lt;/p></description><content:encoded><![CDATA[<p>Arcjet 用 Redis Streams 取代 Kafka 的案例揭露了中小規模場景下「Kafka 的 managed 成本 vs Redis Streams 的運維成本」的具體取捨 — 省下六位數年費的代價是自寫 retention 治理跟監控工具。</p>
<h2 id="業務背景">業務背景</h2>
<p>Arcjet 是 security / bot detection 平台，處理每個 HTTP request 的安全判斷。核心需求是 low-latency 的請求處理 — 安全判斷要在幾毫秒內完成，不能拖慢使用者的 request。</p>
<p>系統架構中有一段 event-driven pipeline 負責把安全事件從 detection layer 傳遞到 analytics 跟 alerting。原本評估用 Kafka 做這段 pipeline，但 managed Kafka 的年費落在六位數美金 — 對 Arcjet 的流量規模跟業務階段，這個成本不合理。</p>
<p>Arcjet 的基礎設施已經有 Redis 做 cache。把 Redis 從純 cache 升級到 cache + Streams，利用既有的 Redis infrastructure 承擔 event pipeline，總成本約 $1k/year。</p>
<h2 id="技術挑戰">技術挑戰</h2>
<h3 id="redis-streams-沒有自動-retention">Redis Streams 沒有自動 retention</h3>
<p>Kafka 的 retention 是內建功能 — 設定 <code>log.retention.hours</code> 後 broker 自動刪除到期資料。Redis Streams 沒有內建的自動 retention — stream 資料會持續累積，直到手動 <code>XTRIM</code> 或 <code>XDEL</code>。</p>
<p>在生產環境下，不處理 retention 意味著 Redis 的記憶體持續成長，最終觸發 eviction policy 或 OOM。對 Arcjet 來說 Redis 同時做 cache 跟 Streams，Streams 的記憶體成長會擠壓 cache 的可用空間。</p>
<h3 id="consumer-group-進度追蹤">Consumer group 進度追蹤</h3>
<p>Redis Streams 的 consumer group 會追蹤每個 consumer 的讀取進度（last delivered ID）。做 <code>XTRIM</code> 時需要確保不刪除尚未被所有 consumer group 確認的訊息 — 否則 consumer 會丟失未處理的事件。</p>
<p>Kafka 的 log compaction 跟 retention 自動處理這個問題（consumer offset 以前的 segment 才會被清理）。Redis Streams 需要 application 自己確認所有 consumer group 的進度，再決定 trim 的位置。</p>
<h3 id="單機-redis-的可靠性邊界">單機 Redis 的可靠性邊界</h3>
<p>Redis 的持久化機制（RDB snapshot + AOF）提供的是 best-effort 的持久性，跟 Kafka 的 replication-based 持久化保證不同。Redis crash + restart 時，AOF 的最後幾筆寫入可能遺失（取決於 <code>appendfsync</code> 設定）。</p>
<p>對 Arcjet 的安全事件場景，偶爾丟失幾筆事件可以接受（security detection 的結果是即時判斷，事後的 analytics 容忍小量遺失）。如果場景是金融交易或 audit log，這個可靠性邊界就不夠。</p>
<h2 id="解法與取捨">解法與取捨</h2>
<h3 id="自建-janitor-process">自建 Janitor process</h3>
<p>Arcjet 自寫了一個 Janitor process 處理 Redis Streams 的 retention：</p>
<ol>
<li>定期檢查每個 stream 的長度（<code>XLEN</code>）</li>
<li>查詢所有 consumer group 的 pending entry list（PEL）跟最後確認位置</li>
<li>計算安全的 trim 位置（所有 consumer group 都已確認的最舊 ID）</li>
<li>執行 <code>XTRIM stream MINID &lt;safe-id&gt;</code> 刪除已確認的舊資料</li>
</ol>
<p>Janitor 的執行頻率根據實際處理速度（~100 msgs/min）設定 — 不需要非常頻繁，但不能完全不跑。</p>
<h3 id="取捨">取捨</h3>
<table>
  <thead>
      <tr>
          <th>面向</th>
          <th>Managed Kafka</th>
          <th>Redis Streams + Janitor</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>年成本</td>
          <td>六位數 USD</td>
          <td>~$1k USD</td>
      </tr>
      <tr>
          <td>Retention 管理</td>
          <td>內建自動</td>
          <td>自寫 Janitor</td>
      </tr>
      <tr>
          <td>持久化保證</td>
          <td>Replication-based（強）</td>
          <td>AOF/RDB（best-effort）</td>
      </tr>
      <tr>
          <td>Consumer group</td>
          <td>原生支援、offset commit 自動</td>
          <td>原生支援、但 trim 要手動協調</td>
      </tr>
      <tr>
          <td>生態工具</td>
          <td>Kafka Connect、Schema Registry</td>
          <td>無（自建）</td>
      </tr>
      <tr>
          <td>擴展性</td>
          <td>Partition 水平擴展</td>
          <td>單 Redis 受限、Cluster 模式複雜</td>
      </tr>
      <tr>
          <td>運維知識</td>
          <td>Kafka 運維（或交給 managed）</td>
          <td>Redis 運維 + 自建 Janitor 維護</td>
      </tr>
  </tbody>
</table>
<h3 id="適用邊界">適用邊界</h3>
<p>Redis Streams 取代 Kafka 的適用邊界：</p>
<ul>
<li><strong>流量規模</strong>：每分鐘數百到數千筆（超過每秒數萬筆需要 Redis Cluster 或多 stream）</li>
<li><strong>持久化要求</strong>：容忍偶爾丟失少量訊息（best-effort）</li>
<li><strong>已有 Redis</strong>：不需要額外部署 Redis、利用既有 infrastructure</li>
<li><strong>Kafka 功能不需要</strong>：不需要 Kafka Connect、Schema Registry、long-term retention、跨 region replication</li>
</ul>
<p>超過這些邊界時，Redis Streams 的自建成本（Janitor + 監控 + retention 治理 + 可靠性補償）會逐漸接近 managed Kafka 的費用，成本優勢消失。</p>
<h2 id="回寫教材的連結">回寫教材的連結</h2>
<ul>
<li><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 vendor 頁</a>：XCLAIM / PEL recovery 的進階主題</li>
<li><a href="/blog/backend/03-message-queue/vendors/kafka/" data-link-title="Apache Kafka" data-link-desc="Distributed event streaming platform、log-based 模型">Kafka vendor 頁</a>：成本對照 — Kafka 的固定成本高但功能完整</li>
<li><a href="/blog/backend/03-message-queue/durable-queue/" data-link-title="3.2 durable queue 與重試策略" data-link-desc="整理持久化佇列、DLQ 與重試流程">3.2 durable queue</a>：Redis Streams 的持久化機制跟 Kafka 的 replication 在 durability 光譜上的位置</li>
<li><a href="/blog/backend/03-message-queue/broker-basics/" data-link-title="3.1 broker 基礎與投遞模型" data-link-desc="先理解 broker、queue、consumer 與 delivery semantics">3.1 broker basics</a>：broker 選型時成本是一級決策維度</li>
</ul>
<h2 id="判讀徵兆">判讀徵兆</h2>
<p>讀者在自己的系統看到以下訊號時，應該回讀本案例：</p>
<ul>
<li>Managed Kafka 的月帳單跟實際流量量級不成比例（低流量但高成本）</li>
<li>已有 Redis infrastructure、考慮把 event pipeline 合併到 Redis</li>
<li>Event pipeline 的流量在每秒數百筆以下、持久化要求是 best-effort</li>
<li>Redis 記憶體持續成長但不確定 Streams 的 retention 有沒有正確執行</li>
</ul>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://blog.arcjet.com/replacing-kafka-with-redis-streams/">Replacing Kafka with Redis Streams</a></li>
</ul>
]]></content:encoded></item><item><title>3.C44 Harness：CD 微服務 async state transfer</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-harness-event-driven-state/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 在 production 落地的三類經常性議題。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Harness 為 CD 微服務之間的 async state transfer 採用 Redis Streams、避開「每個 service 都要知道怎麼跟其他 service 講話」的 brittle HTTP 模式；初始規模 a few thousand msgs/min、Kafka 在此規模 overkill、又能複用已存在的 Redis 基建。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>落地後揭露三類問題：監控缺口（自寫 app 追 consumer lag）、需要主動 MAXLEN truncation、head-of-line blocking 要用 XAUTOCLAIM 重派並設計 redelivery 策略。揭露「Redis Streams 適合中小規模」這個聲明、實際包含三件 production work。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Memory + retention 取捨。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://www.harness.io/blog/event-driven-architecture-redis-streams">Event-Driven Architecture with Redis Streams&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 在 production 落地的三類經常性議題。</p>
<h2 id="觀察">觀察</h2>
<p>Harness 為 CD 微服務之間的 async state transfer 採用 Redis Streams、避開「每個 service 都要知道怎麼跟其他 service 講話」的 brittle HTTP 模式；初始規模 a few thousand msgs/min、Kafka 在此規模 overkill、又能複用已存在的 Redis 基建。</p>
<h2 id="判讀">判讀</h2>
<p>落地後揭露三類問題：監控缺口（自寫 app 追 consumer lag）、需要主動 MAXLEN truncation、head-of-line blocking 要用 XAUTOCLAIM 重派並設計 redelivery 策略。揭露「Redis Streams 適合中小規模」這個聲明、實際包含三件 production work。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Consumer group + PEL / XCLAIM + 失敗接管 / Memory + retention 取捨。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/red-team-delivery-layer/" data-link-title="3.5 攻擊者視角（紅隊）：傳遞層弱點判讀" data-link-desc="從重複投遞、重放濫用、毒訊息與容量壓力，盤點 message delivery 的主要弱點">3.5 紅隊章</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://www.harness.io/blog/event-driven-architecture-redis-streams">Event-Driven Architecture with Redis Streams</a></li>
</ul>
]]></content:encoded></item><item><title>3.C45 Klaxit：Rust + Redis Streams 處理 Heroku Logplex</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-klaxit-rust-log-pipeline/</guid><description>&lt;p>這個案例的核心責任是說明 Redis Streams 在高吞吐 log ingestion 的 consumer group 分流。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Klaxit 用 Redis Streams 處理 Heroku Logplex 匯流的 log、自動偵測並修復 Heroku 平台層 perf 問題（在使用者察覺前）；正式 production 跑超過 6 個月、是團隊第一個 Rust project。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露 high-throughput log ingestion 對 Redis Streams 的壓力：用 consumer group 分流到多個 Rust worker、需要長時間穩定運轉。揭露 client library 品質決定 Redis Streams 在小眾語言（Rust）的可行性。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：XADD / XREAD / XREADGROUP 操作 / Consumer group + PEL。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dev.to/goodtouch/consuming-high-throughput-redis-streams-with-rust-580c">Consuming High-Throughput Redis Streams with Rust&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 Redis Streams 在高吞吐 log ingestion 的 consumer group 分流。</p>
<h2 id="觀察">觀察</h2>
<p>Klaxit 用 Redis Streams 處理 Heroku Logplex 匯流的 log、自動偵測並修復 Heroku 平台層 perf 問題（在使用者察覺前）；正式 production 跑超過 6 個月、是團隊第一個 Rust project。</p>
<h2 id="判讀">判讀</h2>
<p>揭露 high-throughput log ingestion 對 Redis Streams 的壓力：用 consumer group 分流到多個 Rust worker、需要長時間穩定運轉。揭露 client library 品質決定 Redis Streams 在小眾語言（Rust）的可行性。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：XADD / XREAD / XREADGROUP 操作 / Consumer group + PEL。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 vendor 頁</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>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://dev.to/goodtouch/consuming-high-throughput-redis-streams-with-rust-580c">Consuming High-Throughput Redis Streams with Rust</a></li>
</ul>
]]></content:encoded></item><item><title>3.C46 Learning.com：Redis 事件源退場（反例）</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-learning-com-event-source-retreat/</guid><description>&lt;p>這個反例的核心責任是說明 Redis 不適合長期事件儲存、揭露「Redis-as-event-store」的退場路徑。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>Learning.com 把 microservice 之間的 event store 放 Redis 上、一年內累積到 GB/週的 memory 成長、AOF fsync + EBS 磁碟 I/O 變成 latency 痛點。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露「Redis 不適合長期事件儲存」的退場路徑：event 移到 PostgreSQL、Redis 留做訊息佇列 + snapshot；中途靠 syncTimeout 調整、提升 IOPS、調整 AOF fsync 緩解。揭露 broker 選型要看「長期存儲是 source-of-truth 還是 transient」。&lt;strong>注意&lt;/strong>：此文討論的是 Redis-as-event-store 整體、Streams 是其中一塊、引用時要小心區分。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：Memory + retention 取捨 / Sentinel + Cluster 可靠性（持久化選型）。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern&lt;/a>。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://medium.com/lcom-techblog/a-year-with-redis-event-sourcing-lessons-learned-6736068e17cc">A Year with Redis Event Sourcing - Lessons Learned&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個反例的核心責任是說明 Redis 不適合長期事件儲存、揭露「Redis-as-event-store」的退場路徑。</p>
<h2 id="觀察">觀察</h2>
<p>Learning.com 把 microservice 之間的 event store 放 Redis 上、一年內累積到 GB/週的 memory 成長、AOF fsync + EBS 磁碟 I/O 變成 latency 痛點。</p>
<h2 id="判讀">判讀</h2>
<p>揭露「Redis 不適合長期事件儲存」的退場路徑：event 移到 PostgreSQL、Redis 留做訊息佇列 + snapshot；中途靠 syncTimeout 調整、提升 IOPS、調整 AOF fsync 緩解。揭露 broker 選型要看「長期存儲是 source-of-truth 還是 transient」。<strong>注意</strong>：此文討論的是 Redis-as-event-store 整體、Streams 是其中一塊、引用時要小心區分。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：Memory + retention 取捨 / Sentinel + Cluster 可靠性（持久化選型）。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a>。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://medium.com/lcom-techblog/a-year-with-redis-event-sourcing-lessons-learned-6736068e17cc">A Year with Redis Event Sourcing - Lessons Learned</a></li>
</ul>
]]></content:encoded></item><item><title>3.C47 PHP 微服務：Redis Streams + S3 hybrid storage</title><link>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/</link><pubDate>Mon, 18 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/03-message-queue/cases/redis-streams-mateusz-php-microservices/</guid><description>&lt;p>這個案例的核心責任是說明 in-memory 訊息的 payload 限制要靠 hybrid storage 解決。&lt;/p>
&lt;h2 id="觀察">觀察&lt;/h2>
&lt;p>PHP 雙微服務之間的可靠通訊、Kafka 在 PHP 生態工具薄弱、團隊無 Kafka 經驗、production 跑數月後寫此文；明確覆蓋 XADD / XREADGROUP / consumer group / MAXLEN / MINID / XDEL / XACK / XACKDEL（Redis 8.2+）/ XTRIM。&lt;/p>
&lt;h2 id="判讀">判讀&lt;/h2>
&lt;p>揭露 in-memory 訊息的 payload 限制：用 payload compression + S3 hybrid storage（大 payload 存 S3、stream 只放 reference）；用 MAXLEN/MINID 控制 stream 成長。揭露 broker 選型常被「語言生態 client 品質」主導、不是純技術 feature。&lt;strong>注意&lt;/strong>：作者是個人工程師、production 經驗但非知名公司。&lt;/p>
&lt;h2 id="對應大綱">對應大綱&lt;/h2>
&lt;p>Redis Streams 進階主題：XADD/XREAD/XREADGROUP 操作 / Retention (MAXLEN/MINID) / Memory + retention 取捨。&lt;/p>
&lt;h2 id="下一步路由">下一步路由&lt;/h2>
&lt;p>回 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/vendors/redis-streams/" data-link-title="Redis Streams" data-link-desc="Redis 生態內的 streams、append-only log &amp;#43; consumer group">Redis Streams vendor 頁&lt;/a> 與 &lt;a href="https://tarrragon.github.io/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16 Robinhood Faust&lt;/a>（語言生態對照）。&lt;/p>
&lt;h2 id="引用源">引用源&lt;/h2>
&lt;ul>
&lt;li>&lt;a href="https://dev.to/mtk3d/beyond-the-hype-why-we-chose-redis-streams-over-kafka-for-our-microservices-dmc">Beyond the Hype: Why We Chose Redis Streams Over Kafka for Our Microservices&lt;/a>&lt;/li>
&lt;/ul></description><content:encoded><![CDATA[<p>這個案例的核心責任是說明 in-memory 訊息的 payload 限制要靠 hybrid storage 解決。</p>
<h2 id="觀察">觀察</h2>
<p>PHP 雙微服務之間的可靠通訊、Kafka 在 PHP 生態工具薄弱、團隊無 Kafka 經驗、production 跑數月後寫此文；明確覆蓋 XADD / XREADGROUP / consumer group / MAXLEN / MINID / XDEL / XACK / XACKDEL（Redis 8.2+）/ XTRIM。</p>
<h2 id="判讀">判讀</h2>
<p>揭露 in-memory 訊息的 payload 限制：用 payload compression + S3 hybrid storage（大 payload 存 S3、stream 只放 reference）；用 MAXLEN/MINID 控制 stream 成長。揭露 broker 選型常被「語言生態 client 品質」主導、不是純技術 feature。<strong>注意</strong>：作者是個人工程師、production 經驗但非知名公司。</p>
<h2 id="對應大綱">對應大綱</h2>
<p>Redis Streams 進階主題：XADD/XREAD/XREADGROUP 操作 / Retention (MAXLEN/MINID) / Memory + retention 取捨。</p>
<h2 id="下一步路由">下一步路由</h2>
<p>回 <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 vendor 頁</a> 與 <a href="/blog/backend/03-message-queue/cases/kafka-robinhood-faust-python-streaming/" data-link-title="3.C16 Robinhood：Faust Python stream processing" data-link-desc="Robinhood 每天 billions of events、Python 團隊不想用 JVM 生態、把 Kafka Streams 移植到 Python。">3.C16 Robinhood Faust</a>（語言生態對照）。</p>
<h2 id="引用源">引用源</h2>
<ul>
<li><a href="https://dev.to/mtk3d/beyond-the-hype-why-we-chose-redis-streams-over-kafka-for-our-microservices-dmc">Beyond the Hype: Why We Chose Redis Streams Over Kafka for Our Microservices</a></li>
</ul>
]]></content:encoded></item></channel></rss>