<?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>Xclaim on Tarragon</title><link>https://tarrragon.github.io/blog/tags/xclaim/</link><description>Recent content in Xclaim on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 16 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/xclaim/index.xml" rel="self" type="application/rss+xml"/><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></channel></rss>