<?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>Distributed-Lock on Tarragon</title><link>https://tarrragon.github.io/blog/tags/distributed-lock/</link><description>Recent content in Distributed-Lock on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 23 Apr 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/distributed-lock/index.xml" rel="self" type="application/rss+xml"/><item><title>2.4 distributed lock 與租約</title><link>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</link><pubDate>Thu, 23 Apr 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/02-cache-redis/distributed-lock/</guid><description>&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/distributed-lock/" data-link-title="Distributed Lock" data-link-desc="跨機器跨 process 的互斥鎖、用 lease 機制處理 holder 失效">分散式鎖（distributed lock）&lt;/a>的核心責任是協調跨節點互斥，避免同一資源被重複處理。它解的是協調一致性問題；正式狀態一致性仍由交易邊界或版本控制承擔。&lt;/p>
&lt;h2 id="鎖與租約">鎖與租約&lt;/h2>
&lt;p>分散式鎖通常採租約語意：持鎖者在租約有效期內擁有操作權，租約到期後鎖自動釋放、需重新競爭。租約的存在是為了處理「持鎖者掛掉但沒釋放鎖」這個分散式系統無法避免的情況——沒有租約，一個 crash 的節點會讓鎖永遠卡住。代價是引入時鐘漂移、網路延遲與續租失敗這幾個新風險。&lt;/p>
&lt;p>在 Redis 上，取鎖是一個原子命令：&lt;code>SET lock:order:42 &amp;lt;token&amp;gt; NX PX 30000&lt;/code>。&lt;code>NX&lt;/code> 保證只有 key 不存在時才寫入，這讓「檢查鎖是否被持有」與「取得鎖」變成單一原子操作，避免兩個節點同時判斷「沒人持鎖」後都寫入。&lt;code>PX 30000&lt;/code> 設定 30 秒租約，持鎖者 crash 時鎖會在租約到期後自動消失。&lt;code>&amp;lt;token&amp;gt;&lt;/code> 是每個持鎖者產生的唯一隨機值，它的作用在釋放階段才顯現。&lt;/p>
&lt;p>釋放鎖不能用單純的 &lt;code>DEL lock:order:42&lt;/code>，因為這會誤刪別人的鎖。考慮這個時序：節點 A 取得鎖、處理超過 30 秒、租約到期自動釋放、節點 B 取得同一把鎖；此時 A 終於處理完、執行 &lt;code>DEL&lt;/code>，刪掉的是 B 的鎖。正確的釋放是「比對 token 相同才刪」，而這個 check-and-delete 必須原子，用 Lua script 達成：&lt;/p>





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





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