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