<?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>Transaction on Tarragon</title><link>https://tarrragon.github.io/blog/tags/transaction/</link><description>Recent content in Transaction on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Tue, 02 Jun 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/transaction/index.xml" rel="self" type="application/rss+xml"/><item><title>1.3 Transaction 與一致性邊界</title><link>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/transaction-boundary/</guid><description>&lt;p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。&lt;/p>
&lt;p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。&lt;/p>
&lt;h2 id="邊界先於語法">邊界先於語法&lt;/h2>
&lt;p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。&lt;/p>
&lt;p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。&lt;/p>
&lt;h2 id="isolation-level-五級深度">Isolation Level 五級深度&lt;/h2>
&lt;p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 &lt;em>正確性 vs 性能&lt;/em> 之間做取捨。&lt;/p>
&lt;p>&lt;strong>0. Read Uncommitted（dirty read 可能）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>可讀到別的 transaction 還沒 commit 的資料&lt;/li>
&lt;li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）&lt;/li>
&lt;li>實務不要用&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>1. Read Committed（PostgreSQL / Oracle 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>只讀到 commit 的資料&lt;/li>
&lt;li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）&lt;/li>
&lt;li>適合：read-heavy workload、不要求同 transaction 內 read consistency&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>2. Repeatable Read（MySQL InnoDB 預設）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>同 transaction 內 read 一致（snapshot at transaction start）&lt;/li>
&lt;li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了&lt;/li>
&lt;li>適合：報表類 transaction、需要 snapshot 一致性&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>3. Serializable（最強）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>看起來像所有 transaction 序列執行&lt;/li>
&lt;li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）&lt;/li>
&lt;li>衝突時會 serialization failure、應用層必須 retry&lt;/li>
&lt;li>適合：金融交易、ticketing inventory、需要絕對正確&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致&lt;/li>
&lt;li>全球分散式系統的特殊取捨&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/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&lt;/a> 的 Spanner TrueTime 段&lt;/li>
&lt;li>詳見 &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case&lt;/a>&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>選擇原則&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>90% 業務用 Read Committed 夠&lt;/li>
&lt;li>報表 / 對帳用 Repeatable Read&lt;/li>
&lt;li>金融交易 / inventory 用 Serializable&lt;/li>
&lt;li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統&lt;/li>
&lt;/ul>
&lt;h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係&lt;/h2>
&lt;p>&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level&lt;/a> 的責任是定義交易彼此可見性。&lt;code>Read Committed&lt;/code> 在高併發寫入下可維持一般業務一致性；&lt;code>Repeatable Read&lt;/code> 與 &lt;code>Serializable&lt;/code> 提供更強約束、同時提高鎖競爭與重試頻率。&lt;/p></description><content:encoded><![CDATA[<p>交易邊界（transaction boundary）的核心責任是定義哪些資料變更必須一起成立。資料庫交易的價值在於讓同一個業務動作可以被明確提交、明確回退、明確重試。</p>
<p>本章從業務邊界切分開始、進入 isolation level 工程細節、再到 retry 策略、最後處理跨服務 / 跨 region 的 distributed transaction。讀完後讀者能回答：transaction 範圍該多大、isolation 該訂多嚴、deadlock 怎麼處理、跨服務一致性怎麼設計、什麼時候該換 Saga 模式。</p>
<h2 id="邊界先於語法">邊界先於語法</h2>
<p>交易邊界先從業務動作切分、再回到 SQL。建立訂單、扣庫存、寫付款狀態是一個動作；更新推薦分數、寫審計摘要、送通知事件屬於不同節奏、適合拆成後續流程。</p>
<p>當同一個動作內同時包含高延遲外部呼叫、交易範圍會直接放大鎖持有時間。穩定做法是把交易內責任收斂在「需要同時成功」的資料集合、讓外部呼叫或延伸副作用透過 queue / outbox 交給後續流程。</p>
<h2 id="isolation-level-五級深度">Isolation Level 五級深度</h2>
<p>SQL 標準定義四個 isolation level、實務上 PostgreSQL / MySQL / Spanner 等實作有微妙差異。理解各級的具體行為、才能在 <em>正確性 vs 性能</em> 之間做取捨。</p>
<p><strong>0. Read Uncommitted（dirty read 可能）</strong>：</p>
<ul>
<li>可讀到別的 transaction 還沒 commit 的資料</li>
<li>多數 DB 不真的支援這級（會 fallback 到 Read Committed）</li>
<li>實務不要用</li>
</ul>
<p><strong>1. Read Committed（PostgreSQL / Oracle 預設）</strong>：</p>
<ul>
<li>只讀到 commit 的資料</li>
<li>同一個 transaction 內、多次 SELECT 同一筆資料可能讀到不同值（non-repeatable read）</li>
<li>適合：read-heavy workload、不要求同 transaction 內 read consistency</li>
</ul>
<p><strong>2. Repeatable Read（MySQL InnoDB 預設）</strong>：</p>
<ul>
<li>同 transaction 內 read 一致（snapshot at transaction start）</li>
<li>不防 phantom read（標準定義）、但 InnoDB 的 RR 加 gap lock 實際上防住了</li>
<li>適合：報表類 transaction、需要 snapshot 一致性</li>
</ul>
<p><strong>3. Serializable（最強）</strong>：</p>
<ul>
<li>看起來像所有 transaction 序列執行</li>
<li>兩種實作：strict 2PL（lock-based、MySQL）vs SSI（snapshot isolation + 衝突檢測、PostgreSQL）</li>
<li>衝突時會 serialization failure、應用層必須 retry</li>
<li>適合：金融交易、ticketing inventory、需要絕對正確</li>
</ul>
<p><strong>4. External Consistency / Linearizable（Spanner、Aurora DSQL）</strong>：</p>
<ul>
<li>比 Serializable 更強：跨 transaction 的順序跟 wall clock 一致</li>
<li>全球分散式系統的特殊取捨</li>
<li>詳見 <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> 的 Spanner TrueTime 段</li>
<li>詳見 <a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner case</a></li>
</ul>
<p><strong>選擇原則</strong>：</p>
<ul>
<li>90% 業務用 Read Committed 夠</li>
<li>報表 / 對帳用 Repeatable Read</li>
<li>金融交易 / inventory 用 Serializable</li>
<li>全球強一致用 Spanner / Aurora DSQL 等 linearizable 系統</li>
</ul>
<h2 id="isolation-跟-retry-的關係">Isolation 跟 Retry 的關係</h2>
<p><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a> 的責任是定義交易彼此可見性。<code>Read Committed</code> 在高併發寫入下可維持一般業務一致性；<code>Repeatable Read</code> 與 <code>Serializable</code> 提供更強約束、同時提高鎖競爭與重試頻率。</p>
<p>併發交易的常見結果是 deadlock 或 serialization failure。這些結果代表資料庫在保護一致性、應用層需要把它視為可重試路徑：</p>
<ul>
<li><strong>重試次數有上限</strong>（通常 3-5 次）— 避免 retry storm</li>
<li><strong>重試間隔有抖動</strong>（exponential backoff + jitter）— 避免同步衝突</li>
<li><strong>重試前提是動作可重入</strong>（idempotent）— 不會放大副作用</li>
</ul>
<p>對應 <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a> 跟 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> 卡片。</p>
<h2 id="optimistic-vs-pessimistic-locking">Optimistic vs Pessimistic Locking</h2>
<p>當多個 transaction 同時操作同一筆資料、有兩種防衝突策略：</p>
<p><strong>Pessimistic locking（悲觀鎖）</strong>：</p>
<ul>
<li><code>SELECT ... FOR UPDATE</code>、提前 lock 行</li>
<li>適合：衝突機率高、retry 成本高</li>
<li>缺點：lock 期間其他 transaction 等待、容易 deadlock</li>
</ul>
<p><strong>Optimistic locking（樂觀鎖）</strong>：</p>
<ul>
<li>不 lock、用 version column 或 <code>WHERE old_value = ?</code></li>
<li>commit 時若 version 不對、整個 transaction 失敗、應用層 retry</li>
<li>適合：衝突機率低、性能優先</li>
<li>缺點：高衝突場景 retry 多、整體吞吐反而低</li>
</ul>
<p><strong>選擇邏輯</strong>：</p>
<ul>
<li>衝突 &lt; 5% → optimistic（更高吞吐）</li>
<li>衝突 &gt; 30% → pessimistic（避免 retry waste）</li>
<li>中間區 → 量測再決定</li>
</ul>
<p>對應 <a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">hot row contention 處理</a>（<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1</a>）— 高衝突 hot row 通常該換 KV / cache、不該硬擴 SQL。</p>
<h2 id="服務情境checkout-多層邊界">服務情境：Checkout 多層邊界</h2>
<p>電商 checkout 是典型的 transaction boundary 設計題、可拆成兩層邊界。</p>
<p><strong>第一層：交易層（即時一致）</strong>：</p>
<ul>
<li>建立訂單主表</li>
<li>寫入訂單項目</li>
<li>扣減可售庫存</li>
<li>寫入付款待確認狀態</li>
</ul>
<p><strong>第二層：延伸層（最終可達）</strong>：</p>
<ul>
<li>寄訂單確認 email</li>
<li>同步 CRM 系統</li>
<li>觸發 analytics event</li>
<li>更新推薦模型</li>
</ul>
<p>這種切法讓交易控制面跟非同步控制面各自穩定：</p>
<ul>
<li>交易層關注 <em>鎖、隔離與回退</em></li>
<li>非同步層關注 <em>投遞、重試與補償</em></li>
</ul>
<p>對應案例：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a> — 體育博彩 ledger、200 個獨立 cluster 處理 transaction、後續 settlement 跑非同步</li>
<li><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a> — 跨市場銀行 transaction、各市場獨立、跨市場結算非同步</li>
</ul>
<h2 id="distributed-transaction2pc-vs-saga">Distributed Transaction：2PC vs Saga</h2>
<p>當業務動作跨越 <em>多個服務 / 資料庫</em>、傳統 ACID transaction 不夠用、需要 distributed transaction 模式。</p>
<p><strong>Two-Phase Commit (2PC)</strong>：</p>
<ul>
<li>階段 1：coordinator 詢問所有 participant「你能 commit 嗎？」</li>
<li>階段 2：所有都說 yes → coordinator 廣播 commit；任一說 no → 廣播 abort</li>
<li><strong>優點</strong>：強一致、ACID 保證</li>
<li><strong>缺點</strong>：coordinator failure 會 block 所有 participant、性能差、跨服務複雜</li>
<li>適合：少數高一致性需求的場景（金融交易、跨多 DB 一致性）</li>
</ul>
<p><strong>Saga Pattern</strong>：</p>
<ul>
<li>把長 transaction 拆成多個 local transaction + compensating transaction</li>
<li>每個 step 成功 → 進下個；任一失敗 → 倒回去跑 compensation</li>
<li>例：訂單 step1 扣庫存、step2 收款、step3 送貨。step2 失敗 → 跑 step1 的 compensation（補庫存）</li>
<li><strong>優點</strong>：高可用、性能好、容易擴展</li>
<li><strong>缺點</strong>：不是強一致、中間狀態可見、compensation 必須設計</li>
<li>適合：multi-service 業務流程、可接受 <a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
</ul>
<p><strong>Choreography vs Orchestration</strong>：</p>
<ul>
<li>Choreography：每個 service 自己決定下一步（event-driven）</li>
<li>Orchestration：中央 orchestrator 控制流程（state machine）</li>
<li>大規模傾向 orchestration（容易追蹤、debug）、小規模 choreography 足夠</li>
</ul>
<p><strong>對應案例</strong>：</p>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a> — 售票 + 付款分開：DynamoDB 接搶單（local transaction）、legacy server 跑付款（compensation 處理庫存回退）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/fanduel-dual-peak-betting-streaming/" data-link-title="9.C28 FanDuel：體育直播 &#43; 投注的雙重峰值" data-link-desc="FanDuel 3.5M MAU、Super Bowl 期間擴容 5-10 倍、用 AWS Local Zones &#43; Wavelength &#43; Outposts 處理 20&#43; 州的雙重峰值">9.C28 FanDuel</a> — 投注 → 結算的 saga 流程</li>
</ul>
<p>詳見 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern 卡片</a> 跟 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 Outbox Pattern</a>。</p>
<h2 id="跨-region-transactioncap-取捨">跨 Region Transaction：<a href="/blog/backend/knowledge-cards/cap/" data-link-title="CAP Theorem" data-link-desc="分散式系統在網路分區時一致性與可用性的取捨框架">CAP</a> 取捨</h2>
<p>當 transaction 必須跨 region 同時成立、CAP 定理開始作用。</p>
<p><strong>Single-region transaction</strong>（PostgreSQL / MySQL / Aurora）：</p>
<ul>
<li>ACID within region</li>
<li>跨 region 用 async replication、不是 transaction</li>
</ul>
<p><strong>Multi-region eventual consistency</strong>（DynamoDB Global Tables、Cosmos DB session/eventual）：</p>
<ul>
<li>各 region 都能寫</li>
<li>LWW 或 application-level conflict resolution</li>
<li>不是 ACID、是 BASE</li>
</ul>
<p><strong>Multi-region strong consistency</strong>（Spanner、Aurora DSQL、CockroachDB）：</p>
<ul>
<li>跨 region linearizable transaction</li>
<li>代價是 latency（跨洲 100-200ms <a href="/blog/backend/knowledge-cards/quorum/" data-link-title="Quorum" data-link-desc="分散式系統以多數節點同意作為提交或讀取有效性的門檻">quorum</a>）</li>
<li>對應 <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></li>
</ul>
<p><strong>決策邏輯</strong>：</p>
<ul>
<li>業務不需要跨 region 強一致 → single-region OLTP + eventual replication</li>
<li>需要跨 region 強一致 + 接受 latency → Spanner / Aurora DSQL</li>
<li>需要跨 region 寫但接受最終一致 → Cosmos DB session / DynamoDB Global Tables</li>
</ul>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>deadlock rate 升高</td>
          <td>交易範圍過大或鎖順序不一致</td>
          <td>統一更新順序、縮小 transaction 範圍</td>
      </tr>
      <tr>
          <td>transaction duration 在尖峰時段上升</td>
          <td>交易內含慢查詢或外部依賴</td>
          <td>將外部呼叫移出交易、補索引與查詢計畫</td>
      </tr>
      <tr>
          <td>retry 成功率下降</td>
          <td>重試條件與業務冪等假設不一致</td>
          <td>補 idempotency key、調整 retry 邏輯</td>
      </tr>
      <tr>
          <td>rollback 後仍出現業務狀態殘留</td>
          <td>邊界切分和副作用落點未對齊</td>
          <td>將副作用統一移到 outbox / consumer 路徑</td>
      </tr>
      <tr>
          <td>交易內讀寫跨多資料域導致 contention 爆發</td>
          <td>業務聚合邊界與資料模型邊界衝突</td>
          <td>重新切 aggregate 與拆分熱點資料結構</td>
      </tr>
      <tr>
          <td>Serializable retry 率 &gt; 10%</td>
          <td>isolation 太嚴或業務衝突高</td>
          <td>降到 Repeatable Read 或拆 hot row</td>
      </tr>
      <tr>
          <td>跨服務 transaction 用 2PC 卡住</td>
          <td>coordinator failure 阻塞</td>
          <td>改 Saga + compensation</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>交易保護的是一致性、不是吞吐量最大化。把過多步驟包進單一交易、會同時放大鎖競爭與回退成本。把交易切成可驗證的業務單位、能讓高併發下的可預期性更高。</p>
<p>重試保護的是暫時性失敗、不是所有失敗。沒有冪等保護的重試會放大副作用、特別是金流、庫存、配額這類正式狀態。</p>
<p>isolation level 不是「越強越好」。Serializable 比 Read Committed 慢數倍、且 retry rate 上升。只在 <em>必要</em> 場景用最強 isolation、其他場景用最低可接受 isolation。</p>
<p>distributed transaction 不是「跨服務就要 2PC」。多數 multi-service 業務用 Saga 更可靠、2PC 是少數場景的特殊工具。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>Transaction 相關重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora</a></td>
          <td>Aurora MySQL ACID transaction、200 個獨立 cluster 隔離 transaction scope</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/spanner-planetary-scale-database-gcp/" data-link-title="9.C10 Cloud Spanner：每秒 10 億請求的全球一致性資料庫" data-link-desc="Google Cloud Spanner 內部峰值 10 億 req/sec、跨地區強一致 — 全球分散式 OLTP 容量參考">9.C10 Spanner</a></td>
          <td>External consistency（linearizable）跨 region transaction、TrueTime</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/standard-chartered-aurora-banking/" data-link-title="9.C14 Standard Chartered：受監管銀行的 Aurora 4000 TPS 容量提升" data-link-desc="Standard Chartered 銀行遷移到 Aurora 後吞吐量提升 10 倍至 4000 TPS、跨 7 個受監管市場">9.C14 Standard Chartered</a></td>
          <td>跨市場 transaction 各市場獨立 cluster、合規限制</td>
      </tr>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/tixcraft-ticketing-flash-sale-spike/" data-link-title="9.C15 拓元 Tixcraft：售票搶購的瞬間爆量架構" data-link-desc="拓元用 DynamoDB 當寫入緩衝 &#43; 傳統伺服器當慢速消費者、承受 100K&#43; 同時選位 &#43; 30 秒從 6 台擴到 800 台">9.C15 Tixcraft</a></td>
          <td>搶票 + 付款 saga 模式、DynamoDB queue + legacy SQL</td>
      </tr>
  </tbody>
</table>
<h2 id="案例回寫">案例回寫</h2>
<p>交易邊界可用 <a href="/blog/backend/08-incident-response/cases/github/2018-oct21-mysql-topology-incident/" data-link-title="GitHub 2018 Oct21 MySQL Topology Incident" data-link-desc="2018-10-21 GitHub 因 network partition 觸發跨區資料庫拓撲異常的事故解析：資料一致性優先、fail-forward 決策與長時間恢復。">GitHub 2018 Oct21 MySQL Topology Incident</a> 做回寫。先看事件中的主從切換與恢復順序、再回到本章判讀三件事：哪些變更必須同交易成功、哪些副作用應拆到 outbox、哪些錯誤屬於可重試而非立即回退。</p>
<p>這個案例主要支撐的是「提交與副作用切分」判讀、不直接支撐 schema naming 或 cache freshness；若問題落在資料命名或快取新鮮度、應回到 1.2 或 2.x。</p>
<p>若事件出現資料已寫入但外部流程落後、或重試後副作用重複、先收斂本章的邊界切分與重試前提、再同步更新 <a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> 與 <a href="/blog/backend/03-message-queue/consumer-design/" data-link-title="3.4 consumer 設計與去重" data-link-desc="整理 consumer、checkpoint 與 replay safety">3.4 consumer 設計</a>。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<p>交易邊界設計會直接影響後續模組的可操作性。</p>
<ol>
<li>與 03 的交接：交易外副作用透過 <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">outbox pattern</a> 與 consumer 落地。</li>
<li>與 1.7 的交接：付款狀態拆欄位、雙寫與回呼更新要進入 <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">Schema Migration Rollout 證據</a> 的驗證流程。</li>
<li>與 1.10 / 1.11 的交接：KV 跟全球分散式 OLTP 的 transaction model 不同、選型時要回到本章邊界判讀。</li>
<li>與 04 的交接：交易失敗需要對齊 <a href="/blog/backend/04-observability/observability-evidence-package/" data-link-title="4.20 Observability Evidence Package" data-link-desc="把 log、metric、trace、audit 與資料品質限制包成可交接證據">Observability Evidence Package</a> 的查詢與證據欄位。</li>
<li>與 06 的交接：高風險交易變更納入 <a href="/blog/backend/06-reliability/release-gate/" data-link-title="6.8 Release Gate 與變更節奏" data-link-desc="把驗證、migration、相容性納入放行判準">Release Gate</a> 與 <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">Migration Safety</a>。</li>
<li>與 08 的交接：交易層回退或 fail-forward 判斷記錄到 <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>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<ul>
<li>平行：<a href="/blog/backend/01-database/high-concurrency-access/" data-link-title="1.1 高併發下的 SQL 讀寫邊界" data-link-desc="說明高併發服務如何共用資料庫 client、控制 transaction、管理 connection pool、避免資料庫成為瓶頸">1.1 高併發資料存取</a>（connection pool / hot row）</li>
<li>下游：<a href="/blog/backend/01-database/database-migration-playbook/" data-link-title="1.6 資料庫轉換實作：雙寫、回填、切流與回滾" data-link-desc="同 DB 內 schema 演進與資料變更的可分段驗證流程、跟 1.12 cross-DB migration 分工">1.6 資料庫轉換實作</a> / <a href="/blog/backend/01-database/schema-migration-rollout-evidence/" data-link-title="1.7 Schema Migration Rollout 證據（Schema Migration Rollout Evidence）實作示範" data-link-desc="以訂單付款狀態欄位演進示範 schema migration 如何產出 evidence、release gate 與 incident decision log。">1.7 Schema Migration Rollout 證據</a> / <a href="/blog/backend/01-database/kv-document-capacity-planning/" data-link-title="1.10 KV / Document DB 容量規劃" data-link-desc="DynamoDB / Cosmos DB / Bigtable / MongoDB 等 KV / Document DB 的容量設計、partition key 取捨、capacity mode 選擇">1.10 KV / Document DB 容量規劃</a> / <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></li>
<li>跨模組：<a href="/blog/backend/03-message-queue/outbox-pattern/" data-link-title="3.3 outbox pattern 與發佈一致性" data-link-desc="把 transaction 與 event publish 分離">3.3 outbox pattern</a> / <a href="/blog/backend/06-reliability/migration-safety/" data-link-title="6.11 Migration Safety 與 DB Rollout" data-link-desc="把 schema migration 從一次性事件變成可逆、可漸進的 rollout 流程">6.11 Migration Safety</a> / <a href="/blog/backend/09-performance-capacity/bottleneck-localization/" data-link-title="9.5 瓶頸定位流程" data-link-desc="從 app 到 DB / cache / broker / 第三方 quota 的逐層瓶頸定位">9.5 瓶頸定位流程</a></li>
<li>卡片：<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">Isolation Level</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">Transaction Boundary</a> / <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency</a> / <a href="/blog/backend/knowledge-cards/outbox-pattern/" data-link-title="Outbox Pattern" data-link-desc="說明資料庫狀態變更與事件發布如何透過 outbox 維持一致">Outbox Pattern</a> / <a href="/blog/backend/knowledge-cards/exponential-backoff/" data-link-title="Exponential Backoff" data-link-desc="說明重試間隔如何逐步拉長以降低下游壓力">Exponential Backoff</a></li>
<li>Spanner 一致性深入：<a href="/blog/backend/01-database/vendors/spanner/truetime-api-depth/" data-link-title="Spanner TrueTime API 深度：GPS &#43; 原子鐘、commit wait、為什麼 line-rate scaling 才是設計目的" data-link-desc="TrueTime 是手段、line-rate scaling 才是 Spanner 的設計目的。本文先扣商業邏輯：傳統 OLTP coordinator 為什麼是 bottleneck、Spanner 怎麼用 TrueTime &#43; Paxos 換成拓樸感知多 leader；再展開 TrueTime ε / commit wait 數學、ε 暴衝失敗模式、cross-region voting 對 latency 的影響、跟 9.C10 Google internal dogfood 揭露的線性擴展模式對照">TrueTime API 深入</a> / <a href="/blog/backend/01-database/vendors/spanner/consistency-models-comparison/" data-link-title="Spanner Consistency Models 對照：external consistency vs serializability vs linearizability" data-link-desc="external consistency、serializability、linearizability 是三個常被混用的概念。本文先精確定義三者差異、再用 line-rate scaling 對照表（PG SSI / CockroachDB / Spanner / Aurora DSQL）回答為什麼 Spanner 不只是『更強的 serializable』、最後用 9.C10 揭露的 cross-region quorum 100-200ms 物理硬限解釋『強一致 &#43; 全球部署』的真實 cost">Spanner 一致性模型對照</a></li>
<li>CockroachDB retry / 隔離深入：<a href="/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/" data-link-title="CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑" data-link-desc="CockroachDB default SERIALIZABLE、application 必須包 retry loop 處理 40001 serialization_failure。本文走 PG → CockroachDB application contract 重塑視角、SAVEPOINT cockroach_restart 語法、5 種失敗模式（retry storm / 非冪等 / cross-statement state / hot row / long-running transaction）。**整篇是跨 case 合成 frame**：DoorDash case 沒揭露 retry pattern、只揭露 PG wire protocol 相容 &#43; SQL 行為仍要 audit、本章 retry contract 重塑屬通用工程議題從 Cockroach Labs 官方 docs 合成">CockroachDB transaction retry pattern</a> / <a href="/blog/backend/01-database/vendors/cockroachdb/aurora-dsql-spanner-decision-tree/" data-link-title="CockroachDB vs Aurora DSQL vs Spanner：撞牆訊號分型 &#43; 七問題決策樹" data-link-desc="Distributed SQL 三選一決策樹。先用撞牆訊號分型識別 driver path（DoorDash 單主寫入撞牆 / Netflix Cassandra 缺口 / Hard Rock 合規驅動）、再走七問題（跨雲 / 雲商生態 / 風險預算 / PG 相容 / 管理負擔 / team size / vendor sizing barrier）。PostgreSQL 相容性 audit checklist 4 項、Spanner 100 pu sizing barrier、Hard Rock 「省 10-20 工程師」機會成本警示、Netflix Database Platform Team 規模">Aurora DSQL / Spanner / CockroachDB 決策樹</a></li>
<li>Aurora 寫入語意深入：<a href="/blog/backend/01-database/vendors/aurora/storage-architecture/" data-link-title="Aurora Storage Architecture：quorum-based 分散式 log 與韌性即性能設計" data-link-desc="Aurora storage / compute 分離、6-way 跨 AZ replication、4-of-6 write / 3-of-6 read quorum、韌性投資自動 amortize 成 read 性能、DraftKings 6ms 寫 / &lt;1ms 讀 production reference">Aurora 儲存層架構</a>（6 寫 / 4 讀 quorum 對 transaction 的影響）</li>
</ul>
]]></content:encoded></item><item><title>DynamoDB Transaction 與 Conditional Write：跨 item 原子性、optimistic locking 與 idempotency</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/</link><pubDate>Tue, 02 Jun 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/transactions-conditional-writes/</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>對帳跑出一筆異常：用戶錢包餘額扣了 100 元、但對應訂單沒建立。追 log 發現 application 先 &lt;code>PutItem&lt;/code> 扣餘額、再 &lt;code>PutItem&lt;/code> 建訂單、兩步之間 process 被 OOM kill、第二步沒跑完。另一個系統反向情境：秒殺活動庫存剩 1、兩個請求同時讀到「剩 1」、各自 &lt;code>PutItem&lt;/code> 扣成 0、實際賣出 2 件。兩個 production 痛點指向同一件事 — DynamoDB 預設的單筆寫入沒有跨 item 原子性、也沒有「讀到的值寫回時還沒被改」的保證。本文展開 DynamoDB 提供的三層寫保護：跨 item transaction、單 item conditional write、version-based optimistic locking。&lt;/p>
&lt;blockquote>
&lt;p>&lt;strong>寫一致性前提：先確認 workload 適配 DynamoDB&lt;/strong>：本篇假設 workload 已通過 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>。寫一致性是 &lt;em>已選 DynamoDB&lt;/em> 後的操作層議題；若 workload 需要頻繁跨多表多列複雜交易、那是 relational 的主場、應先回頭問 DynamoDB 是否選錯。&lt;/p>&lt;/blockquote>
&lt;h2 id="核心機制三層寫保護">核心機制：三層寫保護&lt;/h2>
&lt;p>DynamoDB 的寫一致性由三種粒度不同的工具組成 — 單 item 寫、conditional write、跨 item transaction，三者解的問題與成本各異，不是單一 ACID 開關：&lt;/p>
&lt;table>
 &lt;thead>
 &lt;tr>
 &lt;th>工具&lt;/th>
 &lt;th>解的問題&lt;/th>
 &lt;th>原子性範圍&lt;/th>
 &lt;th>成本&lt;/th>
 &lt;/tr>
 &lt;/thead>
 &lt;tbody>
 &lt;tr>
 &lt;td>單 item 寫&lt;/td>
 &lt;td>一筆 item 的 put / update / delete&lt;/td>
 &lt;td>單 item&lt;/td>
 &lt;td>1x WCU&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>Conditional write&lt;/td>
 &lt;td>只在條件成立時才寫（防覆蓋、防重複）&lt;/td>
 &lt;td>單 item + 前置條件&lt;/td>
 &lt;td>1x WCU（條件不成立也計費）&lt;/td>
 &lt;/tr>
 &lt;tr>
 &lt;td>TransactWriteItems&lt;/td>
 &lt;td>多筆 item 一起成功或一起失敗&lt;/td>
 &lt;td>跨 item（同 region / account）&lt;/td>
 &lt;td>2x WCU（prepare + commit 兩階段）&lt;/td>
 &lt;/tr>
 &lt;/tbody>
&lt;/table>
&lt;p>&lt;strong>TransactWriteItems 的工程語意&lt;/strong>：&lt;/p>
&lt;ul>
&lt;li>一次 transaction 最多含若干個 action（put / update / delete / condition check）— 上限屬 vendor 規格、實作時 cross-verify AWS doc 當前數字&lt;/li>
&lt;li>全成功或全失敗：任一 action 的 condition 不成立、整個 transaction roll back、拋 &lt;code>TransactionCanceledException&lt;/code> 帶 &lt;code>CancellationReasons&lt;/code>&lt;/li>
&lt;li>不跨 region、不跨 account：transaction 只在單一 region 單一 account 內成立、Global Tables 多 region 寫不享有跨 region transaction（對應 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&amp;#43; 跨裝置同步的對照">global-tables-conflict&lt;/a>）&lt;/li>
&lt;li>兩階段（prepare + commit）導致 2x capacity 消耗 — 這是 transaction 不能濫用的成本根源&lt;/li>
&lt;/ul>
&lt;blockquote>
&lt;p>&lt;strong>Scope warning&lt;/strong>：「TransactWriteItems 100 action 上限」、「transaction 2x WCU」這些具體數字屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc 當前值。本文不含對應 production case 揭露的 transaction 規模數字。&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>對帳跑出一筆異常：用戶錢包餘額扣了 100 元、但對應訂單沒建立。追 log 發現 application 先 <code>PutItem</code> 扣餘額、再 <code>PutItem</code> 建訂單、兩步之間 process 被 OOM kill、第二步沒跑完。另一個系統反向情境：秒殺活動庫存剩 1、兩個請求同時讀到「剩 1」、各自 <code>PutItem</code> 扣成 0、實際賣出 2 件。兩個 production 痛點指向同一件事 — DynamoDB 預設的單筆寫入沒有跨 item 原子性、也沒有「讀到的值寫回時還沒被改」的保證。本文展開 DynamoDB 提供的三層寫保護：跨 item transaction、單 item conditional write、version-based optimistic locking。</p>
<blockquote>
<p><strong>寫一致性前提：先確認 workload 適配 DynamoDB</strong>：本篇假設 workload 已通過 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>。寫一致性是 <em>已選 DynamoDB</em> 後的操作層議題；若 workload 需要頻繁跨多表多列複雜交易、那是 relational 的主場、應先回頭問 DynamoDB 是否選錯。</p></blockquote>
<h2 id="核心機制三層寫保護">核心機制：三層寫保護</h2>
<p>DynamoDB 的寫一致性由三種粒度不同的工具組成 — 單 item 寫、conditional write、跨 item transaction，三者解的問題與成本各異，不是單一 ACID 開關：</p>
<table>
  <thead>
      <tr>
          <th>工具</th>
          <th>解的問題</th>
          <th>原子性範圍</th>
          <th>成本</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>單 item 寫</td>
          <td>一筆 item 的 put / update / delete</td>
          <td>單 item</td>
          <td>1x WCU</td>
      </tr>
      <tr>
          <td>Conditional write</td>
          <td>只在條件成立時才寫（防覆蓋、防重複）</td>
          <td>單 item + 前置條件</td>
          <td>1x WCU（條件不成立也計費）</td>
      </tr>
      <tr>
          <td>TransactWriteItems</td>
          <td>多筆 item 一起成功或一起失敗</td>
          <td>跨 item（同 region / account）</td>
          <td>2x WCU（prepare + commit 兩階段）</td>
      </tr>
  </tbody>
</table>
<p><strong>TransactWriteItems 的工程語意</strong>：</p>
<ul>
<li>一次 transaction 最多含若干個 action（put / update / delete / condition check）— 上限屬 vendor 規格、實作時 cross-verify AWS doc 當前數字</li>
<li>全成功或全失敗：任一 action 的 condition 不成立、整個 transaction roll back、拋 <code>TransactionCanceledException</code> 帶 <code>CancellationReasons</code></li>
<li>不跨 region、不跨 account：transaction 只在單一 region 單一 account 內成立、Global Tables 多 region 寫不享有跨 region transaction（對應 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a>）</li>
<li>兩階段（prepare + commit）導致 2x capacity 消耗 — 這是 transaction 不能濫用的成本根源</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：「TransactWriteItems 100 action 上限」、「transaction 2x WCU」這些具體數字屬 AWS vendor 規格、會隨版本調整、實作時 cross-verify 官方 doc 當前值。本文不含對應 production case 揭露的 transaction 規模數字。</p></blockquote>
<p>對應 knowledge card：<a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a>、<a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary</a>、<a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level</a>。</p>
<h2 id="conditional-write最便宜的一致性工具">Conditional Write：最便宜的一致性工具</h2>
<p>跨 item transaction 之前、先看單 item conditional write 能不能解。多數「race condition」其實是單 item 問題、不需要 transaction 的 2x 成本。</p>
<p>ConditionExpression 在寫入前檢查條件、條件不成立則拒絕寫入並拋 <code>ConditionalCheckFailedException</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 防重複建立：只有 item 不存在時才寫</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 class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;META&#34;</span><span class="p">,</span> <span class="s2">&#34;status&#34;</span><span class="p">:</span> <span class="s2">&#34;created&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;attribute_not_exists(PK)&#34;</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="p">)</span></span></span></code></pre></div>




<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 防超賣：只有庫存 &gt; 0 時才扣</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">update_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl">    <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;SKU#</span><span class="si">{</span><span class="n">sku</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;STOCK&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl">    <span class="n">UpdateExpression</span><span class="o">=</span><span class="s2">&#34;SET stock = stock - :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">5</span><span class="cl">    <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;stock &gt;= :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">6</span><span class="cl">    <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;:one&#34;</span><span class="p">:</span> <span class="mi">1</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><p>第二個例子是關鍵：<code>update_item</code> 帶 condition 是 <em>原子的 read-modify-write</em>。DynamoDB 在單 item 上保證「條件檢查 + 寫入」不會被其他寫入插隊。前述「兩個請求同時讀到剩 1」的超賣問題、用單 item conditional update 即可解、不需要 transaction。</p>
<h2 id="optimistic-locking跨讀寫週期的保護">Optimistic Locking：跨讀寫週期的保護</h2>
<p>Conditional write 解單次寫的 race；當 application 需要「讀出來、業務邏輯運算、再寫回」、且運算期間不能被別人改、用 version-based optimistic locking。</p>
<p>機制是在 item 上維護一個 <code>version</code> attribute、寫回時用 condition 確認 version 沒被改過：</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">update_with_optimistic_lock</span><span class="p">(</span><span class="n">pk</span><span class="p">,</span> <span class="n">new_balance</span><span class="p">,</span> <span class="n">expected_version</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">update_item</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">        <span class="n">Key</span><span class="o">=</span><span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="n">pk</span><span class="p">,</span> <span class="s2">&#34;SK&#34;</span><span class="p">:</span> <span class="s2">&#34;WALLET&#34;</span><span class="p">},</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="n">UpdateExpression</span><span class="o">=</span><span class="s2">&#34;SET balance = :b, version = version + :one&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">        <span class="n">ConditionExpression</span><span class="o">=</span><span class="s2">&#34;version = :expected&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">        <span class="n">ExpressionAttributeValues</span><span class="o">=</span><span class="p">{</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;:b&#34;</span><span class="p">:</span> <span class="n">new_balance</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="s2">&#34;:one&#34;</span><span class="p">:</span> <span class="mi">1</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="s2">&#34;:expected&#34;</span><span class="p">:</span> <span class="n">expected_version</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <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><p>讀出時拿到 <code>version=5</code>、運算後寫回時 condition 是 <code>version = 5</code>；若期間別人已寫成 <code>version=6</code>、condition 失敗、application 收到 <code>ConditionalCheckFailedException</code>、retry 整個讀-算-寫週期。</p>
<p>optimistic 的代價是衝突時要重試、不是阻塞等待。高衝突 workload（同一 item 大量並發寫）optimistic locking 會 retry 風暴、這時要回頭問資料模型 — 把熱點 item 拆開、或改用單 item atomic counter（<code>ADD</code>）避免 read-modify-write。</p>
<blockquote>
<p><strong>Scope warning</strong>：optimistic locking 是通用並發控制 pattern、DynamoDB 用 ConditionExpression 實作；本段機制描述屬 vendor 規格 + 通用工程知識、非 production case 揭露。</p></blockquote>
<h2 id="idempotencytransaction-的重複提交保護">Idempotency：transaction 的重複提交保護</h2>
<p>分散式系統的寫入會重試（network timeout、client retry）。同一筆 transaction 重送兩次、不能扣兩次款。DynamoDB transaction 提供 <code>ClientRequestToken</code> 做 dedup：</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="n">client</span><span class="o">.</span><span class="n">transact_write_items</span><span class="p">(</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="n">ClientRequestToken</span><span class="o">=</span><span class="n">request_id</span><span class="p">,</span>  <span class="c1"># 同 token 在 dedup window 內視為同一次</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="n">TransactItems</span><span class="o">=</span><span class="p">[</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;Update&#34;</span><span class="p">:</span> <span class="p">{</span>  <span class="c1"># 扣錢包</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">            <span class="s2">&#34;TableName&#34;</span><span class="p">:</span> <span class="s2">&#34;wallet&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">            <span class="s2">&#34;Key&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;S&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;USER#</span><span class="si">{</span><span class="n">uid</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">}},</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">            <span class="s2">&#34;UpdateExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;SET balance = balance - :amt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">            <span class="s2">&#34;ConditionExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;balance &gt;= :amt&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">            <span class="s2">&#34;ExpressionAttributeValues&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;:amt&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;N&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">amount</span><span class="p">)}},</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="p">}},</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">        <span class="p">{</span><span class="s2">&#34;Put&#34;</span><span class="p">:</span> <span class="p">{</span>  <span class="c1"># 建訂單</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">            <span class="s2">&#34;TableName&#34;</span><span class="p">:</span> <span class="s2">&#34;orders&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">            <span class="s2">&#34;Item&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;PK&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;S&#34;</span><span class="p">:</span> <span class="sa">f</span><span class="s2">&#34;ORDER#</span><span class="si">{</span><span class="n">order_id</span><span class="si">}</span><span class="s2">&#34;</span><span class="p">},</span> <span class="s2">&#34;amount&#34;</span><span class="p">:</span> <span class="p">{</span><span class="s2">&#34;N&#34;</span><span class="p">:</span> <span class="nb">str</span><span class="p">(</span><span class="n">amount</span><span class="p">)}},</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">            <span class="s2">&#34;ConditionExpression&#34;</span><span class="p">:</span> <span class="s2">&#34;attribute_not_exists(PK)&#34;</span><span class="p">,</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">        <span class="p">}},</span>
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="p">],</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl"><span class="p">)</span></span></span></code></pre></div><p>同一個 <code>ClientRequestToken</code> 在 dedup window 內重送、DynamoDB 視為同一次、不會重複執行。這解掉開場的「扣款成功但訂單沒建」問題：兩個 action 在同一 transaction、要嘛都成、要嘛都不成；client 重試帶同 token、不會重複扣款。</p>
<blockquote>
<p><strong>Scope warning</strong>：「ClientRequestToken dedup window 約 10 分鐘」屬 AWS vendor 規格、實作時 cross-verify 官方 doc；application 層仍應有自己的 idempotency key 設計、不依賴 vendor dedup window 當唯一防線（對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 卡）。</p></blockquote>
<h2 id="操作流程">操作流程</h2>
<p>從一致性需求判讀到工具選擇的 6 步流程。</p>
<h4 id="step-1分類寫入的一致性需求">Step 1：分類寫入的一致性需求</h4>
<p>每個寫入路徑標記它真正需要的保護：</p>
<ul>
<li>單筆獨立寫、無前置條件 → 單 item put / update（最便宜）</li>
<li>單筆寫但要防覆蓋 / 防重複 / 防超賣 → 單 item conditional write</li>
<li>讀-算-寫週期、期間不能被改 → version optimistic locking</li>
<li>多筆 item 必須一起成功或失敗 → TransactWriteItems</li>
</ul>
<h4 id="step-2先用-conditional-write-解單-item-race">Step 2：先用 conditional write 解單 item race</h4>
<p>把「需要 transaction」當成最後選項。多數 race condition 是單 item 問題、conditional update 的 atomic read-modify-write 已足夠、成本 1x 而非 2x。</p>
<h4 id="step-3跨-item-才上-transaction">Step 3：跨 item 才上 transaction</h4>
<p>只有「多筆 item 的修改必須綁在一起」才用 TransactWriteItems。例：扣錢包 + 建訂單 + 寫流水帳三筆綁定。寫進 transaction 的 item 數量越少越好、每多一個 item 多一份 2x 成本。</p>
<h4 id="step-4加-idempotency-token">Step 4：加 idempotency token</h4>
<p>所有會被 client 重試的 transaction 帶 <code>ClientRequestToken</code>；token 用業務層的唯一鍵（order_id / request_id）、不要用隨機值（隨機值每次重試都不同、dedup 失效）。</p>
<h4 id="step-5處理失敗例外">Step 5：處理失敗例外</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="kn">from</span> <span class="nn">botocore.exceptions</span> <span class="kn">import</span> <span class="n">ClientError</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">
</span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="k">try</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">    <span class="n">client</span><span class="o">.</span><span class="n">transact_write_items</span><span class="p">(</span><span class="o">...</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="k">except</span> <span class="n">ClientError</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="n">code</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;Error&#34;</span><span class="p">][</span><span class="s2">&#34;Code&#34;</span><span class="p">]</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">    <span class="k">if</span> <span class="n">code</span> <span class="o">==</span> <span class="s2">&#34;TransactionCanceledException&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">        <span class="n">reasons</span> <span class="o">=</span> <span class="n">e</span><span class="o">.</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;CancellationReasons&#34;</span><span class="p">]</span>  <span class="c1"># 逐 action 失敗原因</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">        <span class="c1"># 區分 ConditionalCheckFailed（業務拒絕、不重試）</span>
</span></span><span class="line"><span class="ln">10</span><span class="cl">        <span class="c1"># vs TransactionConflict / ThrottlingError（可重試）</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">elif</span> <span class="n">code</span> <span class="o">==</span> <span class="s2">&#34;ConditionalCheckFailedException&#34;</span><span class="p">:</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="k">pass</span>  <span class="c1"># 單 item condition 失敗、業務層決定</span></span></span></code></pre></div><p>關鍵：<code>ConditionalCheckFailed</code> 是 <em>業務拒絕</em>（庫存不足、訂單已存在）、不該不分原因一律重試；<code>TransactionConflict</code> / <code>ThrottlingError</code> 才是可重試的 transient error。混為一談會把「庫存真的不夠」當成 transient 一直重試。</p>
<h4 id="step-6驗證點">Step 6：驗證點</h4>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-python" data-lang="python"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1"># 驗證 conditional write 真的擋住併發</span>
</span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"># 啟兩個並發 update 扣同一庫存、確認只有一個成功、另一個拋 ConditionalCheckFailed</span>
</span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="n">response</span> <span class="o">=</span> <span class="n">table</span><span class="o">.</span><span class="n">update_item</span><span class="p">(</span><span class="o">...</span><span class="p">,</span> <span class="n">ReturnValues</span><span class="o">=</span><span class="s2">&#34;UPDATED_NEW&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="nb">print</span><span class="p">(</span><span class="n">response</span><span class="p">[</span><span class="s2">&#34;Attributes&#34;</span><span class="p">])</span>  <span class="c1"># 確認 version / stock 變化符合預期</span></span></span></code></pre></div><p><strong>Rollback boundary</strong>：transaction 本身全成全敗、無 partial state 需要 rollback；但 application 層若在 transaction 外還有副作用（送通知、呼叫外部 API）、那些不在 transaction 保護內、要另行設計補償。</p>
<h2 id="失敗模式">失敗模式</h2>
<p>production 常見的 5 個踩雷：</p>
<h4 id="case-1用-transaction-取代本該單-item-的寫">Case 1：用 transaction 取代本該單 item 的寫</h4>
<p>team 把所有寫入都包進 TransactWriteItems「保險」、cost 翻倍、且 transaction 有 throughput 上限比單寫低。修法：transaction 只用於真正跨 item 綁定的場景；單 item 用 conditional write。</p>
<h4 id="case-2optimistic-lock-在高衝突-item-上-retry-風暴">Case 2：optimistic lock 在高衝突 item 上 retry 風暴</h4>
<p>熱點 item（如全站唯一的計數器）大量並發寫、version condition 不斷失敗、application retry 風暴、latency 爆炸。修法：高衝突計數改用 atomic <code>ADD</code>（單 item 原子累加、不需 read-modify-write）；或把計數 shard 成多個 item 分散寫入。</p>
<h4 id="case-3idempotency-token-用隨機值">Case 3：idempotency token 用隨機值</h4>
<p>這個 case 的失敗代價跟其他踩雷不同層級。Case 1（cost 翻倍）、Case 2（retry 風暴）、Case 5（跨 region 誤解）都可以在發現後調整設定或改資料模型補救；idempotency token 用隨機值導致的重複扣款是 <em>財務不可逆</em> — 每次 client retry 產生新 token、dedup 完全失效、同一筆付款被執行多次、錢已經從用戶帳戶扣走、要靠對帳發現後人工退款，且退款流程本身又是另一條容易出錯的補償路徑。修法：token 綁業務唯一鍵（order_id / payment_id）、同一筆業務操作的所有重試共用同一 token；且不只依賴 DynamoDB 的 dedup window（有時效上限），application 層自己也維護 idempotency 記錄當第二道防線（對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 卡）。涉及金流的寫入，這道防線要在上線前用「同一 token 重送 N 次只執行一次」的測試明確驗證。</p>
<h4 id="case-4把-conditionalcheckfailed-當-transient-error-重試">Case 4：把 ConditionalCheckFailed 當 transient error 重試</h4>
<p>庫存真的為 0、condition 永遠失敗、application 無限重試打爆 capacity。修法：例外分流 — 業務拒絕（ConditionalCheckFailed）回報給呼叫端、transient error（throttle / conflict）才 backoff retry。</p>
<h4 id="case-5以為-transaction-跨-region-有效">Case 5：以為 transaction 跨 region 有效</h4>
<p>Global Tables 多 region 部署、誤以為 TransactWriteItems 在跨 region 也原子。實際 transaction 只在單 region 成立、跨 region 是 last-writer-wins（對應 <a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a>）。修法：跨 region 一致性需求不能靠 transaction、要重新設計資料 ownership（單一 region 為 write authority）。</p>
<p><strong>Anti-recommendation</strong>：寫入無併發競爭、或業務本身可接受最終一致（各 message_id 獨立的訊息事件即屬此類）→ 不要為了求保險而加 transaction；transaction 的 2x 成本只在真正需要跨 item 原子性時才值得。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<p>CloudWatch metric：</p>
<ul>
<li><code>TransactionConflict</code>：transaction 因併發衝突取消的次數、持續高代表熱點 item 競爭</li>
<li><code>ConditionalCheckFailedRequests</code>：condition 失敗次數、區分業務拒絕 vs 設計問題</li>
<li><code>ThrottledRequests</code>：transaction 因 capacity 不足被限流、transaction 的 2x 消耗更容易撞上限</li>
</ul>
<p><strong>判讀</strong>：</p>
<ul>
<li><code>TransactionConflict</code> 持續上升 → 資料模型有熱點、考慮拆 item 或改 atomic counter</li>
<li><code>ConditionalCheckFailed</code> 突然飆高 → 可能是業務異常（大量重複請求 / 攻擊）、也可能是 application 邏輯把 version 算錯</li>
<li>transaction 的 capacity 用量按 2x 計、容量規劃要把 transaction 比例算進去</li>
</ul>
<blockquote>
<p><strong>Scope warning</strong>：本文未引用 production case 的 transaction metric 數字；上述 metric 名稱與判讀屬 vendor 規格 + 通用觀測工程。</p></blockquote>
<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/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">1.3 transaction 與一致性邊界</a>。</p>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="跟-relational-transaction-的責任差異">跟 relational transaction 的責任差異</h3>
<p>DynamoDB transaction 跟 relational transaction 不是同一個東西。Relational transaction 支援任意複雜的多表多列交易、長交易、isolation level 調整；DynamoDB transaction 是「一次性提交一組有限 action、全成全敗、無互動式 transaction、無 SELECT FOR UPDATE」。當 application 需要長交易、複雜 join 內的一致性、或多步互動式 transaction、那是 relational 的場景、不該硬塞進 DynamoDB（回頭看 single-table 4 軸前置判讀）。</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> — 該篇主寫 <em>讀</em> 一致性（eventual vs strong read）、本篇主寫 <em>寫</em> 原子性、兩篇互補</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> — 跨 item transaction 常用於 single-table 內多 entity 綁定寫</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/global-tables-conflict/" data-link-title="DynamoDB Global Tables：multi-region active-active、LWW conflict 與 cross-device sync 正向用例" data-link-desc="Global Tables 不只是 conflict 痛點、也是 cross-device sync / global read / DR failover 的正向工程方案；本文展開 B2B SaaS vs B2C 業務 driver、LWW conflict resolution、reconciliation pipeline，含 Genesys 99.999% 跨 15 region 跟 Disney&#43; 跨裝置同步的對照">global-tables-conflict</a> — transaction 不跨 region、多 region 寫衝突另有處理</li>
<li><a href="/blog/backend/01-database/vendors/dynamodb/streams-lambda-event-driven/" data-link-title="DynamoDB Streams 與 Lambda 事件驅動：CDC、shard 順序保證、消費模式與失敗處理" data-link-desc="DynamoDB Streams 不是免費的可靠事件流；本文展開 stream record 的四種 view type、shard 對應 partition 的順序保證邊界、Lambda event source mapping vs Kinesis 消費模式、at-least-once 下游冪等需求，以及 batch 失敗時的 bisect / DLQ 處理">streams-lambda-event-driven</a> — transaction 寫入會觸發 stream、下游 event 處理要 idempotent</li>
<li>替代路由：頻繁複雜交易需求 → 回 <a href="/blog/backend/01-database/vendors/postgresql/" data-link-title="PostgreSQL" data-link-desc="多用途 OLTP 主流關聯式資料庫、MVCC、豐富 SQL 特性、是 Aurora / Cosmos DB / Spanner / CockroachDB / Aurora DSQL 的相容目標">PostgreSQL</a> / <a href="/blog/backend/01-database/vendors/aurora/" data-link-title="AWS Aurora" data-link-desc="AWS managed PostgreSQL / MySQL、storage / compute 分離、&#43;75% 效能改善的 production 證據">Aurora</a>、relational transaction 是主場</li>
<li>對應 <a href="/blog/backend/01-database/reconciliation-data-repair/" data-link-title="1.9 Reconciliation 與 Data Repair" data-link-desc="資料不一致的分類、偵測模式、修復策略、audit trail、跟 backup / PITR 整合">1.9 Reconciliation 與 Data Repair</a> — 寫一致性失守後的對帳與修復</li>
</ul>
]]></content:encoded></item><item><title>CockroachDB Transaction Retry Pattern：serializable default 與 application contract 重塑</title><link>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</link><pubDate>Wed, 27 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/transaction-retry-pattern/</guid><description>&lt;blockquote>
&lt;p>本文是 &lt;a href="https://tarrragon.github.io/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview&lt;/a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 &lt;em>serializable default 對 application transaction contract 的重塑&lt;/em>。&lt;/p>
&lt;p>&lt;strong>Scope warning（最高、F4 Frame 2）&lt;/strong>：&lt;strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露&lt;/strong>。3 個 CockroachDB direct case（&lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&amp;#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&amp;#43; cluster / 60&amp;#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix&lt;/a> / &lt;a href="https://tarrragon.github.io/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &amp;#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &amp;#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital&lt;/a>）對 application transaction retry contract 重塑的揭露 &lt;em>都偏弱&lt;/em> — DoorDash case 只寫 PostgreSQL wire &lt;em>protocol-level&lt;/em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、&lt;strong>沒&lt;/strong>直接寫 &lt;code>40001 serialization_failure&lt;/code> / &lt;code>SAVEPOINT cockroach_restart&lt;/code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 &lt;em>自己跑 application audit&lt;/em> 而不是直接套合成的 pattern。&lt;/p>&lt;/blockquote>
&lt;hr>
&lt;h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊&lt;/h2>
&lt;p>團隊從 PostgreSQL（default &lt;code>READ COMMITTED&lt;/code>）遷到 CockroachDB（default &lt;code>SERIALIZABLE&lt;/code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 &lt;code>40001 serialization_failure&lt;/code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。&lt;/p></description><content:encoded><![CDATA[<blockquote>
<p>本文是 <a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a> 的 implementation-layer deep article。Overview 已界定 CockroachDB 的 PostgreSQL wire 相容定位、本文聚焦 <em>serializable default 對 application transaction contract 的重塑</em>。</p>
<p><strong>Scope warning（最高、F4 Frame 2）</strong>：<strong>本篇整篇是跨 case 合成 frame、不是單一 case 揭露</strong>。3 個 CockroachDB direct case（<a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a> / <a href="/blog/backend/09-performance-capacity/cases/netflix-cockroachdb-multi-region-fleet/" data-link-title="9.C40 Netflix：380&#43; CockroachDB cluster 的 multi-active 拓樸艦隊" data-link-desc="Netflix 把 Cassandra 不夠用的 transactional workload 移到 CockroachDB、380&#43; cluster / 60&#43; 跨 region、含 Open Connect、studio cloud drive、gaming control plane">9.C40 Netflix</a> / <a href="/blog/backend/09-performance-capacity/cases/hard-rock-digital-cockroachdb-sports-betting/" data-link-title="9.C41 Hard Rock Digital：CockroachDB on AWS Outposts、Wire Act 合規 &#43; 跨州單一邏輯 DB" data-link-desc="Hard Rock Digital 用 CockroachDB 跨 AWS Outposts &#43; US-East-1、Wire Act 強制資料留州、單一邏輯 DB 解多州 sportsbook、100 node 32 vCPU 撐 Super Bowl">9.C41 Hard Rock Digital</a>）對 application transaction retry contract 重塑的揭露 <em>都偏弱</em> — DoorDash case 只寫 PostgreSQL wire <em>protocol-level</em> 相容、SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」、<strong>沒</strong>直接寫 <code>40001 serialization_failure</code> / <code>SAVEPOINT cockroach_restart</code> / hot row contention / retry loop pattern。Netflix / Hard Rock case 完全沒寫 retry pattern。本章 retry pattern 議題從 Cockroach Labs 官方 SQL Layer docs + PG → CockroachDB 通用 contract 重塑視角合成、DoorDash 只作為 trigger context（撞牆訊號 + 觸發遷移）、不是 ground truth case study。讀者引用本章內容到實際系統前、應該 <em>自己跑 application audit</em> 而不是直接套合成的 pattern。</p></blockquote>
<hr>
<h2 id="問題情境從-pg-read-committed-遷到-cockroachdb-serializable-的-application-衝擊">問題情境：從 PG READ COMMITTED 遷到 CockroachDB SERIALIZABLE 的 application 衝擊</h2>
<p>團隊從 PostgreSQL（default <code>READ COMMITTED</code>）遷到 CockroachDB（default <code>SERIALIZABLE</code>）、上線後 application transaction retry 突然爆增、user-facing latency p99 高 5 倍、error rate 顯著上升。Driver 不會自動 retry — 應用層必須認得 <code>40001 serialization_failure</code> 並包 retry loop with exponential backoff。沒包就是直接拋例外給用戶。</p>
<p>讀者常問：</p>
<ul>
<li>為什麼同樣的 transaction 在 CockroachDB 一直 retry、在 PostgreSQL 從來不會？</li>
<li><code>40001 serialization_failure</code> error 怎麼處理、能不能直接 swallow？</li>
<li>我要把所有 application transaction 都改成 retry loop 包起來嗎？</li>
<li>能不能改 isolation level 回 <code>READ COMMITTED</code>、放棄 serializable 保證？</li>
</ul>
<p>四題的回答都依賴一個前提：CockroachDB 的 application transaction contract 跟 PostgreSQL default 不一樣、必須重塑。</p>
<h3 id="scope-warning-explicit-labeldoordash-case-沒揭露-retry-pattern">Scope warning explicit label：DoorDash case 沒揭露 retry pattern</h3>
<p><strong>DoorDash case 沒直接揭露 serializable retry contract / 40001 / SAVEPOINT pattern / hot row contention</strong>。case 只寫「PostgreSQL wire protocol 相容、實際 SQL 行為（serializable default、retry semantics、partial index）<em>仍要驗證</em>」（DoorDash 觀察段 / 策略段 3、F4.4）。</p>
<p>本章 retry pattern 議題是從 PG → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露。引用 DoorDash 時應該用：</p>
<ul>
<li><strong>正確口徑</strong>：「DoorDash 揭露 Aurora Postgres 1.636 M QPS 撞牆 → 引出 distributed SQL retry contract 需求、本章 retry pattern 議題是從 PostgreSQL → CockroachDB 通用 contract 重塑視角合成、不是 DoorDash case 直接揭露」</li>
<li><strong>不要寫成</strong>：「DoorDash retry pattern」、「DoorDash 揭露 40001 處理」之類把合成包成 case fact 的語法</li>
</ul>
<h3 id="case-anchortrigger-context不是-ground-truth">Case anchor（trigger context、不是 ground truth）</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>：提供「PG wire 相容、SQL 行為仍要 audit」的 case 警語（F4.4）、作為本章 <em>為什麼 retry contract 要重塑</em> 的觸發訊號。retry pattern 本體走 standard-driven（Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs）</li>
</ul>
<p>Sibling 對照 <a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings Aurora financial ledger</a> 提供 <em>PostgreSQL READ COMMITTED + Aurora</em> 的另一條路徑 — 用 application-level sharding（200 個獨立 Aurora cluster）避開 retry、而不是處理 retry。<strong>Scope warning</strong>：DraftKings case <em>沒</em> 寫 PostgreSQL READ COMMITTED retry pattern、case 是 Aurora 內 business sharding 路徑。本章引用 DraftKings 為「假想若把 DraftKings 遷 CockroachDB 會撞到 retry contract 重塑」合成對照、不是 case 直接揭露。</p>
<h2 id="核心機制serializable-default-跟-postgresql-的差異">核心機制：serializable default 跟 PostgreSQL 的差異</h2>
<blockquote>
<p><strong>來源分層</strong>：本段機制來源是 Cockroach Labs 官方 SQL Layer docs + Transaction Retry docs（standard-driven）、<em>不是</em> 從 case 抽取。3 個 direct case 都沒揭露這些機制細節。</p></blockquote>
<h3 id="serializable-是-cockroachdb-的-default">Serializable 是 CockroachDB 的 default</h3>
<p>CockroachDB 預設 <code>SERIALIZABLE</code> — 最強 isolation level、保證 transaction 結果等同某個 serial order（即所有 transaction 像逐個按順序執行）。對比：</p>
<table>
  <thead>
      <tr>
          <th>維度</th>
          <th>PostgreSQL default</th>
          <th>CockroachDB default</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Isolation</td>
          <td>READ COMMITTED</td>
          <td>SERIALIZABLE</td>
      </tr>
      <tr>
          <td>衝突處理</td>
          <td>後 writer 等 lock</td>
          <td>衝突即 abort、丟 40001</td>
      </tr>
      <tr>
          <td>機制</td>
          <td>row lock + MVCC</td>
          <td>timestamp ordering + write intent</td>
      </tr>
      <tr>
          <td>Retry 必要性</td>
          <td>通常不需要</td>
          <td>application 必須有 retry loop</td>
      </tr>
      <tr>
          <td>SSI 對應</td>
          <td>PG SSI（opt-in）</td>
          <td>預設啟用</td>
      </tr>
  </tbody>
</table>
<h3 id="conflict-detectionread--write-set-衝突就-abort">Conflict detection：read / write set 衝突就 abort</h3>
<p>CockroachDB 追蹤每個 transaction 的 read set 跟 write set。當兩個並行 transaction 的 read / write set 衝突、CockroachDB abort 後到的那個、發 <a href="/blog/backend/knowledge-cards/serialization-failure/" data-link-title="Serialization Failure" data-link-desc="SERIALIZABLE isolation 衝突偵測後 abort 的協議、SQL state 40001、application 必須包 retry loop">Serialization Failure</a>（<code>40001 serialization_failure</code>）。</p>
<p>對比 PostgreSQL serializable（SSI）：兩者都是「post-detect」、commit 時偵測 anomaly、不是 pre-lock。差別在 <em>衝突偵測時機</em> 跟 <em>成本</em>：</p>
<ul>
<li>PostgreSQL SSI：用 predicate lock 追蹤 query 條件、commit 時偵測</li>
<li>CockroachDB：用 timestamp ordering + write intent、衝突 <em>當下</em> 就 abort</li>
</ul>
<p>CockroachDB 的成本在「衝突立刻 abort 不等 commit」、好處是「retry window 較短、不會跑完整個 transaction 才發現衝突」。</p>
<h3 id="application-端-retrydriver-不自動處理">Application 端 retry：driver 不自動處理</h3>
<p>關鍵：<strong>CockroachDB driver 不自動 retry</strong>。application 收到 <code>40001 serialization_failure</code> 必須自己決定怎麼處理 — exponential backoff retry、circuit break、或拋給上層。</p>
<p>對比 PostgreSQL：PostgreSQL READ COMMITTED 幾乎不會丟 serialization failure（後 writer 等 lock 不 abort）、SERIALIZABLE 才會、但多數 application 沒走 SERIALIZABLE。CockroachDB <em>預設</em> 就是 SERIALIZABLE、所以 retry loop 是 <em>必要</em>、不是 optional。</p>
<h3 id="savepoint-pattern官方推薦寫法">Savepoint pattern：官方推薦寫法</h3>
<p>Cockroach Labs 官方推薦的 retry pattern 用 <code>SAVEPOINT cockroach_restart</code>：</p>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">BEGIN</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 2</span><span class="cl"><span class="w"></span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 4</span><span class="cl"><span class="w"></span><span class="c1">-- 做正常 transaction 工作
</span></span></span><span class="line"><span class="ln"> 5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 6</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">-</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">1</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 7</span><span class="cl"><span class="w"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">accounts</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="n">balance</span><span class="w"> </span><span class="o">+</span><span class="w"> </span><span class="mi">100</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="mi">2</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln"> 8</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln"> 9</span><span class="cl"><span class="w"></span><span class="n">RELEASE</span><span class="w"> </span><span class="n">SAVEPOINT</span><span class="w"> </span><span class="n">cockroach_restart</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">10</span><span class="cl"><span class="w"></span><span class="k">COMMIT</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">11</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">12</span><span class="cl"><span class="w"></span><span class="c1">-- 如果中途 40001：
</span></span></span><span class="line"><span class="ln">13</span><span class="cl"><span class="c1">-- ROLLBACK TO SAVEPOINT cockroach_restart;
</span></span></span><span class="line"><span class="ln">14</span><span class="cl"><span class="c1">-- 重新跑 transaction body、再 RELEASE + COMMIT</span></span></span></code></pre></div><p><code>cockroach_restart</code> 是特殊保留 savepoint name — CockroachDB 認得這個名字、會把 <code>ROLLBACK TO SAVEPOINT cockroach_restart</code> 視為「重啟整個 transaction」而不是部分 rollback。</p>
<h3 id="read-committed-是-v232-可選降級">READ COMMITTED 是 v23.2+ 可選降級</h3>
<p>CockroachDB v23.2+ 新增 <code>READ COMMITTED</code> isolation level — application 可選擇用 weaker isolation 換少 retry。但這是「降級」、失去 serializable 保證 — 對應的反例段在失敗模式段展開（金融 ledger 走 READ COMMITTED 可能讓 balance 變負）。</p>
<p>對應 <a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> 跟 <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a>。</p>
<h3 id="doordash-case-對接點trigger-context-only">DoorDash case 對接點（trigger context only）</h3>
<p>DoorDash case 揭露 PG wire <em>protocol-level</em> 相容、明示 SQL 行為（serializable default / retry semantics / partial index）「仍要驗證」（F4.4）。本章機制段就是回答「audit 什麼」的具體展開 — 但 audit checklist 本體屬通用工程知識、case 沒 ground truth。</p>
<p>引用紀律：「DoorDash 揭露 PG wire 相容、SQL 行為仍要 audit、其中 serializable default 跟 retry semantics 是 application contract 重塑的核心議題」— 把 case 揭露的 fact 跟本章合成的 frame 分開講。</p>
<h2 id="操作流程retry-loop-設計">操作流程：retry loop 設計</h2>
<h3 id="retry-loop-偽碼">Retry loop 偽碼</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-go" data-lang="go"><span class="line"><span class="ln"> 1</span><span class="cl"><span class="k">for</span> <span class="nx">attempt</span> <span class="o">:=</span> <span class="mi">0</span><span class="p">;</span> <span class="nx">attempt</span> <span class="p">&lt;</span> <span class="nx">MAX_RETRIES</span><span class="p">;</span> <span class="nx">attempt</span><span class="o">++</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln"> 2</span><span class="cl">    <span class="nx">tx</span><span class="p">,</span> <span class="nx">err</span> <span class="o">:=</span> <span class="nx">db</span><span class="p">.</span><span class="nf">Begin</span><span class="p">()</span>
</span></span><span class="line"><span class="ln"> 3</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 4</span><span class="cl">
</span></span><span class="line"><span class="ln"> 5</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln"> 6</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">!=</span> <span class="kc">nil</span> <span class="p">{</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">();</span> <span class="k">return</span> <span class="nx">err</span> <span class="p">}</span>
</span></span><span class="line"><span class="ln"> 7</span><span class="cl">
</span></span><span class="line"><span class="ln"> 8</span><span class="cl">    <span class="c1">// ... 跑 transaction body ...</span>
</span></span><span class="line"><span class="ln"> 9</span><span class="cl">
</span></span><span class="line"><span class="ln">10</span><span class="cl">    <span class="nx">_</span><span class="p">,</span> <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Exec</span><span class="p">(</span><span class="s">&#34;RELEASE SAVEPOINT cockroach_restart&#34;</span><span class="p">)</span>
</span></span><span class="line"><span class="ln">11</span><span class="cl">    <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span>
</span></span><span class="line"><span class="ln">12</span><span class="cl">        <span class="nx">err</span> <span class="p">=</span> <span class="nx">tx</span><span class="p">.</span><span class="nf">Commit</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">13</span><span class="cl">        <span class="k">if</span> <span class="nx">err</span> <span class="o">==</span> <span class="kc">nil</span> <span class="p">{</span> <span class="k">return</span> <span class="kc">nil</span> <span class="p">}</span> <span class="c1">// 成功</span>
</span></span><span class="line"><span class="ln">14</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">15</span><span class="cl">
</span></span><span class="line"><span class="ln">16</span><span class="cl">    <span class="k">if</span> <span class="nf">isSerializationFailure</span><span class="p">(</span><span class="nx">err</span><span class="p">)</span> <span class="p">{</span> <span class="c1">// SQLSTATE == &#34;40001&#34;</span>
</span></span><span class="line"><span class="ln">17</span><span class="cl">        <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">18</span><span class="cl">        <span class="nx">backoff</span> <span class="o">:=</span> <span class="nx">time</span><span class="p">.</span><span class="nf">Duration</span><span class="p">(</span><span class="nx">math</span><span class="p">.</span><span class="nf">Pow</span><span class="p">(</span><span class="mi">2</span><span class="p">,</span> <span class="nb">float64</span><span class="p">(</span><span class="nx">attempt</span><span class="p">)))</span> <span class="o">*</span> <span class="mi">10</span> <span class="o">*</span> <span class="nx">time</span><span class="p">.</span><span class="nx">Millisecond</span>
</span></span><span class="line"><span class="ln">19</span><span class="cl">        <span class="nx">time</span><span class="p">.</span><span class="nf">Sleep</span><span class="p">(</span><span class="nx">backoff</span> <span class="o">+</span> <span class="nf">jitter</span><span class="p">())</span>
</span></span><span class="line"><span class="ln">20</span><span class="cl">        <span class="k">continue</span>
</span></span><span class="line"><span class="ln">21</span><span class="cl">    <span class="p">}</span>
</span></span><span class="line"><span class="ln">22</span><span class="cl">
</span></span><span class="line"><span class="ln">23</span><span class="cl">    <span class="nx">tx</span><span class="p">.</span><span class="nf">Rollback</span><span class="p">()</span>
</span></span><span class="line"><span class="ln">24</span><span class="cl">    <span class="k">return</span> <span class="nx">err</span> <span class="c1">// 非 retry-able error</span>
</span></span><span class="line"><span class="ln">25</span><span class="cl"><span class="p">}</span>
</span></span><span class="line"><span class="ln">26</span><span class="cl"><span class="k">return</span> <span class="nx">ErrMaxRetriesExceeded</span></span></span></code></pre></div><p>關鍵點：</p>
<ul>
<li>exponential backoff with jitter（避免 retry storm 同步）</li>
<li>max retry 上限（避免無限 loop、要有 circuit breaker）</li>
<li>只 retry serialization failure、其他 error 直接拋</li>
<li>transaction body 必須是 <em>冪等</em> 的（同樣 input 多次執行結果一致）</li>
</ul>
<h3 id="配置">配置</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 改 transaction isolation level（v23.2+ 才支援 READ COMMITTED）
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SET</span><span class="w"> </span><span class="k">TRANSACTION</span><span class="w"> </span><span class="k">ISOLATION</span><span class="w"> </span><span class="k">LEVEL</span><span class="w"> </span><span class="k">READ</span><span class="w"> </span><span class="k">COMMITTED</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看當前 session 預設
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SHOW</span><span class="w"> </span><span class="k">SESSION</span><span class="w"> </span><span class="n">default_transaction_isolation</span><span class="p">;</span></span></span></code></pre></div><h3 id="驗證點">驗證點</h3>





<div class="highlight"><pre tabindex="0" class="chroma"><code class="language-sql" data-lang="sql"><span class="line"><span class="ln">1</span><span class="cl"><span class="c1">-- 看 transaction retry 統計
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">txn_stats</span><span class="p">;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w"></span><span class="c1">-- 看哪些 query / table 衝突最多
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="c1"></span><span class="k">SELECT</span><span class="w"> </span><span class="o">*</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">crdb_internal</span><span class="p">.</span><span class="n">cluster_contention_events</span><span class="w"> </span><span class="k">ORDER</span><span class="w"> </span><span class="k">BY</span><span class="w"> </span><span class="k">count</span><span class="w"> </span><span class="k">DESC</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">10</span><span class="p">;</span></span></span></code></pre></div><h3 id="idempotency-設計transaction-body-必須冪等">Idempotency 設計：transaction body 必須冪等</h3>
<p>retry-safe transaction body 必須冪等 — 同樣 input 多次執行結果一致。這是 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">idempotency</a> 在 distributed SQL retry contract 下的具體展開、不是 optional：</p>
<table>
  <thead>
      <tr>
          <th>Transaction body</th>
          <th>是否冪等</th>
          <th>為什麼</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><code>UPDATE balance SET balance = balance - 100</code></td>
          <td>是</td>
          <td>同樣 input 每次都減 100</td>
      </tr>
      <tr>
          <td><code>UPDATE balance SET balance = 900</code></td>
          <td>是</td>
          <td>設成絕對值、retry 不影響</td>
      </tr>
      <tr>
          <td><code>INSERT INTO logs VALUES (...)</code></td>
          <td>否</td>
          <td>retry 後重複寫、要加 UNIQUE constraint</td>
      </tr>
      <tr>
          <td><code>INSERT ON CONFLICT (id) DO NOTHING</code></td>
          <td>是</td>
          <td>用 ON CONFLICT 處理重複</td>
      </tr>
      <tr>
          <td><code>UPDATE counter SET val = val + 1</code></td>
          <td>否（語意問題）</td>
          <td>retry 後加超過預期次數</td>
      </tr>
  </tbody>
</table>
<p>冪等性是 application 設計議題、不是 CockroachDB 配置可解的 — application contract 重塑的核心成本就在這。</p>
<h3 id="rollback-邊界">Rollback 邊界</h3>
<p>transaction 自身有 <code>SAVEPOINT cockroach_restart</code> 邊界、<code>ROLLBACK TO SAVEPOINT</code> 後可重試整個 transaction body。但：</p>
<ul>
<li>commit 後不可回滾 — 業務狀態還原只能新交易補償</li>
<li>application 端如果在 transaction <em>外</em> cache state、retry 後 state 不一致（見失敗模式段）</li>
</ul>
<h2 id="失敗模式">失敗模式</h2>
<h3 id="retry-stormcontention-嚴重時-cpu-雪崩">Retry storm：contention 嚴重時 CPU 雪崩</h3>
<p>當高頻寫入撞同一 row（例：全局 counter、熱門商品 inventory）、serializable 衝突率可能 100%、application 端 retry loop 不斷重跑、CPU 雪崩。</p>
<p>修法：</p>
<ul>
<li>Max retry 上限 + circuit breaker：超過就放棄、回 5xx 給 client、避免 retry storm 拖垮 cluster</li>
<li>改 schema 避開 hot row（partition by region、shard counter、用 sequence 代替全局 counter）</li>
<li>監控 <code>crdb_internal.cluster_contention_events</code>、針對 top-N table 改設計</li>
</ul>
<h3 id="非冪等-transaction-重試double-count">非冪等 transaction 重試：double-count</h3>
<p>最危險的 production bug：transaction body 不是冪等的、retry 後資料重複寫。ledger double-count、payment 重複扣款、log 重複記錄。</p>
<p>修法：</p>
<ul>
<li>transaction body 寫成 <code>UPDATE balance SET balance = balance - X</code>（相對運算）、不寫 <code>UPDATE balance SET balance = Y</code>（絕對賦值依賴 read 結果）</li>
<li><code>INSERT</code> 加 UNIQUE constraint + <code>ON CONFLICT DO NOTHING</code></li>
<li>用 idempotency key（client 帶 UUID、server 端 dedupe）</li>
</ul>
<h3 id="cross-statement-state-假設">Cross-statement state 假設</h3>
<p>application 在 transaction <em>外</em> cache state（例：開 transaction 前 read 一個值、跑 transaction 期間用 cached 值）— retry 從 SAVEPOINT 重來時、cached state 不會重新讀、retry 後 state 不一致。</p>
<p>修法：</p>
<ul>
<li>把 cached state 改成在 transaction 內 read</li>
<li>retry loop 內 reset 所有 cached state</li>
<li>用 closure / scope 限制 cache 的生命週期到 transaction 內</li>
</ul>
<h3 id="hot-row-contention">Hot row contention</h3>
<p>高頻 update 同一 row（例：全局計數器、熱門商品庫存、世界冠軍直播觀眾數）— serializable 衝突率接近 100%、無論 retry 多少次都繼續衝突。</p>
<p>修法（schema-level、不是 application-level）：</p>
<ul>
<li>用 sequence 或 distributed counter（每節點本地 + 定期 aggregate）</li>
<li>partition by hash key、把單一 row 拆成 N 個 sub-row</li>
<li>改 <em>append-only</em> + 定期 aggregate（事件流 + materialized view）</li>
</ul>
<h3 id="改-read-committed-後忘了驗證業務語意">改 READ COMMITTED 後忘了驗證業務語意</h3>
<p>v23.2+ 可改 <code>READ COMMITTED</code>、少 retry 但失去 serializable 保證。對金融 ledger：READ COMMITTED 可能讓 balance 變負（兩個並行 withdraw 都看到 balance=100、都扣 50、結果 balance=-50）。</p>
<p>修法：</p>
<ul>
<li>金融 / 庫存 / 配額這類 <em>strict consistency</em> 場景必須留 SERIALIZABLE</li>
<li>READ COMMITTED 只用在 <em>容忍 stale read</em> 的場景（搜尋結果 / 分析 dashboard）</li>
<li>改 isolation level 前 <em>跑 application audit</em>、確認業務語意能容忍</li>
</ul>
<h3 id="long-running-transactionretry-機率隨時間線性上升">Long-running transaction：retry 機率隨時間線性上升</h3>
<p>transaction read 開始時間早、commit 時 conflict window 大、retry 機率隨 transaction duration 線性上升。</p>
<p>修法：</p>
<ul>
<li>transaction scope 縮小 — 只包必要 read / write、不要把 RPC call / external API 放 transaction 內</li>
<li>kill long-running query（<code>SHOW SESSIONS</code> + <code>CANCEL QUERY</code>）</li>
<li>把 batch update 拆成多個小 transaction、加 idempotency key</li>
</ul>
<h3 id="distributed-deadlock-跟-retry-互動">Distributed deadlock 跟 retry 互動</h3>
<p>CockroachDB 用 distributed deadlock detection（每個 node 維護 wait-for graph、定期跨 node 交換）跟 PostgreSQL local lock 表的 deadlock detection 不同。一般情況下、被 detector 選為 victim 的 transaction 會直接 abort、application retry loop 應該收到 <code>40001</code> 後重跑。但在三種 corner case 下會跟 retry loop 形成雪崩 pattern：</p>
<ul>
<li>多 transaction 同時撞同一組熱 row、deadlock detector 跨節點時間窗有 lag、多個 victim 同時 abort 後同時 retry、撞回同一個 deadlock window</li>
<li>跨節點的 distributed deadlock 偵測週期（預設 200ms+）放大 application retry latency、application 的 retry backoff 沒對齊偵測週期、形成「detect → abort → 快速 retry → 再 deadlock」迴圈</li>
<li>Application 把 deadlock victim 當 <code>40001</code> 直接 retry、不分流出來看、就難以從 metric 區分「serialization conflict retry」跟「distributed deadlock retry」、調 schema / contention 的策略會用錯方向</li>
</ul>
<p>修法（屬通用工程議題、case 未直接揭露）：</p>
<ul>
<li>Retry backoff 至少對齊 distributed deadlock 偵測週期、避免在偵測窗內快速 retry</li>
<li>加 jitter、不同 session 的 retry 不同步</li>
<li>Application metric 分桶記錄 <code>serialization_conflict_retry</code> vs <code>distributed_deadlock_retry</code>、避免 contention 改善方向判錯</li>
<li>Schema 設計階段避免「跨節點熱 row 環形依賴」（例：兩個服務交叉 update 對方的 counter row）</li>
</ul>
<h3 id="跨-case-合成-scope-warningdraftkings-對照">跨 case 合成 Scope warning：DraftKings 對照</h3>
<p>DraftKings ledger 對照 — <strong>DraftKings case 沒寫 PostgreSQL READ COMMITTED retry pattern</strong>、case 內容是「Aurora 內 business sharding 路徑」、用 200 個獨立 cluster 解 Aurora single-primary 撞牆。本章把 DraftKings 拿來當「假想若遷 CockroachDB 需改 SERIALIZABLE + retry loop」的合成對照、不是 case 揭露的 fact。</p>
<p>實際 DraftKings 走 Aurora + application sharding 而非 CockroachDB、所以「DraftKings retry pattern」這個說法本身就是合成 — 應該寫成「DraftKings 走 Aurora sharding 避開 retry contract 重塑、若改走 CockroachDB 則需處理本章描述的 application 改寫」。</p>
<h2 id="容量與觀測">容量與觀測</h2>
<h3 id="必看-metric">必看 metric</h3>
<ul>
<li><code>Transaction retry rate</code>：per table、per session</li>
<li><code>Serialization failure rate</code>：絕對值 + ratio</li>
<li><code>Transaction duration p99</code>：long-running 是 retry 的根因之一</li>
<li><code>Hot ranges by retry count</code>：top contention 來源</li>
<li>Application metric：retry count per request、retry-induced latency p99、circuit breaker trip count</li>
</ul>
<h3 id="容量公式">容量公式</h3>
<ul>
<li>基底 QPS × (1 + avg retry count) = 實際 transaction load</li>
<li>例：1000 QPS、avg retry = 0.3 → 實際 cluster 處理 1300 transaction/s</li>
</ul>
<p>retry rate 是 <em>容量規劃必納入</em> 的變數 — 沒算 retry 就會 underestimate 真實 load。</p>
<h3 id="tuning">Tuning</h3>
<ul>
<li>reduce transaction scope：transaction 越短、conflict window 越小</li>
<li>kill long-running query：transaction 過長要主動截斷</li>
<li>partition hot rows：schema-level 解 hot contention</li>
<li>改 isolation 到 READ COMMITTED（如果業務語意允許）</li>
</ul>
<h3 id="回路徑">回路徑</h3>
<ul>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.5 瓶頸定位流程</a> 判斷 retry-bound vs CPU-bound</li>
<li><a href="/blog/backend/09-performance-capacity/" data-link-title="模組九：效能工程與容量規劃" data-link-desc="把『目前配置能撐多少、要加多少機器』變成可量化、可驗證、可改進的工程流程">9.6 容量規劃模型</a> retry rate × baseline QPS</li>
<li><a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h2 id="邊界與整合">邊界與整合</h2>
<h3 id="sibling-deep-articles">Sibling deep articles</h3>
<ul>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a>：為什麼 serializable 是 distributed SQL 的合理 default</li>
<li><a href="../locality-aware-schema/">locality-aware schema</a>：partition 降低 hot row contention</li>
<li><a href="../survival-goals/">survival goals</a>：cross-region latency 加長 retry window</li>
</ul>
<h3 id="跟-postgresql-對照">跟 PostgreSQL 對照</h3>
<p>PostgreSQL READ COMMITTED 是 default、application 沒 retry loop 是 acceptable。遷 CockroachDB <em>必須</em> 重塑 application transaction contract — 這是 migration 階段最容易 underestimate 的成本。</p>
<p>對應 PostgreSQL MVCC + SSI 機制細節、見 <a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a>。</p>
<h3 id="migration-playbook">Migration playbook</h3>
<p>PG → CockroachDB 的 application audit 必看 transaction shape：</p>
<ul>
<li>每個 transaction 的 read / write set 預估衝突率</li>
<li>是否冪等（retry-safe）</li>
<li>transaction duration（long-running 是 retry 放大器）</li>
<li>業務語意能否容忍 READ COMMITTED（避開 retry 的 fallback）</li>
</ul>
<h3 id="1x-章節互引">1.x 章節互引</h3>
<ul>
<li><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 強一致取捨">1.3 Transaction Boundary</a> 上游 — distributed transaction 邊界</li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a></li>
</ul>
<h3 id="何時不用本文">何時不用本文</h3>
<ul>
<li>純 read-only workload、無 contention</li>
<li>已用 PostgreSQL serializable（application contract 相似、遷移衝擊小）</li>
<li>用 CockroachDB v23.2+ READ COMMITTED 且業務允許 stale read</li>
</ul>
<h2 id="相關連結">相關連結</h2>
<ul>
<li><a href="/blog/backend/01-database/vendors/cockroachdb/" data-link-title="CockroachDB" data-link-desc="分散式 SQL、PostgreSQL 相容、跨區強一致、Spanner 的開源 / 跨雲替代">CockroachDB vendor overview</a></li>
<li><a href="../hlc-raft-consensus/">HLC + Raft consensus</a></li>
<li><a href="/blog/backend/09-performance-capacity/cases/doordash-cockroachdb-orders-platform/" data-link-title="9.C39 DoorDash：Aurora Postgres 寫入瓶頸 → CockroachDB 多主寫入" data-link-desc="DoorDash 從 Aurora Postgres 遷到 CockroachDB、解 1.6 M QPS 單主寫入瓶頸、外送平台爆量壓力下重做 OLTP 拓樸">9.C39 DoorDash</a>（trigger context — PG wire 相容警語）</li>
<li><a href="/blog/backend/09-performance-capacity/cases/draftkings-aurora-financial-ledger/" data-link-title="9.C4 DraftKings：Aurora 撐 100 萬 ops/min 的體育博彩金融帳本" data-link-desc="DraftKings 用 Aurora MySQL 跑體育博彩金融帳本、Super Bowl 流量 &#43;50% 不影響延遲">9.C4 DraftKings</a>（合成對照 — Aurora sharding 路徑）</li>
<li><a href="/blog/backend/01-database/vendors/postgresql/mvcc-lock-model/" data-link-title="PostgreSQL MVCC &#43; Lock Model：為什麼 PG 比 MySQL 少 deadlock、但 vacuum 是別的代價" data-link-desc="PG 用 *MVCC-heavy &#43; 少 explicit lock* 的並行控制、跟 MySQL InnoDB 的 *lock-based*（record / gap / next-key）相反。本文走 MVCC 機制（tuple version &#43; xmin/xmax &#43; visibility）、PG 4 種 lock（row-level / table-level / advisory / predicate）、預測 SERIALIZABLE 行為、5 production 踩雷（idle transaction 卡 vacuum / SELECT FOR UPDATE 跨 transaction / advisory lock 沒釋放 / bloat 不是 vacuum 問題 / predicate lock 在 SSI 下 rollback）、跟 MySQL lock-contention sibling 對比">PostgreSQL MVCC + Lock Model</a></li>
<li><a href="/blog/backend/knowledge-cards/isolation-level/" data-link-title="Isolation Level" data-link-desc="說明資料庫交易隔離級別如何影響並發讀寫結果">isolation level 卡</a> / <a href="/blog/backend/knowledge-cards/transaction-boundary/" data-link-title="Transaction Boundary" data-link-desc="說明哪些資料變更應在同一個交易中一起成功或一起回復">transaction boundary 卡</a></li>
<li>官方：<a href="https://www.cockroachlabs.com/docs/stable/transactions.html">CockroachDB Transactions</a> / <a href="https://www.cockroachlabs.com/docs/stable/transaction-retry-error-reference.html">Transaction Retry Error Reference</a> / <a href="https://www.cockroachlabs.com/docs/stable/read-committed.html">READ COMMITTED v23.2 announcement</a></li>
</ul>
]]></content:encoded></item></channel></rss>