<?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>Conflict-Resolution on Tarragon</title><link>https://tarrragon.github.io/blog/tags/conflict-resolution/</link><description>Recent content in Conflict-Resolution on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Thu, 25 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/conflict-resolution/index.xml" rel="self" type="application/rss+xml"/><item><title>DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB&lt;/a> overview 的 implementation-layer deep article。寫作參照 &lt;a href="https://tarrragon.github.io/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology&lt;/a>。&lt;/p>&lt;/blockquote>
&lt;p>B2B SaaS 跟客戶 SLA 寫 99.99%、單 region 跑了一年遇過兩次 region-level outage、合計 downtime 已逼近 SLA 上限。team 要把核心 table 改 Global Tables active-active、首問是「multi-region write 之後資料還會一致嗎」。這個問題的答案是：&lt;em>不會、但有工程解法&lt;/em>；DynamoDB Global Tables 用 LWW（Last Writer Wins）跨 region async 同步、conflict 偵測跟 reconciliation 要 application 自己加。&lt;/p>
&lt;p>但 Global Tables 不只是 conflict 痛點。Disney+ 用同一個機制處理 cross-device sync（手機看一半回家用電視繼續）、Genesys 用同一個機制做 15 region B2B 客服平台的 99.999% 可用性。本文先講正向 access pattern（避免讓讀者誤以為 Global Tables 只是「跨 region 寫入會 conflict、所以痛苦」）、再展開 conflict resolution 跟 reconciliation 設計。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Workload 適配本 vendor 才繼續&lt;/strong>：DynamoDB 4 軸判讀（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）軸見 &lt;a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀&lt;/a>。Global Tables 是 &lt;em>已選 DynamoDB 後&lt;/em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner / Cosmos DB strong consistency level、不是用 LWW 補。&lt;/p>&lt;/blockquote>
&lt;h2 id="b2b-saas-vs-b2c-業務-driver-對比">B2B SaaS vs B2C 業務 driver 對比&lt;/h2>
&lt;p>Global Tables 不是預設選擇、是 &lt;em>業務性質&lt;/em> 決定的工程投資。&lt;code>9.C24 Genesys&lt;/code> 揭露兩條關鍵 frame — 可用性目標的業務 driver、跟每多一個 9 的 cost 指數成長。&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>業務性質&lt;/th>
 &lt;th>典型可用性目標&lt;/th>
 &lt;th>年停機容忍&lt;/th>
 &lt;th>Multi-region 投資邏輯&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>B2C 大型網站&lt;/td>
 &lt;td>99.9%&lt;/td>
 &lt;td>8.76 小時&lt;/td>
 &lt;td>通常單 region + PITR / cross-region backup 划算&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>B2B SaaS&lt;/td>
 &lt;td>99.95% 或 99.99%（合約）&lt;/td>
 &lt;td>4.4 小時 / 52.6 分鐘&lt;/td>
 &lt;td>合約義務、客戶 SLA 違約有金錢損失、ROI 正向&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>客服平台類&lt;/td>
 &lt;td>99.999%（合約客戶）&lt;/td>
 &lt;td>5.26 分鐘&lt;/td>
 &lt;td>客戶停線損失極大、15 region 投資合理（Genesys）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>B2C 大型網站&lt;/strong>通常 99.9% SLA、年停機 8.76 小時可接受、單 region + PITR + cross-region backup 是常見配置；改 Global Tables 邊際成本高、ROI 通常不正向。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/dynamodb/" data-link-title="DynamoDB" data-link-desc="AWS managed key-value、partition-based scaling、9000 萬 RPS sustained 實戰證據">DynamoDB</a> overview 的 implementation-layer deep article。寫作參照 <a href="/blog/posts/vendor-%E6%B7%B1%E5%BA%A6%E6%8A%80%E8%A1%93%E6%96%87%E7%AB%A0%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84%E5%90%8C-vendor-%E7%B3%BB%E5%88%97%E7%9A%84%E9%96%8B%E5%A0%B4%E8%BC%AA%E6%9B%BF%E9%A9%97%E8%AD%89/" data-link-title="Vendor 深度技術文章方法論的演化紀錄：同 vendor 系列的開場輪替驗證" data-link-desc="vendor overview 飽和後要寫單一功能深度文章、需要選題與結構依據時回來。這套方法論的驗證來源與 cadence variant 在高風險場景（同 vendor sub-tool 系列）的實證。">vendor deep article methodology</a>。</p></blockquote>
<p>B2B SaaS 跟客戶 SLA 寫 99.99%、單 region 跑了一年遇過兩次 region-level outage、合計 downtime 已逼近 SLA 上限。team 要把核心 table 改 Global Tables active-active、首問是「multi-region write 之後資料還會一致嗎」。這個問題的答案是：<em>不會、但有工程解法</em>；DynamoDB Global Tables 用 LWW（Last Writer Wins）跨 region async 同步、conflict 偵測跟 reconciliation 要 application 自己加。</p>
<p>但 Global Tables 不只是 conflict 痛點。Disney+ 用同一個機制處理 cross-device sync（手機看一半回家用電視繼續）、Genesys 用同一個機制做 15 region B2B 客服平台的 99.999% 可用性。本文先講正向 access pattern（避免讓讀者誤以為 Global Tables 只是「跨 region 寫入會 conflict、所以痛苦」）、再展開 conflict resolution 跟 reconciliation 設計。</p>
<blockquote>
<p><strong>Workload 適配本 vendor 才繼續</strong>：DynamoDB 4 軸判讀（PK 天然均勻 / control plane vs data plane / consistency 可接受 eventual / access pattern 穩定）軸見 <a href="../single-table-design-pattern/#dynamodb-%e9%81%a9%e7%94%a8%e5%ba%a6%e5%89%8d%e7%bd%ae%e5%88%a4%e8%ae%804-%e8%bb%b8">single-table-design-pattern 開頭 4 軸前置判讀</a>。Global Tables 是 <em>已選 DynamoDB 後</em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner / Cosmos DB strong consistency level、不是用 LWW 補。</p></blockquote>
<h2 id="b2b-saas-vs-b2c-業務-driver-對比">B2B SaaS vs B2C 業務 driver 對比</h2>
<p>Global Tables 不是預設選擇、是 <em>業務性質</em> 決定的工程投資。<code>9.C24 Genesys</code> 揭露兩條關鍵 frame — 可用性目標的業務 driver、跟每多一個 9 的 cost 指數成長。</p>
<table>
  <thead>
      <tr>
          <th>業務性質</th>
          <th>典型可用性目標</th>
          <th>年停機容忍</th>
          <th>Multi-region 投資邏輯</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>B2C 大型網站</td>
          <td>99.9%</td>
          <td>8.76 小時</td>
          <td>通常單 region + PITR / cross-region backup 划算</td>
      </tr>
      <tr>
          <td>B2B SaaS</td>
          <td>99.95% 或 99.99%（合約）</td>
          <td>4.4 小時 / 52.6 分鐘</td>
          <td>合約義務、客戶 SLA 違約有金錢損失、ROI 正向</td>
      </tr>
      <tr>
          <td>客服平台類</td>
          <td>99.999%（合約客戶）</td>
          <td>5.26 分鐘</td>
          <td>客戶停線損失極大、15 region 投資合理（Genesys）</td>
      </tr>
  </tbody>
</table>
<p><strong>B2C 大型網站</strong>通常 99.9% SLA、年停機 8.76 小時可接受、單 region + PITR + cross-region backup 是常見配置；改 Global Tables 邊際成本高、ROI 通常不正向。</p>
<p><strong>B2B SaaS</strong> 99.95% 或 99.99% SLA 多半寫進合約、違約有具體金錢損失；Global Tables 的 N region cost 對比 SLA 違約成本通常 ROI 正向。critical 的是 <em>合約義務</em> 不是 <em>技術完美</em>。</p>
<p><strong>客服平台類</strong> 99.999% 是極端可用性目標、年停機 5.26 分鐘、Genesys 撐 8000+ orgs 的客服平台、客戶停線損失極大、跨 15 region 的 active-active 是合理投資。但 <em>不是每個 SaaS 都該追 99.999%</em>、是 <em>業務性質決定下限</em>。</p>
<p><strong>成本對比</strong>（<code>9.C24</code> 揭露）：15 region 成本約 = 1 region 的 15x（base table cost）+ 跨 region replication WCU。每多一個 9、容量規劃跟運維成本指數成長。</p>
<blockquote>
<p><strong>Scope warning（指標口徑紀律）</strong>：99.999% 是「12 個月滾動歷史值、不代表未來持續達成」（<code>9.C24</code> 警惕段第 1 條）。可用性是滾動指標、不是恆久承諾。引用 Genesys 99.999% 數字時要明示口徑（滾動 / customer-facing），不要寫成「DynamoDB 保證 99.999%」。</p></blockquote>
<h2 id="正向-access-pattern不只-conflict-議題">正向 access pattern：不只 conflict 議題</h2>
<p>Global Tables 不只是 DR / availability、也是正向 access pattern 的工程方案。先建立正向用例的判讀、再進 conflict 細節。</p>
<p><strong>Cross-device sync</strong>（<code>9.C27 Disney+</code> 揭露）：用戶在手機看到一半、晚上回家用電視繼續、播放進度跨裝置同步。Global Tables 自然解這個 access pattern — 用戶在不同 region 登入同帳號、寫入自動同步、最終一致性可接受場景。</p>
<p><strong>Global read（latency 優化）</strong>：跨地域用戶讀取就近 region 副本、latency 從 200ms 降到 &lt; 10ms。read 比 write 多很多倍的 workload（feed / catalog / user profile）受益最大。</p>
<p><strong>DR failover</strong>：region-level outage 時 application 切到 secondary region 繼續服務、RTO 通常 &lt; 5 分鐘（DNS / routing 切換時間、不含 application 端 reconnect）。</p>
<p><strong>B2C 也可能划算的場景</strong>：cross-device sync 是 <em>user-facing experience</em>、不是合規 / SLA driver。B2C 大規模平台（Disney+ / Spotify 類）也可能投資 Global Tables。判讀軸是「sync 體驗是否核心 UX」、不只「合約 SLA」。</p>
<h2 id="核心機制lww-conflict-resolution">核心機制：LWW conflict resolution</h2>
<p>Global Tables 的 first-class concept：</p>
<ul>
<li><strong>Multi-region active-active</strong>：每個 region 都能寫、async replication；typical replication latency &lt; 1s 但 <em>無 SLA</em></li>
<li><strong>LWW by wall clock</strong>：conflict 由 attribute <code>aws:rep:updatetime</code> 決定、純物理時間；不是 logical clock、不是 vector clock</li>
<li><strong>同 region read-your-write</strong>：本 region 寫立即可讀（同 region quorum 內）、其他 region 看到要等 replication</li>
<li><strong>Capacity 獨立</strong>：每個 region 自己的 RCU/WCU、<code>ReplicatedWriteCapacityUnits</code> 是跨 region replication 額外 WCU、按 region 數倍計</li>
</ul>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/consistency-level/" data-link-title="Consistency Level" data-link-desc="資料系統對讀寫一致性語意的可選擇層級">consistency level</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>。</p>
<h2 id="設計流程">設計流程</h2>
<p>從 access pattern 分類到 reconciliation pipeline 的 6 步流程。</p>
<h4 id="step-1access-pattern-分類">Step 1：access pattern 分類</h4>
<p>把 table 中的資料分兩類：</p>
<ul>
<li><strong>region-pinned data</strong>：user 主要 region（合規 / 地理 affinity）；不啟用 Global Tables、用 region-pinned cluster</li>
<li><strong>global data</strong>：跨 region read / cross-device sync；啟用 Global Tables</li>
</ul>
<p>不是所有 table 都該上 Global Tables；user profile 跨 region 同步、但用戶交易紀錄可能該 pin 在合規 region。</p>
<h4 id="step-2啟用-global-tables">Step 2：啟用 Global Tables</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">aws dynamodb update-table <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --table-name orders <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --replica-updates <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  <span class="s1">&#39;[{&#34;Create&#34;: {&#34;RegionName&#34;: &#34;us-east-1&#34;}}]&#39;</span></span></span></code></pre></div><p>加 region 後 vendor 自動 backfill；backfill 期間 capacity 雙倍（原 region + 新 region 同步流量）、要預留 capacity buffer。</p>
<h4 id="step-3application-寫入策略">Step 3：application 寫入策略</h4>
<p>兩種寫入策略：</p>
<ul>
<li><strong>home region write</strong>：每 user 固定一個 home region 寫、避免 conflict；user 跨 region 漫遊時透過 routing 仍寫 home</li>
<li><strong>nearest region write</strong>：latency 優先、user 寫就近 region；conflict 機率高、必須加 idempotency 跟 reconciliation</li>
</ul>
<p>選擇：</p>
<table>
  <thead>
      <tr>
          <th>場景</th>
          <th>寫入策略</th>
          <th>理由</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>user profile / 設定</td>
          <td>home region write</td>
          <td>conflict 少、簡單</td>
      </tr>
      <tr>
          <td>cross-device sync</td>
          <td>nearest region write</td>
          <td>用戶在不同裝置同時操作、容忍 LWW</td>
      </tr>
      <tr>
          <td>訂單 / 金流</td>
          <td>home region write</td>
          <td>業務不容許 conflict 損失</td>
      </tr>
  </tbody>
</table>
<h4 id="step-4idempotency-設計">Step 4：idempotency 設計</h4>
<p>每筆 write 加 <code>request_id</code> 或 <code>client_timestamp</code>、application 端去重：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">write_with_idempotency</span><span class="p">(</span><span class="n">user_id</span><span class="p">,</span> <span class="n">action</span><span class="p">,</span> <span class="n">request_id</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">table</span><span class="o">.</span><span class="n">put_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">Item</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">            <span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">user_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ACTION#</span><span class="si">{</span><span class="n">action</span><span class="si">}</span><span class="s2">#</span><span class="si">{</span><span class="n">request_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="s2">&#34;ts&#34;</span><span class="p">:</span> <span class="n">datetime</span><span class="o">.</span><span class="n">utcnow</span><span class="p">()</span><span class="o">.</span><span class="n">isoformat</span><span class="p">(),</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;request_id&#34;</span><span class="p">:</span> <span class="n">request_id</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="p">},</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;attribute_not_exists(request_id)&#34;</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="p">)</span></span></span></code></pre></div><p><code>ConditionExpression</code> 在同一 region 內擋重複；跨 region eventual 仍可能 race，conflict 落到 LWW + reconciliation。</p>
<blockquote>
<p><strong>Scope warning（重要）</strong>：「加 request_id 或 client_timestamp」具體實作屬通用工程知識、<code>9.C26 PayPay</code> case 揭露「通知不可丟失」的需求分層、<em>沒有</em> 揭露具體 idempotency 實作。引用 PayPay 時要降溫成「PayPay 揭露需求分層（通知 vs 訊息）、idempotency 為通用工程實作」、不寫成「PayPay 使用 request_id」（陷阱 4：把通用工程實作寫成 case 揭露）。</p></blockquote>
<h4 id="step-5conflict-detection">Step 5：conflict detection</h4>
<p>DynamoDB Streams 訂閱、Lambda 比較 <code>aws:rep:updatetime</code> 跟 application timestamp、抓出可疑 conflict 進 reconciliation queue：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">def</span> <span class="nf">detect_conflict</span><span class="p">(</span><span class="n">stream_event</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">new_image</span> <span class="o">=</span> <span class="n">stream_event</span><span class="p">[</span><span class="s2">&#34;dynamodb&#34;</span><span class="p">][</span><span class="s2">&#34;NewImage&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">repl_time</span> <span class="o">=</span> <span class="n">new_image</span><span class="p">[</span><span class="s2">&#34;aws:rep:updatetime&#34;</span><span class="p">][</span><span class="s2">&#34;S&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">app_time</span> <span class="o">=</span> <span class="n">new_image</span><span class="p">[</span><span class="s2">&#34;client_timestamp&#34;</span><span class="p">][</span><span class="s2">&#34;S&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nb">abs</span><span class="p">(</span><span class="n">parse</span><span class="p">(</span><span class="n">repl_time</span><span class="p">)</span> <span class="o">-</span> <span class="n">parse</span><span class="p">(</span><span class="n">app_time</span><span class="p">))</span> <span class="o">&gt;</span> <span class="n">timedelta</span><span class="p">(</span><span class="n">seconds</span><span class="o">=</span><span class="mi">5</span><span class="p">):</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">        <span class="c1"># 可疑 conflict、進 reconciliation</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">sqs</span><span class="o">.</span><span class="n">send_message</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="n">QueueUrl</span><span class="o">=</span><span class="n">RECONCILIATION_QUEUE</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">            <span class="n">MessageBody</span><span class="o">=</span><span class="n">json</span><span class="o">.</span><span class="n">dumps</span><span class="p">(</span><span class="n">stream_event</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">)</span></span></span></code></pre></div><blockquote>
<p><strong>Scope warning</strong>：DynamoDB Streams 用法屬通用工程實作、<code>9.C26 PayPay</code> case <em>沒有</em> 明示用 Streams、引用時要分層（PayPay 揭露需求、Streams 是工程實作的標準解）。</p></blockquote>
<h4 id="step-6reconciliation-pipeline">Step 6：reconciliation pipeline</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-text" data-lang="text"><span class="line"><span class="ln">1</span><span class="cl">Conflict event → SQS queue → Lambda / human review → merge logic → write back</span></span></code></pre></div><p>merge logic 視業務而定：</p>
<ul>
<li>訂單金額 conflict：抓最大值（避免少收）</li>
<li>用戶設定 conflict：抓最新（user-facing 行為一致）</li>
<li>watchlist conflict：union（兩裝置加的都保留）</li>
</ul>
<p><strong>驗證點</strong>：DR drill 演 region outage、確認 secondary region 接管後 read / write 都正常；<code>ReplicationLatency</code> p99 &lt; 1s。</p>
<p><strong>Rollback boundary</strong>：region 可逐個移除、但 active-active 改 active-passive 期間 application 需配合路由切換；先 application 切再移 region、不可同時做。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>實際部署常見的 5 種失敗：</p>
<h4 id="case-1lww-默默吃掉-write">Case 1：LWW 默默吃掉 write</h4>
<p>跨 region 同一 record concurrent update、後到的 write 因 timestamp 較大蓋過先到的；business 看到「我送出的更新沒了」、稽核 log 才發現 conflict。修法：critical write 加 <code>ConditionExpression</code> 比較 <code>version</code> attribute、conflict 時 application 端 retry + merge；不要依賴 LWW 作為 conflict 解。</p>
<h4 id="case-2clock-skew-讓-lww-倒置">Case 2：Clock skew 讓 LWW 倒置</h4>
<p>region A 寫入 timestamp 因 NTP skew 比 region B 後寫快 200ms、結果舊資料贏。修法：依靠 application timestamp + monotonic counter、不依賴 server wall clock；critical write 用 conditional version + retry。</p>
<blockquote>
<p><strong>Scope warning</strong>：「200ms NTP skew」具體數字屬通用工程估算、case 未揭露具體 skew 範圍。</p></blockquote>
<h4 id="case-3replication-lag-撞-slo">Case 3：Replication lag 撞 SLO</h4>
<p>大 batch write 期間 replication lag 從 1s 變 30s、跨 region read 看到 30s 前資料、application 端 user 操作異常。修法：偵測 <code>ReplicationLatency</code> 升高時 application 端切 home region read、避免跨 region eventual read；把 replication lag 加進 SLO 監控、設 alarm。</p>
<h4 id="case-4dr-切換後-stale-data-持續-propagate">Case 4：DR 切換後 stale data 持續 propagate</h4>
<p>primary region outage 切到 secondary、舊 primary 恢復後仍把 outdated data 推回去、覆蓋 secondary 期間的新寫入。修法：DR runbook 含「舊 primary 恢復後人工 reconciliation 或重建」step、不可全自動 catch-up；舊 primary 恢復前先確認 replication 方向是「從 secondary catch up」而非「推舊資料回 secondary」。</p>
<h4 id="case-5跨-region-transaction-失敗">Case 5：跨 region transaction 失敗</h4>
<p>application 試圖跨 region <code>TransactWriteItems</code>、API 不支援跨 region transaction、原子性破裂。修法：transaction 限同 region 內、跨 region 用 <a href="/blog/backend/knowledge-cards/saga/" data-link-title="Saga" data-link-desc="處理跨服務分散事務的補償型 transaction 序列、用最終一致換 ACID atomic">saga</a> + idempotent + reconciliation；不要把同 region 的 transaction 假設搬到跨 region。</p>
<p><strong>Anti-recommendation</strong>：single-region availability 已達 99.95% + RTO 可接受 1 小時 + 預算敏感（特別 B2C 場景）→ 用 PITR + 跨 region backup 而非 Global Tables；Global Tables cost = N × single region cost 不止（對應 B2B vs B2C driver 對比）。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>ReplicationLatency</code>：p99 通常 &lt; 1s、建議 SLO 設 5s alarm</li>
<li><code>PendingReplicationCount</code>：積壓量、batch write 期間會升高</li>
<li><code>ReplicatedWriteCapacityUnits</code>：跨 region replication 額外 WCU、按 region 數倍計</li>
</ul>
<p>DynamoDB Streams + Lambda：抓 conflict event、寫進獨立 audit table；reconciliation job 從 audit table 跑、不直接動 base table。</p>
<p><strong>Region-level dashboard</strong>：每個 region 獨立 capacity / latency / error rate panel；DR drill 看是否能在 RTO 內切換。</p>
<p><strong>Cost monitoring</strong>：</p>
<ul>
<li>Global Tables cost ≈ N region × base cost + replication WCU</li>
<li>4 region 成本約 4.5x single region；15 region（Genesys 規模）約 15x</li>
<li>每多一個 region 都要重新算 ROI（軸 6 vendor crossover 的延伸）</li>
</ul>
<p><strong>指標口徑紀律</strong>（重要）：99.99% / 99.999% SLA 是 <em>滾動指標 + 歷史值</em>、不是永久承諾；引用 Genesys 99.999% 時明示「12 個月滾動 / customer-facing」、不寫成「DynamoDB 保證 99.999%」。</p>
<p>接回 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>、<a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="frame-5region-pinned-global-tables-吸收合規邊界">Frame 5：region-pinned Global Tables 吸收合規邊界</h3>
<p>Global Tables 不只是高可用工具、也是 <em>合規邊界</em>（<a href="/blog/backend/knowledge-cards/data-residency/" data-link-title="Data Residency" data-link-desc="合規要求資料留在特定地理邊界內、跨境複製違反合規、推動 fleet 拓樸決策">Data Residency</a> 拓樸）的吸收層。DynamoDB 在 vendor capability 層級支援 <em>region-pinned replication</em> — 每張 table 可獨立決定哪些 region 參與 replication group、部分 region 可不加入。這個 capability 同時服務三類場景：合規分離（受監管市場資料不跨境）、cost / latency 取捨（資料只在主要服務 region 同步）、災備拓樸（少數 region 純讀備援）。<code>9.C24 Genesys</code> 15 region 揭露的是 <em>延遲就近接入</em> 的 B2B SaaS 拓樸（客戶服務延遲敏感、必須在客戶所在地有 region）— case 原文沒明示合規應用、但 region-pinned capability 在 Genesys 規模下天然能容納合規市場分離、是同 capability 的 <em>可能應用維度</em>、不是 case 已驗證的具體實踐。</p>
<p>跨 vendor 對照：</p>
<table>
  <thead>
      <tr>
          <th>Vendor</th>
          <th>合規吸收機制</th>
          <th>拓樸特性</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>DynamoDB</td>
          <td>region-pinned Global Tables（按 region 開關 replication、各市場可分離）</td>
          <td>仍是 active-active、但 replication 範圍可控</td>
      </tr>
      <tr>
          <td>Aurora</td>
          <td>fleet 拓樸（每市場獨立 cluster、合規禁止跨境 = Global Database 反指標）</td>
          <td>active-passive per market、跨市場不複製</td>
      </tr>
      <tr>
          <td>CockroachDB</td>
          <td>locality + placement（邏輯一個 cluster + region pinning + Outposts）</td>
          <td>單 logical cluster、physical row 鎖在合規 region</td>
      </tr>
      <tr>
          <td>MongoDB / Cosmos DB</td>
          <td>cluster-per-region（無 row-level locality 等價物、整 cluster 切割）</td>
          <td>各 region 獨立 cluster、application 層做市場 routing</td>
      </tr>
  </tbody>
</table>
<p><strong>為什麼 DynamoDB 在這個 frame 退化得最輕</strong>：Global Tables 的 region 開關是 <em>attribute 級</em> 設計（每張 table 可獨立決定哪些 region 參與）、不像 Aurora 必須整 cluster 拆。讀者要把「跨境合規 + 高可用」雙重需求兼顧時、DynamoDB 是最少結構性改造的路徑 — 但代價是 LWW conflict 跟 reconciliation 設計仍要自己做。</p>
<p><strong>何時 region-pinned 而非 active-active</strong>：受監管金融 / 個資跨境禁止的市場（如 GDPR strict 條款區、中國個資法 PIPL、巴西 LGPD）— 該 region 仍開 DynamoDB table、但 <em>不加入 Global Tables replication group</em>、跟其他 region 完全切割。capability 設計上支援這種按 region 開關 replication 的拓樸；具體是否套用、要看 <em>讀者自己的市場合規清單</em>、不是把 Genesys 規模當必然證據（Genesys case 揭露的是延遲就近接入、未明示合規分離實踐）。</p>
<h3 id="disney-vs-genesys兩種-global-tables-工程動機">Disney+ vs Genesys：兩種 Global Tables 工程動機</h3>
<p><code>9.C27 Disney+</code> 跟 <code>9.C24 Genesys</code> 是 Global Tables 兩種不同的工程動機：</p>
<ul>
<li><strong>Disney+</strong>：cross-device sync 是 user-facing UX、watchlist + 播放進度跨裝置同步、B2C 但 sync 是 core experience</li>
<li><strong>Genesys</strong>：99.999% B2B SaaS 合約義務、15 region active-active、客服平台停線損失極大</li>
</ul>
<p>兩個 case 都用 Global Tables、但動機完全不同 — Disney+ 是 UX driver、Genesys 是合約 driver。寫進你自己的設計時要明示自己屬哪一型，因為兩種型別的 cost 容忍度跟 conflict 容忍度完全不同。</p>
<h3 id="sibling-與-cross-link">Sibling 與 cross-link</h3>
<ul>
<li><a href="/blog/backend/01-database/vendors/dynamodb/consistency-model-optimization/" data-link-title="DynamoDB Strongly Consistent → Eventually Consistent：same protocol, different contract" data-link-desc="DynamoDB consistency model 從 strongly consistent read 改 eventually consistent read 是 50% cost 優化但風險集中在 application contract — 同 vendor / 同 protocol / 同 table / 不同 read consistency；驗證 [#128](/report/data-topology-as-audit-dimension/) self-aware limitation 提出的 consistency axis 候選；涵蓋 read pattern audit / 5 個 production 踩雷">consistency-model-optimization</a> — 同 region eventual / strong 取捨、本篇是跨 region 延伸</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/on-demand-vs-provisioned/" data-link-title="DynamoDB On-Demand vs Provisioned：6 軸決策、auto-scaling 邊界與 cost crossover" data-link-desc="capacity mode 選擇不是單軸 peak/avg ratio；本文展開 6 軸決策（peak/avg / 讀寫比 trend / surge 暫時 vs 永久 baseline / predictable-peak vs flash-sale / DBA 工時釋放 / vendor vs 自管 cost crossover），含 Zomato 50% 成本下降、Zoom 30x permanent surge、Amazon Ads sustained workload 等 case 分軸 anchor">on-demand-vs-provisioned</a> — 多 region capacity 規劃放大、軸 5 工時釋放在 multi-region 更顯著</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/partition-key-antipatterns/" data-link-title="DynamoDB Partition Key 反模式與 Write Sharding：composite key 修復跟 mode × partition 交叉判讀" data-link-desc="DynamoDB partition 上限 1000 WCU 是 hot partition 的根因；composite key（event_id &#43; shard suffix）跟 calculated shard（hash % N）兩種修法、mode × partition 在 provisioned / on-demand 不同表現，以及 9.C15 Tixcraft 6750x 擴展的工程細節">partition-key-antipatterns</a> — hot partition 跨 region 同樣存在、每個 region 的 partition 都要均勻</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/single-table-design-pattern/" data-link-title="DynamoDB Single-Table Design：從適用度前置判讀到 access pattern 反推 PK/SK" data-link-desc="DynamoDB single-table 設計不是「資料表越少越好」，而是 access pattern 反推 PK/SK 跟 GSI；本文先做 DynamoDB 適用度 4 軸前置判讀（PK 天然均勻 / control plane vs data plane / consistency / access pattern 穩定），再展開設計流程、failure modes 與 durable queue 正向用例">single-table-design-pattern</a> — single-table 設計在 multi-region 仍適用、access pattern 反推 PK/SK 不變</li>
<li>替代路由：global strong consistency 必要 → Spanner / Cosmos DB strong consistency level</li>
<li>Migration playbook：single-region → Global Tables 屬 topology re-layout、對應 <a href="/blog/posts/migration-playbook-%E6%96%B9%E6%B3%95%E8%AB%96%E7%9A%84%E6%BC%94%E5%8C%96%E7%B4%80%E9%8C%84stage-0-variant-%E8%A6%8F%E5%8A%83%E6%8A%8A-collapse-%E7%8E%87%E5%BE%9E-60-%E9%99%8D%E5%88%B0-0/" data-link-title="Migration Playbook 方法論的演化紀錄：Stage 0 variant 規劃把 collapse 率從 60% 降到 0%" data-link-desc="跨 vendor migration playbook 需要獨立寫作方法論的依據，以及這套方法論從三輪 batch dogfood 中演化出來的驗證證據。">migration playbook methodology</a> Type F</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">Genesys 9.C24</a> 互引：15 region 5 個 9 可用性的工程實踐 + B2B SaaS 業務 driver</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/disney-plus-content-metadata/" data-link-title="9.C27 Disney&#43;：DynamoDB 撐每日數十億動作的觀看歷史" data-link-desc="Disney&#43; 用 DynamoDB 撐每日數十億動作的觀看歷史、watchlist、播放進度等串流 metadata">Disney+ 9.C27</a> 互引：cross-device sync 作為正向 access pattern</li>
<li>跟 <a href="/blog/backend/09-performance-capacity/cases/paypay-mobile-payment-messaging/" data-link-title="9.C26 PayPay：行動支付每日 3 億訊息的 DynamoDB 後端" data-link-desc="日本最大行動支付 PayPay 每日 3 億訊息、用 DynamoDB 處理通知與訊息功能、支撐次秒級反應">PayPay 9.C26</a> 互引：揭露需求分層（通知 vs 訊息）、idempotency / Streams 為通用工程實作、PayPay 未公開揭露具體實作</li>
</ul>
]]></content:encoded></item><item><title>Cosmos DB Multi-Region Write：active-active、LWW、custom merge、Strong + multi-region 互斥的 AP 取捨</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/multi-region-write-conflict/</guid><description>&lt;p>Cosmos DB 是 &lt;em>AP 系統&lt;/em>（&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP&lt;/a> 三選二、放棄跨 region linearizability 換取 multi-region write 可用性）。跨 region 寫同一筆 document 必然有 conflict、Cosmos DB 提供三種 resolution policy 處理：LWW（Last-Writer-Wins）、custom merge stored procedure、conflict feed manual reconciliation。本文先講 AP 取捨的硬約束（為什麼 Strong consistency 跟 multi-region write 互斥）、再進三種 resolution 機制、再進廣告 SLA vs 實測可用性的鏈路拆解（DB 端 SLA 不等於使用者體驗）。&lt;/p>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁&lt;/a> 的深度展開、也是 &lt;em>Strong + multi-region 互斥&lt;/em> 議題的 SSoT 主寫位置（&lt;a href="../consistency-levels-engineering/">consistency-levels-engineering&lt;/a> cross-link 過來、不展開）。Case anchor 是 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth&lt;/a>（AR 遊戲跨 region 寫入、5 consistency level + multi-region SLA）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS&lt;/a>（Black Friday 全球零售）+ &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected&lt;/a>（鏈路 SLA 拆解、跨 vendor 適用做 frame anchor）。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>Cosmos DB 適用度前置判讀&lt;/strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 &lt;a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing&lt;/a>、本篇不重複展開。Multi-region write + conflict resolution 是 &lt;em>已選 Cosmos DB 後&lt;/em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner 或 Cosmos DB Strong（單一 write region）、不是用 LWW 補。&lt;/p></description><content:encoded><![CDATA[<p>Cosmos DB 是 <em>AP 系統</em>（<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 三選二、放棄跨 region linearizability 換取 multi-region write 可用性）。跨 region 寫同一筆 document 必然有 conflict、Cosmos DB 提供三種 resolution policy 處理：LWW（Last-Writer-Wins）、custom merge stored procedure、conflict feed manual reconciliation。本文先講 AP 取捨的硬約束（為什麼 Strong consistency 跟 multi-region write 互斥）、再進三種 resolution 機制、再進廣告 SLA vs 實測可用性的鏈路拆解（DB 端 SLA 不等於使用者體驗）。</p>
<p>本文是 <a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor 頁</a> 的深度展開、也是 <em>Strong + multi-region 互斥</em> 議題的 SSoT 主寫位置（<a href="../consistency-levels-engineering/">consistency-levels-engineering</a> cross-link 過來、不展開）。Case anchor 是 <a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth</a>（AR 遊戲跨 region 寫入、5 consistency level + multi-region SLA）+ <a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS</a>（Black Friday 全球零售）+ <a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a>（鏈路 SLA 拆解、跨 vendor 適用做 frame anchor）。</p>
<blockquote>
<p><strong>Cosmos DB 適用度前置判讀</strong>：本篇假設 workload 已通過 Cosmos DB 適用度四層 framing（API model 三型遷移路徑 / RU 思維轉換成本 / multi-model 差異化是否真用上 / 跨雲 hedging vs 單雲 lock-in）— 詳見 <a href="../mongodb-api-vs-sql-api/#%e5%9b%9b%e5%b1%a4-framingvendor-selection-%e7%9a%84%e7%9c%9f%e5%af%a6%e6%b1%ba%e7%ad%96%e8%bb%b8">mongodb-api-vs-sql-api 開頭四層 framing</a>、本篇不重複展開。Multi-region write + conflict resolution 是 <em>已選 Cosmos DB 後</em> 的拓樸決策；strong global consistency 必要的 workload 應走 Spanner 或 Cosmos DB Strong（單一 write region）、不是用 LWW 補。</p></blockquote>
<h2 id="問題情境active-active-的-conflict-是必然代價">問題情境：active-active 的 conflict 是必然代價</h2>
<p>典型觸發場景：產品要 global active-active（每個 region 都能寫、低延遲）、Cosmos DB 是 AP 系統、不像 Spanner 用 quorum 強一致；跨 region 寫同一筆 document 必然有 conflict、團隊不知道「conflict 真的發生時、誰贏 / 怎麼處理 / 業務語義保不保得住」。</p>
<p>讀者徵兆：</p>
<ul>
<li>「multi-region write 開了、user 在 A region 寫『加入購物車』、B region 寫『移除購物車』、最後哪個贏」</li>
<li>「LWW 用 timestamp 決定、client clock skew 不就破壞了嗎」</li>
<li>「conflict feed 是什麼、要不要消費」</li>
<li>「multi-region write 開了之後 consistency level 還能設 Strong 嗎」</li>
<li>「廣告寫 99.999%、為什麼實測只有 99%」</li>
</ul>
<p>真實壓力：購物車跨 region 寫入丟失、遊戲玩家狀態跨 region 衝突回滾、IoT device 跨 region 寫 telemetry 後消失。這些事故的根因不是 bug、是 multi-region write 的 <em>設計取捨</em>、需要在 selection 階段就決定 conflict resolution policy。</p>
<h2 id="核心機制">核心機制</h2>
<h3 id="ap-取捨的硬約束為什麼-strong--multi-region-write-互斥">AP 取捨的硬約束：為什麼 Strong + multi-region write 互斥</h3>
<p>Cosmos DB 是 AP 系統（在 partition 的情況下選 availability 跟 partition tolerance、放棄 cross-region linearizability）。multi-region write 的兩個前置條件：</p>
<ul>
<li>account 開啟 <code>enableMultipleWriteLocations = true</code></li>
<li>consistency level <em>不能設 Strong</em>（multi-region write 跟 Strong 互斥、時間敏感 claim、查 <a href="https://learn.microsoft.com/azure/cosmos-db/consistency-levels">最新文件</a>）</li>
</ul>
<p>為什麼互斥（CAP 三選二的硬約束）：</p>
<ul>
<li><strong>Strong consistency</strong> 在 Cosmos DB 的實作是 quorum-based linearizable read — 確保 read 拿到最新 commit、需要 <em>單一 write region</em> 來保證寫入順序</li>
<li><strong>Multi-region write</strong> 是 active-active、每個 region 都能寫 — 不存在「單一 write region」、寫入是 LWW-based eventual consistency</li>
<li>兩者在技術上 <em>不能同時成立</em> — 不是 Microsoft 工程選擇問題、是 distributed system 的基本限制（跟 Spanner 用 Paxos quorum + TrueTime 不同的設計路徑）</li>
</ul>
<p>對 selection 的意義：產品要「全球都能寫」就接受 eventual consistency；產品要「全球 linearizable」就轉 Spanner / Aurora DSQL、Cosmos DB 不是替代品。把 Cosmos DB Strong 跟 Spanner external consistency 等同視之是 <em>常見的選型誤判</em>。</p>
<p><a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 的 Strong 段只 cross-link 過來、不展開 conflict resolution 細節 — 本篇是 SSoT 主寫位置。</p>
<h3 id="conflict-偵測">Conflict 偵測</h3>
<p>同一 document（partition key + id）在多 region 並發寫入、Cosmos DB 偵測為 conflict。偵測機制基於 LSN（log sequence number）、不是 timestamp — 兩個 region 對同一 document 寫入時、replication 過程比對 LSN 發現分歧、進 resolution。</p>
<h3 id="三種-conflict-resolution-policy">三種 conflict resolution policy</h3>
<h4 id="lwwlast-writer-wins預設">LWW（Last-Writer-Wins、預設）</h4>
<ul>
<li>機制：用 <code>_ts</code>（system timestamp）或自訂 numeric property、value 大的贏</li>
<li>副作用：clock skew 在 ms 級就能讓「先寫的反而贏」、業務邏輯破洞</li>
<li>適合：純覆寫場景（如玩家位置最新值、IoT 最新讀數）— write 順序不影響業務語義</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;LastWriterWins&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionPath&#34;</span><span class="p">:</span> <span class="s2">&#34;/customTimestamp&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="custom-merge-stored-procedure">Custom merge stored procedure</h4>
<ul>
<li>機制：寫一個 JavaScript stored proc、conflict 時 Cosmos DB 呼叫、proc 回傳 merge 結果</li>
<li>適合：要保留業務語義的場景（購物車 merge = union 兩邊 items、計數器 merge = sum、status 機器 merge = 狀態圖規則）</li>
<li>風險：stored proc 在 Cosmos DB JavaScript runtime 跑、有 timeout / RU 限制；複雜 merge 邏輯難 debug</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionProcedure&#34;</span><span class="p">:</span> <span class="s2">&#34;dbs/mydb/colls/mycoll/sprocs/resolveCart&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><h4 id="conflict-feed-manual-reconciliation">Conflict feed manual reconciliation</h4>
<ul>
<li>機制：Cosmos DB 把 conflict 寫入 conflict feed、不自動解決、app 自行消費並 reconcile</li>
<li>適合：conflict 需要人工 / 業務流程判斷、不能 auto-resolve（如金融交易、合規場景）</li>
<li>風險：feed 不消費就累積、後續分析失準；app 需要實作 reconcile 流程</li>
</ul>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span> <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span> <span class="p">}</span></span></span></code></pre></div><p>（沒指 procedure、conflict 全進 feed、app 用 SDK <code>ReadConflictsAsync()</code> / Change Feed Processor pattern 消費）</p>
<h3 id="跟其他-vendor-對比">跟其他 vendor 對比</h3>
<ul>
<li><strong>DynamoDB Global Tables</strong>：也是 LWW、<em>無</em> custom merge、<em>無</em> conflict feed — 行為比 Cosmos DB 簡單但彈性少</li>
<li><strong>Spanner</strong>：用 Paxos quorum、<em>不會有 conflict</em>（CP 系統、可用性換一致性）— 跨 region write 需 quorum、latency 100-200ms</li>
<li><strong>Aurora Global Database</strong>：single-primary（一個 region 寫、其他 region 讀）、不是真 multi-region write、無 conflict</li>
</ul>
<p>對應 knowledge cards：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a>、<a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a>、<a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a>。</p>
<h2 id="操作流程">操作流程</h2>
<h3 id="開啟-multi-region-write">開啟 multi-region write</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-bash" data-lang="bash"><span class="line"><span class="ln">1</span><span class="cl">az cosmosdb update --name mycosmos --resource-group myrg <span class="se">\
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="se"></span>  --enable-multiple-write-locations <span class="nb">true</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="se"></span>  --locations <span class="nv">regionName</span><span class="o">=</span>eastus <span class="nv">failoverPriority</span><span class="o">=</span><span class="m">0</span> <span class="se">\
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="se"></span>  --locations <span class="nv">regionName</span><span class="o">=</span>westeurope <span class="nv">failoverPriority</span><span class="o">=</span><span class="m">1</span></span></span></code></pre></div><p>開啟後 <em>不能直接關回</em>、要 disable + 改 region 配置 + re-enable、有停機窗口。</p>
<h3 id="設定-lww-policycontainer-層">設定 LWW policy（container 層）</h3>
<p>建 container 時指定、可事後改但 conflict 行為以新 policy 為準（既有 conflict 不會重 resolve）。預設用 <code>_ts</code> 比較；改成 customTimestamp 時要保證 application 寫入時 <em>用單調遞增</em> 的 timestamp source（不能用 client clock）。</p>
<h3 id="設定-custom-merge">設定 custom merge</h3>
<p>建 stored proc：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-javascript" data-lang="javascript"><span class="line"><span class="ln">1</span><span class="cl"><span class="kd">function</span> <span class="nx">resolveCart</span><span class="p">(</span><span class="nx">incomingItem</span><span class="p">,</span> <span class="nx">existingItem</span><span class="p">,</span> <span class="nx">isTombstone</span><span class="p">,</span> <span class="nx">conflictingItems</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="c1">// 範例：merge 購物車 items（取 union）
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="c1"></span>  <span class="kd">var</span> <span class="nx">merged</span> <span class="o">=</span> <span class="nx">existingItem</span><span class="p">;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">  <span class="nx">merged</span><span class="p">.</span><span class="nx">items</span> <span class="o">=</span> <span class="nx">mergeArrays</span><span class="p">(</span><span class="nx">existingItem</span><span class="p">.</span><span class="nx">items</span><span class="p">,</span> <span class="nx">incomingItem</span><span class="p">.</span><span class="nx">items</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">  <span class="nx">merged</span><span class="p">.</span><span class="nx">_ts</span> <span class="o">=</span> <span class="nb">Math</span><span class="p">.</span><span class="nx">max</span><span class="p">(</span><span class="nx">existingItem</span><span class="p">.</span><span class="nx">_ts</span><span class="p">,</span> <span class="nx">incomingItem</span><span class="p">.</span><span class="nx">_ts</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">  <span class="nx">__</span><span class="p">.</span><span class="nx">response</span><span class="p">.</span><span class="nx">setBody</span><span class="p">(</span><span class="nx">merged</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl"><span class="p">}</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-json" data-lang="json"><span class="line"><span class="ln">1</span><span class="cl"><span class="s2">&#34;conflictResolutionPolicy&#34;</span><span class="err">:</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl">  <span class="nt">&#34;mode&#34;</span><span class="p">:</span> <span class="s2">&#34;Custom&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">  <span class="nt">&#34;conflictResolutionProcedure&#34;</span><span class="p">:</span> <span class="s2">&#34;dbs/mydb/colls/mycoll/sprocs/resolveCart&#34;</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>驗證：proc 內處理 timeout / exception；測 edge case（空 array / null / 並發 3+ region 寫入）。</p>
<h3 id="消費-conflict-feed">消費 conflict feed</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-csharp" data-lang="csharp"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">// .NET SDK</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="kt">var</span> <span class="n">iterator</span> <span class="p">=</span> <span class="n">container</span><span class="p">.</span><span class="n">GetItemQueryIterator</span><span class="p">&lt;</span><span class="n">ConflictProperties</span><span class="p">&gt;(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="s">&#34;SELECT * FROM c&#34;</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="k">while</span> <span class="p">(</span><span class="n">iterator</span><span class="p">.</span><span class="n">HasMoreResults</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="kt">var</span> <span class="n">response</span> <span class="p">=</span> <span class="k">await</span> <span class="n">iterator</span><span class="p">.</span><span class="n">ReadNextAsync</span><span class="p">();</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="k">foreach</span> <span class="p">(</span><span class="kt">var</span> <span class="n">conflict</span> <span class="k">in</span> <span class="n">response</span><span class="p">)</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">7</span><span class="cl">        <span class="k">await</span> <span class="n">ProcessConflict</span><span class="p">(</span><span class="n">conflict</span><span class="p">);</span>
</span></span><span class="line"><span class="ln">8</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">9</span><span class="cl"><span class="p">}</span></span></span></code></pre></div><p>用 Change Feed Processor pattern 把 conflict feed 當 stream 消費、寫到 reconcile queue、由業務流程處理。</p>
<h3 id="驗證點">驗證點</h3>
<ul>
<li>跨 region 並發寫測試（synthetic load）、觀察 conflict count / resolution result</li>
<li>Custom merge stored proc 跑過 edge case（exception / null / 並發 3+）</li>
<li>Conflict feed 不積壓（lag &lt; 5 min）</li>
<li>Region 故障時 application 仍能寫（active-active 設計、不需 manual failover）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="failure-1全用-lww--用-server-timestamp">Failure 1：全用 LWW + 用 server timestamp</h3>
<p>clock skew 在 ms 級可能讓「先寫的反而贏」、業務邏輯破洞。常見徵兆：使用者反映「我明明先按確認、後來改的反而是舊的」、debug 才發現是跨 region clock skew。</p>
<p>修：</p>
<ul>
<li>用 <code>customTimestamp</code> 從 application 端 monotonic source 取（如 Snowflake ID、HLC、Lamport clock）</li>
<li>或改用 custom merge stored proc、用業務邏輯而非 timestamp 決勝</li>
<li>或拆 collection、把 conflict 高的 collection 用 stored proc、低的用 LWW</li>
</ul>
<h3 id="failure-2業務語義不適合-lww">Failure 2：業務語義不適合 LWW</h3>
<p>購物車（要 union）、計數器（要 sum）、status 機器（要狀態圖）全用 LWW = <em>資料丟失</em>。LWW 的設計假設是「最新 write 就是正確答案」、但很多業務語義不是覆寫關係。</p>
<p>修：盤點 collection 的業務語義、選對應 resolution policy：</p>
<ul>
<li>覆寫關係 → LWW</li>
<li>累積關係 → custom merge stored proc（union / sum / set 合併）</li>
<li>狀態機 → custom merge stored proc（按狀態圖規則 resolve）</li>
<li>需要人工裁決 → conflict feed</li>
</ul>
<h3 id="failure-3custom-merge-stored-proc-沒測-edge-case">Failure 3：Custom merge stored proc 沒測 edge case</h3>
<p>proc throw exception 時 Cosmos DB 行為：conflict 留 feed、不會自動 retry。團隊以為 proc 跑了就沒事、實際 conflict 累積在 feed、後續分析失準。</p>
<p>修：proc 內部 try-catch、log exception、確保 <em>任何輸入都能 return 一個合理結果</em>（即使是 fallback 到 LWW）；定期掃 conflict feed 檢查積壓。</p>
<h3 id="failure-4不消費-conflict-feed">Failure 4：不消費 conflict feed</h3>
<p>選 manual mode 後忘記實作 feed consumer、conflict 累積、後續分析失準。常見徵兆：feed lag metric alert、或業務反映「資料對不上」、最後發現 conflict feed 裡躺著一堆未處理的 conflict。</p>
<p>修：選 conflict feed mode 前先實作 consumer pipeline（Azure Function trigger on Change Feed / 自建 worker）；設 alert：feed lag &gt; 5 min 通知。</p>
<h3 id="failure-5期待-multi-region-write-還有-strong-consistency">Failure 5：期待 multi-region write 還有 Strong consistency</h3>
<p>兩者互斥、開啟 multi-region write 後 Strong 自動 downgrade（或拒絕設定、時間敏感、查最新文件）。團隊以為「multi-region + Strong = 全球 linearizable」、底層是設計 incompatibility。</p>
<p>修：在 selection 階段就決定「要 active-active write 還是要 Strong」 — 兩者只能擇一。要全球 linearizable 轉 Spanner / Aurora DSQL、要 active-active 就接受 eventual / session / bounded staleness。</p>
<h3 id="failure-6跨-region-寫入後立即同-session-read-看不到">Failure 6：跨 region 寫入後立即同 session read 看不到</h3>
<p>session token 沒跨 region 傳遞、看似 inconsistency 其實是 session 沒對齊。典型 anti-pattern：service A 在 region 1 寫、用 region 1 session token；service B 在 region 2 讀、沒拿到 A 的 token、看不到 A 的寫。</p>
<p>修：session token 隨 request 傳遞（通常進 HTTP header）；或改 account 層 Bounded staleness（提供跨 session 的 K/T bound）；見 <a href="../consistency-levels-engineering/">consistency-levels-engineering</a> 的 session token 管理段。</p>
<h3 id="failure-7region-故障時的-failover-邏輯誤判">Failure 7：Region 故障時的 failover 邏輯誤判</h3>
<p>multi-region write 已是 active-active、<em>不需要 manual failover</em> — 一個 region 掛、其他 region 自動承接寫入。但若用了 <code>failoverPriority</code> 配置、failover 邏輯仍要審 — priority 是 <em>當 multi-region read 切到哪個 region 為 primary</em>、不是 active-active 的 routing。</p>
<p>修：multi-region write 場景不用依賴 failoverPriority、用 Traffic Manager / Front Door 做 region routing；application 端 SDK 配置 <code>PreferredLocations</code> 讓 SDK 自己選 nearest region。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<ul>
<li>必看 metric：<code>ConflictCount</code>、<code>ReplicationLatency</code> per region pair、conflict feed lag</li>
<li>Conflict rate 監控：正常 &lt; 0.01%、突增代表 hot key 或 region 同步異常</li>
<li>Cost 影響：multi-region write 開啟後、寫入成本 × region 數（每個 region 都 replicate）— 3 region active-active = 3x write <a href="/blog/backend/knowledge-cards/request-unit/" data-link-title="Request Unit" data-link-desc="Cosmos DB 的容量抽象單位、1 RU = 1KB document strong-consistent read 的 CPU &#43; memory &#43; IOPS 綜合 cost、寫 ~5 RU、複雜 query 數百 RU">Request Unit</a> cost</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/capacity-planning/" data-link-title="9.6 容量規劃模型" data-link-desc="peak forecast、headroom budget、growth curve、autoscaling sizing">9.6 容量規劃模型</a>：multi-region write multiplier 進 sizing</li>
<li>對應 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">4.20 Observability Evidence Package</a>：conflict rate 當 reliability evidence</li>
<li>Alert：conflict rate &gt; 0.1%、conflict feed lag &gt; 5 min、cross-region replication lag &gt; SLA</li>
</ul>
<h3 id="廣告-sla-vs-實測可用性鏈路拆解本章合成-frame">廣告 SLA vs 實測可用性鏈路拆解（本章合成 frame）</h3>
<p>9.C11 Minecraft Earth 平台揭露的 Cosmos DB SLA：</p>
<ul>
<li>single-region 99.99%</li>
<li>multi-region 99.999%</li>
</ul>
<p>這是 <em>DB 端 SLA</em>、不是 <em>端到端系統 SLA</em>。真實 production 系統的可用性是鏈路乘積：</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">實測可用性 = DB SLA × 網路 SLA × 應用層 SLA × 客戶端可達性</span></span></code></pre></div><p><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected</a> 揭露「99.99% target vs 99% 實測」段的觀察：兩個 9 的差距 <em>不是</em> MongoDB / Atlas 自身問題、是 end-to-end 鏈路（車輛無線網路 / cellular tower / cloud network / event bus / microservice / DB cluster 任一環節掉都會打掉可用性）。Cosmos DB multi-region write 同模型：</p>
<ul>
<li>多 region active-active 可解 <em>DB 端可用性</em>、但網路 / 應用層任一掉、實測仍 &lt; 99.99%</li>
<li>廣告 99.999% 是 multi-region availability zone 級、<em>不是</em> 「使用者 request 成功率」</li>
</ul>
<p>引用時必須明示：Cosmos DB multi-region 廣告 99.999% 是 DB 端、要算實測可用性必須補網路 / 應用層 SLA 乘積、Toyota case 的「99% 實測」揭露的就是這個鏈路問題、跨 vendor 都適用。</p>
<p>跟 conflict resolution 的關係：多 region 高可用性 <em>買來</em> 的代價是 conflict、conflict rate 是 reliability 的暗稅 — 廣告 SLA 不計 conflict 處理成本。production 設計要把「conflict resolution 的工程成本」加進 multi-region write 的 ROI 評估。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<ul>
<li>Sibling deep articles：<a href="../consistency-levels-engineering/">consistency-levels-engineering</a>（multi-region write 跟 Strong 互斥的 cross-link 來源）、<a href="../partition-key-design/">partition-key-design</a>（hot partition 會放大 conflict）、<a href="../ru-cost-model-sizing/">ru-cost-model-sizing</a>（multi-region cost × region 數）</li>
<li>跟 <a href="/blog/backend/01-database/vendors/spanner/" data-link-title="Google Cloud Spanner" data-link-desc="全球分散式 strong-consistency OLTP、TrueTime API、線性擴展到 10 億 req/sec">Spanner vendor</a> 對比：CP vs AP、無 conflict vs LWW / custom</li>
<li>跟 DynamoDB Global Tables 對比：兩者都 LWW、Cosmos DB 多 custom merge + conflict feed</li>
<li>跟 1.x 章節：<a href="/blog/backend/01-database/global-distributed-oltp/" data-link-title="1.11 全球分散式 OLTP" data-link-desc="Spanner / Aurora DSQL / Cosmos DB multi-region write / CockroachDB / TiDB 的全球一致性取捨">1.11 全球分散式 OLTP</a> 把 multi-region write 模式並陳</li>
<li>Knowledge cards：<a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">stale-read</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">rpo</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">rto</a></li>
<li>Anti-recommendation：single-region write + cross-region read replica 在大多數情況更便宜、更易推理；只有 <em>write residency</em> 是產品契約（合規 / latency / 業務需求）時才升 multi-region write</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cosmosdb/" data-link-title="Azure Cosmos DB" data-link-desc="全球分散式 multi-model DB、5 個 consistency levels、Microsoft 自家 dogfood 證據">Cosmos DB vendor overview</a> — 本文是該頁尾 multi-region write + conflict resolution backlog 的深度展開</li>
<li><a href="/blog/backend/09-performance-capacity/cases/minecraft-earth-cosmos-db-global/" data-link-title="9.C11 Minecraft Earth：Azure Cosmos DB 上的全球分散式 AR 遊戲" data-link-desc="Minecraft Earth 用 Cosmos DB 跨地區分散、測試到 100 萬 RU/s 仍維持承諾延遲">9.C11 Minecraft Earth case</a> — multi-region 99.999% / single-region 99.99% SLA 來源</li>
<li><a href="/blog/backend/09-performance-capacity/cases/asos-cosmos-db-black-friday/" data-link-title="9.C21 ASOS：Cosmos DB 在 Black Friday 撐 1.67 億請求" data-link-desc="ASOS 在 2016 Black Friday 用 Azure Cosmos DB 撐 24 小時 1.67 億請求、3500 req/sec、48ms 平均延遲">9.C21 ASOS case</a> — 全球零售 multi-region 補充</li>
<li><a href="/blog/backend/09-performance-capacity/cases/toyota-connected-mongodb-telematics-iot/" data-link-title="9.C38 Toyota Connected：MongoDB Atlas 撐 900 萬車輛 telematics、月 180 億 transaction" data-link-desc="Toyota Connected 用 MongoDB Atlas 撐 Safety Connect 900 萬車、月 180 億 transaction、緊急訊號 3 秒內到 agent">9.C38 Toyota Connected case</a> — 鏈路 SLA 拆解 frame anchor（跨 vendor 適用）</li>
<li><a href="../consistency-levels-engineering/">consistency-levels-engineering</a> — Strong + multi-region 互斥的 cross-link 目的地</li>
<li><a href="/blog/backend/knowledge-cards/stale-read/" data-link-title="Stale Read" data-link-desc="讀取到落後於最新寫入版本的舊資料">Stale Read 卡片</a> / <a href="/blog/backend/knowledge-cards/rpo/" data-link-title="RPO" data-link-desc="說明恢復點目標如何定義可接受資料損失範圍">RPO 卡片</a> / <a href="/blog/backend/knowledge-cards/rto/" data-link-title="RTO" data-link-desc="說明恢復時間目標如何約束事故回復策略">RTO 卡片</a> — 概念基底</li>
<li>官方：<a href="https://learn.microsoft.com/azure/cosmos-db/conflict-resolution-policies">Cosmos DB conflict resolution</a> / <a href="https://learn.microsoft.com/azure/cosmos-db/how-to-multi-master">Multi-region writes</a></li>
</ul>
]]></content:encoded></item><item><title>並行 AI Agent 修改同一檔案的衝突模式與協調策略</title><link>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</link><pubDate>Thu, 25 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/work-log/%E4%B8%A6%E8%A1%8C-ai-agent-%E4%BF%AE%E6%94%B9%E5%90%8C%E4%B8%80%E6%AA%94%E6%A1%88%E7%9A%84%E8%A1%9D%E7%AA%81%E6%A8%A1%E5%BC%8F%E8%88%87%E5%8D%94%E8%AA%BF%E7%AD%96%E7%95%A5/</guid><description>&lt;h2 id="事件">事件&lt;/h2>
&lt;p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。&lt;/p>
&lt;p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 &lt;code>monitor.ts&lt;/code>。&lt;/p>
&lt;p>結果：&lt;/p>
&lt;ul>
&lt;li>三個 agent 回報 branch protection hook 阻擋 src 編輯&lt;/li>
&lt;li>兩個 agent 回報 &lt;code>file modified since read&lt;/code> 拒絕 Edit（另一個 agent 正在寫同一檔案）&lt;/li>
&lt;li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」&lt;/li>
&lt;li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit&lt;/li>
&lt;/ul>
&lt;p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。&lt;/p>
&lt;h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層&lt;/h2>
&lt;p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 &lt;code>monitor.ts&lt;/code> 這一個檔案上。&lt;/p>
&lt;p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。&lt;/p>
&lt;h2 id="教訓">教訓&lt;/h2>
&lt;p>&lt;strong>派發前掃描 &lt;code>where.files&lt;/code>&lt;/strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。&lt;/p>
&lt;p>&lt;strong>序列的代價比衝突的代價低&lt;/strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。&lt;/p>
&lt;p>&lt;strong>Worktree 隔離不是萬靈丹&lt;/strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。&lt;/p>
&lt;h2 id="適用場景">適用場景&lt;/h2>
&lt;p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。&lt;/p></description><content:encoded><![CDATA[<h2 id="事件">事件</h2>
<p>多人（或多 agent）並行開發時，如果修改集中在同一個檔案，協調成本可能抵消並行的收益。以下是一個具體案例。</p>
<p>v0.3.0 的 JS SDK 開發中，五張 ticket 被並行派發給五個 AI agent：flush 邏輯、離線容錯、自動攔截、頁面生命週期、rate limiting。前四個都需要修改同一個檔案 <code>monitor.ts</code>。</p>
<p>結果：</p>
<ul>
<li>三個 agent 回報 branch protection hook 阻擋 src 編輯</li>
<li>兩個 agent 回報 <code>file modified since read</code> 拒絕 Edit（另一個 agent 正在寫同一檔案）</li>
<li>PM 花了多個回合協調 commit 策略：「你先 commit」「你等他完成」「你只 git add 你的檔案」</li>
<li>最終 PM 手動合併所有 agent 的變更，做了一個統一 commit</li>
</ul>
<p>並行派發的目標是縮短總工時。但五個 agent 改同一檔案時，協調成本抵消了並行的收益。</p>
<h2 id="根因派發粒度錯在-ticket-層而非檔案層">根因：派發粒度錯在 ticket 層而非檔案層</h2>
<p>派發決策看的是 ticket 的獨立性——五張 ticket 描述的功能確實獨立（flush、離線、攔截、生命週期各自有清楚的邊界）。但獨立的功能不等於獨立的檔案。五個功能的修改都集中在 <code>monitor.ts</code> 這一個檔案上。</p>
<p>ticket 獨立 =/= 檔案獨立。並行安全的判斷基準應該是後者。</p>
<h2 id="教訓">教訓</h2>
<p><strong>派發前掃描 <code>where.files</code></strong>：如果多張 ticket 的目標檔案有交集，序列化派發。前一張完成並 commit 後，再派下一張。</p>
<p><strong>序列的代價比衝突的代價低</strong>：五個 agent 序列執行可能需要 5 倍時間，但每個 agent 在乾淨的工作區上操作，不需要協調。五個 agent 並行但衝突，PM 的協調時間加上 agent 的等待和重試，總成本可能更高。</p>
<p><strong>Worktree 隔離不是萬靈丹</strong>：git worktree 讓每個 agent 有獨立的工作目錄，避免 working tree 衝突。但如果兩個 agent 修改同一檔案的不同區段，merge 時仍需人工判斷。Worktree 解決的是「同時寫同一個 working tree」的問題，不解決「同時改同一個檔案的語意衝突」。</p>
<h2 id="適用場景">適用場景</h2>
<p>這個 pattern 不限於 AI agent。人類開發者在同一個 Sprint 中被分配修改同一個檔案的不同功能時，也會遇到 merge conflict。差異在於人類可以口頭協調（「我先改完你再改」），agent 目前缺乏這個即時溝通管道。派發者（PM 或 CI 系統）需要在派發時就做好檔案衝突預判。</p>
]]></content:encoded></item></channel></rss>