<?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>Reconciliation on Tarragon</title><link>https://tarrragon.github.io/blog/tags/reconciliation/</link><description>Recent content in Reconciliation on Tarragon</description><generator>Hugo -- gohugo.io</generator><language>zh-TW</language><copyright>Tarragon (CC BY 4.0)</copyright><lastBuildDate>Wed, 13 May 2026 00:00:00 +0000</lastBuildDate><atom:link href="https://tarrragon.github.io/blog/tags/reconciliation/index.xml" rel="self" type="application/rss+xml"/><item><title>1.9 Reconciliation 與 Data Repair</title><link>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</link><pubDate>Wed, 13 May 2026 00:00:00 +0000</pubDate><guid>https://tarrragon.github.io/blog/backend/01-database/reconciliation-data-repair/</guid><description>&lt;p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。&lt;/p>
&lt;p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。&lt;/p>
&lt;h2 id="reconciliation">Reconciliation&lt;/h2>
&lt;p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。&lt;/p>
&lt;p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。&lt;/p>
&lt;h3 id="對帳系統的設計欄位">對帳系統的設計欄位&lt;/h3>
&lt;p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。&lt;/p>
&lt;p>&lt;strong>來源 A 與來源 B&lt;/strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。&lt;/p>
&lt;p>&lt;strong>比對鍵（comparison key）&lt;/strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。&lt;/p>
&lt;p>&lt;strong>時間窗（time window）&lt;/strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 &lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight&lt;/a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。&lt;/p>
&lt;p>&lt;strong>差異分類規則&lt;/strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。&lt;/p>
&lt;p>&lt;strong>Output schema&lt;/strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 &lt;a href="https://tarrragon.github.io/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&lt;/a> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。&lt;/p>
&lt;h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異&lt;/h3>
&lt;p>兩件事都是「找資料異常」、但本質不同、不能互相替代。&lt;/p>
&lt;p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。&lt;/p>
&lt;p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。&lt;/p>
&lt;p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。&lt;/p>
&lt;h2 id="不一致的三種分類">不一致的三種分類&lt;/h2>
&lt;p>不是所有「資料不一致」都一樣。按 &lt;em>成因&lt;/em> 分三類、各有不同處理策略。&lt;/p>
&lt;h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：replication lag、async event delivery、&lt;a href="https://tarrragon.github.io/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency&lt;/a>&lt;/li>
&lt;li>特徵：兩邊都是「對的」、只是 &lt;em>時間點&lt;/em> 不同&lt;/li>
&lt;li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步&lt;/li>
&lt;li>處理：等待收斂或主動觸發 sync、不必修資料&lt;/li>
&lt;li>持續時間：通常 &amp;lt; 1 秒到分鐘級&lt;/li>
&lt;/ul>
&lt;h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：schema migration 期間、dual-write 失敗、partial write&lt;/li>
&lt;li>特徵：兩邊應該一致但實際不一致、其中一邊是 &lt;em>錯的&lt;/em>&lt;/li>
&lt;li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row&lt;/li>
&lt;li>處理：必須修復、不能等&lt;/li>
&lt;li>持續時間：永久（直到修復）&lt;/li>
&lt;/ul>
&lt;h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）&lt;/h3>
&lt;ul>
&lt;li>來源：業務邏輯 bug、應用層 race condition、人工誤操作&lt;/li>
&lt;li>特徵：資料結構 OK、但 &lt;em>業務語意&lt;/em> 錯&lt;/li>
&lt;li>例：訂單付款狀態是 &lt;code>paid&lt;/code> 但金流端是 &lt;code>refunded&lt;/code>、帳戶餘額跟交易紀錄 sum 不符&lt;/li>
&lt;li>處理：複雜、需要業務判斷哪邊是 source of truth&lt;/li>
&lt;li>持續時間：永久（且容易擴大）&lt;/li>
&lt;/ul>
&lt;p>&lt;strong>處理優先序&lt;/strong>：Semantic &amp;gt; Structural &amp;gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。&lt;/p></description><content:encoded><![CDATA[<p>Reconciliation 與 data repair 的核心責任是把資料錯誤從模糊異常轉成可驗證、可修復、可稽核的流程。進入特定資料庫或 ORM 前、讀者需要先理解資料修復屬於正式狀態責任的一部分。</p>
<p>本章從不一致分類開始、進入偵測模式（連續 vs scheduled）、處理修復策略（auto vs manual）、最後對接 audit trail 跟 backup recovery。讀完後讀者能設計：對帳機制、修復 runbook、evidence handoff、audit chain。</p>
<h2 id="reconciliation">Reconciliation</h2>
<p>Reconciliation 的責任是比較兩個或多個資料來源、確認正式狀態是否與外部事實一致。付款狀態要和金流 provider 對齊、發票狀態要和開票系統對齊、庫存狀態要和出貨或倉儲系統對齊。</p>
<p>對帳需要明確定義資料來源、時間窗、比對鍵、差異分類與 owner。這些欄位能把「資料看起來不一致」轉成可分派、可修復、可驗證的決策材料。</p>
<h3 id="對帳系統的設計欄位">對帳系統的設計欄位</h3>
<p>設計對帳作業時、要先把這幾件事談清楚、再寫 query。少談任何一項、對帳結果都會在事故當下被質疑可信度。</p>
<p><strong>來源 A 與來源 B</strong>：明確指出哪個是內部 source of truth、哪個是外部事實。金流對帳的 A 是訂單表、B 是 provider 結算檔；庫存對帳的 A 是訂單庫存表、B 是倉儲 WMS 報表。兩邊都要有明確 owner、否則差異發生時沒人能解釋為何資料長那樣。</p>
<p><strong>比對鍵（comparison key）</strong>：A 跟 B 要用什麼欄位對齊。最理想是雙方共用的業務 ID（例如金流交易序號）；次優是 timestamp + 業務外鍵組合；最差是用 fuzzy matching（金額 + 時間範圍）、這時對帳結果天然帶有噪音、要在 output schema 標示信心度。</p>
<p><strong>時間窗（time window）</strong>：對帳要對哪段時間的資料、什麼時候做。每日對帳通常設定 T-1 整天、跳過今天（避免 <a href="/blog/backend/knowledge-cards/in-flight/" data-link-title="In-Flight Work" data-link-desc="目前已接收但尚未完成處理的工作量">in-flight</a> 資料）；分鐘級對帳要明確處理 in-flight：是排除最近 N 分鐘、還是允許重複跑直到收斂。在跨時區業務裡、時間窗要對齊雙方 timezone、不然每天差異會穩定出現在 0:00 前後。</p>
<p><strong>差異分類規則</strong>：mismatch 不是只有「不一致」一種。常見要再切：「A 有 B 沒有」（missing in B）、「B 有 A 沒有」（missing in A）、「兩邊都有但欄位不同」（value mismatch）、「同一個 key 在 A 有多筆」（duplicate）。每類差異的處理路徑跟 owner 都不同、不分類會讓修復決策無法分派。</p>
<p><strong>Output schema</strong>：對帳產出的不是「對 / 不對」、而是一份結構化報告。最少要有：mismatch 樣本（不是全部）、總筆數與金額影響、覆蓋率（總共比對了多少筆）、未覆蓋資料（哪些 A 或 B 沒涵蓋）、結果時間戳。這份報告會被 <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> 收進釋出證據鏈、結構不穩定會讓上游 release gate 拒絕採信。</p>
<h3 id="對帳跟-anomaly-detection-的差異">對帳跟 anomaly detection 的差異</h3>
<p>兩件事都是「找資料異常」、但本質不同、不能互相替代。</p>
<p>對帳是 deterministic：給定兩個來源、結果是確定的差異集合、可以被任何工程師重跑驗證。anomaly detection 是 statistical：用模型或閾值判斷一筆資料是否「看起來不對」、結果帶機率、不同模型跑出來不一樣。</p>
<p>在金流、庫存、付款這類正式狀態場景、對帳是必須、anomaly detection 是補充。anomaly detection 適合抓「對帳沒設計到的維度」（突然某 tenant 訂單量爆增）、但不能用它當 source of truth、因為事故時無法回答「為何這筆被判定為異常」。</p>
<p>兩者輸出格式也不同：對帳輸出 mismatch list、anomaly detection 輸出 confidence score。把兩者混在同一份報告會讓 incident reviewer 無法判斷哪些是必修、哪些是可疑。</p>
<h2 id="不一致的三種分類">不一致的三種分類</h2>
<p>不是所有「資料不一致」都一樣。按 <em>成因</em> 分三類、各有不同處理策略。</p>
<h3 id="temporal-inconsistency時間性不一致">Temporal Inconsistency（時間性不一致）</h3>
<ul>
<li>來源：replication lag、async event delivery、<a href="/blog/backend/knowledge-cards/eventual-consistency/" data-link-title="Eventual Consistency" data-link-desc="允許短暫不一致、最終收斂到同一資料狀態的一致性語意">eventual consistency</a></li>
<li>特徵：兩邊都是「對的」、只是 <em>時間點</em> 不同</li>
<li>例：cache 跟 DB 看到不同 value（cache 還沒 invalidate）、replica 跟 primary 不同步</li>
<li>處理：等待收斂或主動觸發 sync、不必修資料</li>
<li>持續時間：通常 &lt; 1 秒到分鐘級</li>
</ul>
<h3 id="structural-inconsistency結構性不一致">Structural Inconsistency（結構性不一致）</h3>
<ul>
<li>來源：schema migration 期間、dual-write 失敗、partial write</li>
<li>特徵：兩邊應該一致但實際不一致、其中一邊是 <em>錯的</em></li>
<li>例：訂單寫進主表但 line items 沒寫、外鍵 reference 一個不存在的 row</li>
<li>處理：必須修復、不能等</li>
<li>持續時間：永久（直到修復）</li>
</ul>
<h3 id="semantic-inconsistency語意不一致">Semantic Inconsistency（語意不一致）</h3>
<ul>
<li>來源：業務邏輯 bug、應用層 race condition、人工誤操作</li>
<li>特徵：資料結構 OK、但 <em>業務語意</em> 錯</li>
<li>例：訂單付款狀態是 <code>paid</code> 但金流端是 <code>refunded</code>、帳戶餘額跟交易紀錄 sum 不符</li>
<li>處理：複雜、需要業務判斷哪邊是 source of truth</li>
<li>持續時間：永久（且容易擴大）</li>
</ul>
<p><strong>處理優先序</strong>：Semantic &gt; Structural &gt; Temporal。Semantic 影響業務最深、Temporal 通常自動收斂。</p>
<h2 id="偵測模式">偵測模式</h2>
<p>不同類型的不一致需要不同偵測模式。</p>
<h3 id="continuous-detection持續偵測">Continuous Detection（持續偵測）</h3>
<ul>
<li>每筆寫入跑 sanity check（trigger、constraint）</li>
<li>應用層 invariant check</li>
<li>適合：structural inconsistency（讓 DB 自己擋）</li>
<li>成本：每筆寫入有 overhead</li>
</ul>
<h3 id="scheduled-detection定期對帳">Scheduled Detection（定期對帳）</h3>
<ul>
<li>每 N 分鐘 / 每天跑對帳 query</li>
<li>跟外部 provider 比對</li>
<li>適合：semantic inconsistency（業務級對齊）</li>
<li>成本：對帳 query 本身耗資源</li>
</ul>
<h3 id="sampling-detection抽樣偵測">Sampling Detection（抽樣偵測）</h3>
<ul>
<li>不跑全表、抽樣 10% / 1% 跑 checksum</li>
<li>適合：大表（全表對帳成本高）</li>
<li>成本：可能漏掉低頻 inconsistency</li>
</ul>
<h3 id="reactive-detection反應式偵測">Reactive Detection（反應式偵測）</h3>
<ul>
<li>用戶 / 客服回報後才查</li>
<li>適合：尾長 inconsistency（找不到通用 pattern）</li>
<li>成本：用戶體驗已受影響</li>
</ul>
<p>對應 <a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a> — migration 期間 <a href="/blog/backend/knowledge-cards/shadow-read/" data-link-title="Shadow Read" data-link-desc="說明正式讀取仍走舊路徑時如何暗中讀新路徑比對結果">shadow read</a> 持續對帳、抓 mapping 規則漂移。</p>
<h2 id="data-repair">Data Repair</h2>
<p>Data repair 的責任是把已確認的資料差異修回正式狀態、並保留修復原因、範圍、證據與回退條件。修復可以是 SQL update、補事件、補發 webhook、重建 projection 或人工客服流程、但每種修復都要有範圍控制。</p>
<p>資料修復要先分成三種：</p>
<table>
  <thead>
      <tr>
          <th>類型</th>
          <th>說明</th>
          <th>常見風險</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>欄位修復</td>
          <td>修正單筆或小批正式欄位</td>
          <td>mapping 規則錯誤會造成二次污染</td>
      </tr>
      <tr>
          <td>派生狀態重建</td>
          <td>重建 index、cache、read model</td>
          <td>可能掩蓋正式狀態尚未修復</td>
      </tr>
      <tr>
          <td>補償動作</td>
          <td>補退款、補發票、補通知</td>
          <td>可能產生重複副作用</td>
      </tr>
  </tbody>
</table>
<p>修復前要先確認問題落在哪一層。正式欄位錯誤要修 source of truth；派生狀態錯誤要重建副本；外部副作用漏做要走補償流程。</p>
<p>欄位修復的判讀重點是 mapping 規則是否正確、因為錯誤規則會把單點差異擴成批次污染。派生狀態重建的判讀重點是 source of truth 是否已經正確、否則重建會複製錯誤。補償動作的判讀重點是副作用是否可逆、因為退款、通知或外部 webhook 可能已經被使用者或第三方看見。</p>
<h2 id="repair-原則">Repair 原則</h2>
<p>不管哪種修復、都遵守三個原則：</p>
<h3 id="1-idempotency冪等">1. Idempotency（冪等）</h3>
<ul>
<li>同樣的修復跑兩次、結果跟跑一次一樣</li>
<li>用 <code>WHERE current_value != target_value</code> 而不是無條件 update</li>
<li>補通知 / webhook 帶 idempotency key、第三方可去重</li>
<li>對應 <a href="/blog/backend/knowledge-cards/idempotency/" data-link-title="Idempotency" data-link-desc="說明同一操作執行多次時如何保持結果一致">Idempotency 卡片</a></li>
</ul>
<h3 id="2-auditable可稽核">2. Auditable（可稽核）</h3>
<ul>
<li>每次修復都有 record：誰、什麼時候、改了什麼、為什麼</li>
<li>修復前 + 修復後的 snapshot 都要存</li>
<li>對應 <a href="/blog/backend/knowledge-cards/audit-log/" data-link-title="Audit Log" data-link-desc="說明高風險操作如何留下可追溯、可稽核的紀錄">Audit Log 卡片</a>、<a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team</a> 的 audit 段</li>
</ul>
<h3 id="3-reversible可逆">3. Reversible（可逆）</h3>
<ul>
<li>萬一修復是錯的、能回退到 before state</li>
<li>不可逆操作（DELETE）必須有 dry-run、必須備份</li>
<li>對應 <a href="/blog/backend/knowledge-cards/rollback-window/" data-link-title="Rollback Window" data-link-desc="說明變更進入 production 後還能用哪種方式回退或改路線的時間與條件">Rollback Window 卡片</a></li>
</ul>
<h2 id="修復前的-dry-run-與-impact-assessment">修復前的 dry-run 與 impact assessment</h2>
<p>修復前要先回答「這次修復會碰多少筆、影響多少業務、最壞情況是什麼」、才能進入執行。直接跑 update 是 production-grade 流程的反例、即使在 incident 壓力下也不能跳過這步。</p>
<p><strong>Dry-run 的責任</strong>：把 update 改成 select、用同樣的 WHERE 條件、產出將被修改的資料樣本。Dry-run 結果要包含：影響筆數總計、影響金額或業務值（如果有）、affected tenant / user list 的抽樣、未涵蓋的邊界 case。Dry-run 跟正式修復必須共用 mapping 規則、否則 dry-run 結果無法當審核依據。</p>
<p><strong>規模分級的執行策略</strong>：影響筆數會決定執行方式。</p>
<ul>
<li><strong>單筆到十筆</strong>：客服等級的修復、一名工程師執行 + 一名同儕審核 + audit log 即可。</li>
<li><strong>百筆到千筆</strong>：要在低流量時段執行、分批跑、每批跑完比對 invariant、發現意外停下。</li>
<li><strong>萬筆以上</strong>：當成 production deploy 處理、要有 deploy review、staged rollout（先 1% tenant、再 10%、再全量）、跟 oncall 同步。</li>
<li><strong>跨表 / 跨 service</strong>：必須先做跨團隊 review、確認下游依賴（cache、search index、外部 webhook）的處理計畫、不能單一團隊獨自決定。</li>
</ul>
<p><strong>Impact assessment 的必看欄位</strong>：除了筆數、還要看 <em>連帶影響</em>。修復 orders 表會不會觸發 audit trigger 把每筆寫進 audit log 表？會不會觸發 outbox event 把每筆當成新事件對外發布？會不會讓某 tenant 的 metric 一次性異常、誤觸 alert？這些 second-order effect 在 dry-run 階段就要識別、否則修復本身會變成新事故。</p>
<p><strong>Sandbox / staging 驗證</strong>：不可逆或大規模修復、先在 staging 跑一次、確認 query plan、執行時間、lock 行為。Production 規模沒辦法在 staging 重現的話、至少要在 production 的某個低風險 tenant / region 先試跑、再擴大。</p>
<p><strong>Approval gate（4-eyes process）</strong>：超出單筆規模或修復金錢、權限、個資的場合、必須 <em>兩位以上人員</em> 各自看過 dry-run 結果再簽核。常見實作是：執行者提 PR / ticket 帶 dry-run output、reviewer 簽核後才能執行、執行後產出 audit log 帶兩人簽核紀錄。Reviewer 的責任不是橡皮圖章、是獨立驗證 dry-run 結果跟 incident 描述一致。</p>
<h2 id="repair-patterns">Repair Patterns</h2>
<p>實務上常見的 repair pattern：</p>
<h3 id="pattern-1條件式-update">Pattern 1：條件式 UPDATE</h3>
<p>最簡單也最安全的修復。</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">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w">
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="w"></span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;paid&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><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">12345</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;pending&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">5</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">payment_id</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;abc&#39;</span><span class="p">;</span></span></span></code></pre></div><p><code>AND</code> 條件確保只在 <em>當前狀態符合預期</em> 時才改、避免 race condition。</p>
<h3 id="pattern-2批次修復--節流">Pattern 2：批次修復 + 節流</h3>
<p>大量資料修復、必須節流避免影響 production。</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="c1">-- 每批 100 筆、間隔 1 秒
</span></span></span><span class="line"><span class="ln">2</span><span class="cl"><span class="c1"></span><span class="k">UPDATE</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">SET</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;fixed&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">3</span><span class="cl"><span class="w"></span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w">
</span></span></span><span class="line"><span class="ln">4</span><span class="cl"><span class="w">  </span><span class="k">AND</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">IN</span><span class="w"> </span><span class="p">(</span><span class="k">SELECT</span><span class="w"> </span><span class="n">id</span><span class="w"> </span><span class="k">FROM</span><span class="w"> </span><span class="n">orders</span><span class="w"> </span><span class="k">WHERE</span><span class="w"> </span><span class="n">status</span><span class="w"> </span><span class="o">=</span><span class="w"> </span><span class="s1">&#39;broken&#39;</span><span class="w"> </span><span class="k">LIMIT</span><span class="w"> </span><span class="mi">100</span><span class="p">);</span></span></span></code></pre></div><p>對應 <a href="/blog/backend/knowledge-cards/backfill/" data-link-title="Backfill" data-link-desc="說明如何為既有資料補上新欄位、新索引或新衍生狀態">Backfill 卡片</a> — backfill 跟 batch repair 是同類技術。</p>
<h3 id="pattern-3補事件--補-webhook">Pattern 3：補事件 / 補 webhook</h3>
<p>外部副作用漏做時、補發事件。</p>
<ul>
<li>必須帶 idempotency key（third-party 才能去重）</li>
<li>紀錄補發原因（incident report 連結）</li>
<li>注意：補發前確認 third-party 是否真的沒收到</li>
</ul>
<h3 id="pattern-4重建-derived-state">Pattern 4：重建 derived state</h3>
<p>cache 跟 search index 是 derived state、出錯通常 <em>砍掉重建</em>。</p>
<ul>
<li>不是直接修 cache value、是 invalidate 讓下次 read 重算</li>
<li>大規模重建用 batch job 跑、避免 thundering herd</li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/tubi-elasticache-ml-feature-store/" data-link-title="9.C25 Tubi：從 ScyllaDB 遷到 ElastiCache、ML feature store 達 sub-10ms p99" data-link-desc="Tubi 把 ML 推薦的 feature store 從 ScyllaDB 遷到 ElastiCache for Redis、99 百分位延遲降到 10ms 以下">9.C25 Tubi</a> feature store 重建模式</li>
</ul>
<h3 id="pattern-5point-in-time-recovery">Pattern 5：Point-in-time Recovery</h3>
<p>當資料 <em>損毀且無法重建</em> 時、靠 backup recovery。</p>
<ul>
<li>PostgreSQL：WAL + base backup → PITR</li>
<li>MySQL：binlog + snapshot → PITR</li>
<li>Aurora：cluster snapshot + continuous backup</li>
<li>注意：recovery 期間可能要 <em>整個 DB restore</em>、影響範圍大</li>
</ul>
<h2 id="repair-runbook">Repair Runbook</h2>
<p>Repair runbook 的責任是讓資料修復可重複執行、並降低對當下工程師記憶的依賴。最小 runbook 需要包含：</p>
<ol>
<li>差異查詢與 query link</li>
<li>影響範圍與 tenant / region / time range</li>
<li>修復方式與 dry-run 結果</li>
<li>審核 owner 與執行 owner</li>
<li>rollback condition 與後續 validation query</li>
</ol>
<p>runbook 要和 <a href="/blog/backend/knowledge-cards/validation-query/" data-link-title="Validation Query" data-link-desc="說明遷移、回填與修復期間如何用查詢證明資料語意是否一致">validation query</a> 共用語意。若查詢與修復程式用不同 mapping 規則、修復結果就難以被同一份 evidence 驗證。</p>
<h2 id="audit-與權限邊界">Audit 與權限邊界</h2>
<p>Data repair 常常需要高權限、因此必須接到 audit 與資料保護邊界。修復個資、付款、權限或方案資料時、要保留操作者、審核者、查詢範圍、寫入範圍與修復前後樣本。</p>
<p><strong>Audit log 必要欄位</strong>：</p>
<ul>
<li>timestamp（操作時間）</li>
<li>actor（誰執行）</li>
<li>reviewer（誰審核、如果是 4-eyes process）</li>
<li>query（執行了什麼 SQL / API call）</li>
<li>before / after snapshot（值的變化）</li>
<li>reason（為什麼做這次修復、incident ID）</li>
<li>rollback path（如何回退）</li>
</ul>
<p>這裡要接到 <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">7.7 Audit Trail 與 Accountability Boundary</a>。資料修復同時是可靠性、資安與合規問題。</p>
<h3 id="權限分離與憑證時效">權限分離與憑證時效</h3>
<p>修復權限不該是常駐權限。日常開發 / SRE 帳號只該有 read-only、修復需要時才透過 break-glass 流程申請臨時 write 權限。</p>
<p>常見實作：</p>
<ul>
<li><strong>角色分離</strong>：reviewer 跟 executor 是不同帳號、reviewer 不能執行、executor 不能 self-approve。系統強制檢查兩個帳號不同、避免一人偽造另一身分。</li>
<li><strong>時效性憑證</strong>：申請 write 權限時帶 expiry（30 分鐘 / 2 小時）、過期自動回收。不是「給了就一直有」、避免遺留高權限帳號變成攻擊面。</li>
<li><strong>範圍限定</strong>：申請時要指定哪張表、哪個 tenant / region。粒度不細的話、一次申請就拿到全 production write、超出實際需求。</li>
<li><strong>同步 alert</strong>：高權限被啟用要同步發 alert 到 security channel、給 security team reviewer 看見。事後若 audit log 跟 alert 對不上、表示權限被繞過。</li>
</ul>
<p>對應 <a href="/blog/backend/07-security-data-protection/identity-access-boundary/" data-link-title="7.2 身分與授權邊界" data-link-desc="以問題驅動方式整理身分、授權、會話與供應商身分鏈">Identity Access Boundary</a> 跟 <a href="/blog/backend/07-security-data-protection/secrets-and-machine-credential-governance/" data-link-title="7.6 秘密管理與機器憑證治理" data-link-desc="以問題驅動方式整理 secret、token、key 與機器身份治理">Secrets and Machine Credential Governance</a>。修復權限管理跟 incident-time 緊急存取是同一套機制、不該各做各的。</p>
<h2 id="跨服務--跨組織的對帳責任">跨服務 / 跨組織的對帳責任</h2>
<p>當對帳跨團隊、跨子系統、跨外部 provider 時、責任不清是首要失敗模式。對帳結果在組織邊界 <em>穿越</em> 時、要明確標記每段的 owner、否則 mismatch 出現後、所有相關方都會說「不是我們的問題」。</p>
<p><strong>跨服務對帳的責任切分</strong>：</p>
<ul>
<li><strong>資料 owner</strong>：誰擁有那張表 / 那組欄位、誰負責解釋為何資料長那樣。資料 owner 通常是寫入該表的服務團隊。</li>
<li><strong>對帳作業 owner</strong>：誰負責定義 reconciliation query、跑、看結果。可能跟資料 owner 是不同人（例如平台團隊跑對帳、業務團隊擁有資料）。</li>
<li><strong>差異處理 owner</strong>：mismatch 出現後、誰負責決定修復策略。通常跟資料 owner 一致、但跨團隊 mismatch 要先約定誰主導。</li>
<li><strong>修復執行 owner</strong>：實際下 SQL / call API 的人。可能跟差異處理 owner 不同（後者決策、前者執行）。</li>
</ul>
<p>四個 owner 在簡單場景可以是同一人、在複雜跨團隊場景必須清楚分派。AGENTS.md 規範優先序段的「明確 owner」原則在這裡指的是 <em>對每一段流程</em> 都有人能簽收、不是只指對帳這件事整體有 owner。</p>
<p><strong>跨組織對帳的特殊問題</strong>：跟外部 provider（金流、物流、SaaS supplier）對帳時、對方不見得會接受你的對帳結果、也不見得會給差異列表。常見處理：</p>
<ul>
<li>自己跑兩份對帳：A vs provider report（每天）、A vs provider API（即時抽樣）、兩份結果不同代表 provider report 本身有問題。</li>
<li>約定差異仲裁流程：簽 SLA 時就寫清楚、mismatch 出現後雙方各保留多久的資料、誰先給對方檢視。</li>
<li>不能依賴 provider 修：金流 provider 通常只負責對帳、不負責修你的 DB。修復永遠是你方責任。</li>
</ul>
<h2 id="跟-backup--pitr-整合">跟 Backup / PITR 整合</h2>
<p>備份的 <em>權限獨立性</em> 跟 <em>attack surface</em> 屬於 <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">1.5 Red Team 備份段</a> — 本段聚焦 <em>recovery</em> 角度的資料修復責任。兩者互補：1.5 解決「備份本身怎麼防被攻擊」、本段解決「事故後怎麼用備份回復」。</p>
<p>當修復必須跨越「point in time」時、需要 backup 配合。</p>
<h3 id="snapshot-based-recovery">Snapshot-based recovery</h3>
<ul>
<li>整個 cluster 從 N 小時前的 snapshot 還原</li>
<li>影響：所有 <em>其他</em> 資料也回到那個時間點</li>
<li>適合：catastrophic data corruption</li>
</ul>
<h3 id="pitrpoint-in-time-recovery">PITR（Point-in-Time Recovery）</h3>
<ul>
<li>snapshot + WAL / binlog replay 到指定時間</li>
<li>影響：只在指定時間點 stop replay</li>
<li>適合：「3 小時前 admin 誤刪一張表」這類精準回放</li>
</ul>
<h3 id="logical-backupmysqldump--pg_dump">Logical backup（mysqldump / pg_dump）</h3>
<ul>
<li>整個 schema + data 的 SQL script</li>
<li>適合：跨環境遷移、特定表回復、小規模修復</li>
</ul>
<h3 id="continuous-archive">Continuous archive</h3>
<ul>
<li>WAL / binlog 持續備份到 S3 / GCS</li>
<li>一直可以回放到 <em>任何時間點</em></li>
<li>對應 <a href="/blog/backend/09-performance-capacity/cases/genesys-dynamodb-99999-availability/" data-link-title="9.C24 Genesys：用 DynamoDB 在 15 region 跑出 99.999% 可用性" data-link-desc="Genesys 客服平台用 DynamoDB 為預設資料層、跨 15 主 region &#43; 5 衛星 region、達成 12 個月 99.999% 可用性">9.C24 Genesys 99.999%</a> — 高可用需要快速 PITR</li>
</ul>
<h3 id="recovery-時的對抗壓力">Recovery 時的對抗壓力</h3>
<p>PITR / snapshot recovery 不是純技術問題、會在事故當下面對「為了快、要不要跳檢查」的取捨。對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/vmware-esxiargs-2023-ransomware-recovery-pressure/" data-link-title="7.R7.4.5 VMware ESXiArgs 2023：虛擬化平台勒索回復壓力" data-link-desc="虛擬化平台漏洞被利用後，回復策略與營運連續性會面臨同步壓力">VMware ESXiArgs 2023 ransomware recovery pressure</a> — 虛擬化平台勒索後、團隊在 <em>營運壓力</em> 跟 <em>資料可信度</em> 之間擺盪：snapshot 是否乾淨、回復後資料是否被污染、跳過 integrity check 換 RTO 是否可接受。判讀重點：recovery 流程要事前 <em>演練</em> 過、否則事故當下不知道要 verify 什麼、容易在壓力下接受被污染的 backup。對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a>、事故當下的取捨要寫進 decision log。</p>
<h3 id="rtorpo-跟業務可接受中斷的對照表">RTO/RPO 跟業務可接受中斷的對照表</h3>
<p>業務可接受中斷時間是 RTO/RPO 的判讀對照基準。RTO（Recovery Time Objective、多久能恢復）跟 RPO（Recovery Point Objective、最多丟多少資料）是技術指標、要對照業務側的可接受上限才能判斷夠不夠。常見錯誤是把 RTO/RPO 訂在「技術上能做到的最佳值」、忽略業務實際的容忍範圍。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「定義核心流程的 RTO / RPO、讓資料修復時間跟業務可接受中斷時間明示對照、不藏在直覺」。事故當下發現「DB 能 2 小時恢復、但業務只能容忍 30 分鐘中斷」、來不及補救。</p>
<p><strong>對照表設計</strong>：</p>
<table>
  <thead>
      <tr>
          <th>業務流程</th>
          <th>RTO（技術）</th>
          <th>業務可接受中斷</th>
          <th>落差處理</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>用戶登入</td>
          <td>30 分鐘</td>
          <td>5 分鐘</td>
          <td>加 standby region failover</td>
      </tr>
      <tr>
          <td>訂單寫入</td>
          <td>1 小時</td>
          <td>30 分鐘</td>
          <td>加 outbox + replay</td>
      </tr>
      <tr>
          <td>報表查詢</td>
          <td>4 小時</td>
          <td>1 天</td>
          <td>RTO 充裕、不需投資</td>
      </tr>
      <tr>
          <td>對帳 batch</td>
          <td>8 小時</td>
          <td>3 天</td>
          <td>RTO 充裕</td>
      </tr>
      <tr>
          <td>付款</td>
          <td>1 小時</td>
          <td>0（不能停）</td>
          <td>必須 active-active</td>
      </tr>
  </tbody>
</table>
<p><strong>關鍵情境延伸</strong>：</p>
<ul>
<li><strong>付款（必須 active-active）</strong>：業務可接受中斷為 0、單一 region failover 都不能用（failover 期間用戶看到失敗）、必須多 region 同時寫入、靠 Aurora DSQL / Spanner / Cosmos DB multi-region write 撐。設計權衡是 <em>跨 region 寫入延遲</em> 跟 <em>對帳一致性的特殊處理</em>（同一筆款項可能在兩個 region 各被處理一次、要靠 idempotency key 去重）。詳見 <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><strong>訂單寫入（outbox + replay）</strong>：30 分鐘容忍區間夠用 outbox pattern — 訂單寫進 DB 同步寫進 outbox table、async worker 把 outbox event 推下游。即使下游中斷、訂單本身已落地、event 可在恢復後 replay。設計權衡是 outbox table 的儲存成本跟 replay 邏輯的冪等性、跟 <a href="/blog/backend/03-message-queue/" data-link-title="模組三：訊息佇列與事件傳遞" data-link-desc="整理 durable queue、broker、retry、outbox 與 idempotency 的後端實務">03 訊息佇列模組</a> 的 outbox pattern 整合。</li>
<li><strong>用戶登入（standby region failover）</strong>：5 分鐘容忍意味 <em>自動 failover</em> 必須在這時間內完成、人類介入做不到、要靠 DNS health check + Route 53 / Cloudflare 自動切流。權衡是 standby region 平時付閒置成本、跟 active-active 比、便宜但 failover 時有 1-3 分鐘延遲跟 cache miss。</li>
</ul>
<p>落差是 <em>投資訊號</em>、不是「忽略它」。RTO &gt; 業務容忍時、要嘛降 RTO（加 HA / DR 投資）、要嘛跟業務協商提高容忍（通常不接受）。</p>
<p>判讀重點：對照表要每年 review。業務模式變了（例如從 B2C 變 B2B 客服 SaaS）、容忍時間會大幅縮短、RTO 必須跟著降。</p>
<h2 id="事故角色預定義">事故角色預定義</h2>
<p>DB 事故當下、<em>資安處置</em> 跟 <em>業務連續性處置</em> 要 <em>分軌並行</em>、不是線性執行。這要求事先有 dual-track IC（Incident Command）角色、不是事故當下臨時拉人。</p>
<p>對應 <a href="/blog/backend/07-security-data-protection/red-team/cases/data-exfiltration/change-healthcare-2024-ops-impact/" data-link-title="7.R7.4.3 Change Healthcare 2024：資料事件轉為營運中斷" data-link-desc="醫療支付中樞事件如何同時衝擊資料安全與業務連續性">Change Healthcare 2024</a> — 「技術處置與業務處置分軌並行的前提是事先有 dual-track IC 角色」。沒事先定義、事故當下會出現「資安 team 在隔離系統、business team 在喊客戶等不及」、兩條軌道互相干擾。</p>
<p><strong>Dual-track IC 角色定義</strong>（以下為通用 IC 模型、非案例直接揭露；具體角色細分視組織規模調整）：</p>
<table>
  <thead>
      <tr>
          <th>軌道</th>
          <th>角色</th>
          <th>責任</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>技術軌道</td>
          <td>Tech IC</td>
          <td>漏洞修補、系統恢復、技術決策（rollback / restart 等）</td>
      </tr>
      <tr>
          <td>業務軌道</td>
          <td>Business IC</td>
          <td>客戶溝通、降級流程啟動、合規通報、業務 fallback</td>
      </tr>
      <tr>
          <td>協調軌道</td>
          <td>Overall IC</td>
          <td>兩條軌道協調、跨軌道決策、對外發言</td>
      </tr>
      <tr>
          <td>資料軌道</td>
          <td>Data IC</td>
          <td>資料完整性驗證、修復決策、audit chain</td>
      </tr>
      <tr>
          <td>Comms 軌道</td>
          <td>Communications Lead</td>
          <td>內部通報、外部公告、media 應對</td>
      </tr>
  </tbody>
</table>
<p><strong>Overall IC 跟一般技術 IC 的差異</strong>：一般 IC 主要在技術軌道內決策（要不要 rollback、要不要重啟）；Overall IC 額外承擔 <em>跨軌道仲裁</em> 責任 — 當 Tech IC 想停服務止血、Business IC 想保服務維持收入、兩者衝突時、由 Overall IC 拍板。這個角色需要對技術跟業務都有足夠理解、不能只懂一邊；通常由高階工程主管或 CTO/VP Eng 兼任、不是輪值的 oncall。</p>
<p><strong>Data IC 的特殊角色</strong>：跟其他軌道相比、Data IC 的決策時間軸最長 — 技術修復可能 1 小時完成、但 <em>資料是否被污染、要不要 PITR、PITR 到哪個時間點</em> 可能要 24-72 小時驗證。Data IC 不能被 Tech IC 跟 Business IC 的「快快上線」壓力推動、必須有獨立判斷權。實務上常見的失誤是讓 Tech IC 兼任 Data IC、結果為了 RTO 跳過 integrity check、事後發現資料污染擴大。</p>
<p><strong>事先準備</strong>：</p>
<ul>
<li><strong>Primary + backup 雙人配置</strong>：每個角色都要有 primary + backup、避免單人不可用（休假、生病、被另一事故占住）讓事故當下卡住。實務上要有 <em>指定流程</em> 而非「臨時找誰」、避免事故當下浪費 30 分鐘喬人。</li>
<li><strong>責任寫進 runbook</strong>：runbook 要列出每個角色該做什麼決策、不該做什麼決策（避免越權）。事故當下查職位、會在最壓力大的時候做組織決策、出錯機會高。</li>
<li><strong>定期 tabletop 演練</strong>：演練的重點不是「技術修復對不對」、是「角色交接是否流暢」。Overall IC 跟 Tech IC 之間的權限邊界、Data IC 何時介入、Comms Lead 何時對外發言、都要在演練中試出來。</li>
<li><strong>跨時區 follow-the-sun 輪值</strong>：B2B SaaS 跟全球業務、事故不分時區、要有 24/7 覆蓋。單一時區團隊在事故發生在凌晨時、人力不足或反應慢、會放大事故代價。</li>
</ul>
<p>判讀重點：DB 事故不只是技術事件、會成為 <em>跨多軌道</em> 的事件。角色預定義是組織能力、不是技術能力、但缺它會放大技術事故的代價。</p>
<p>對應 <a href="/blog/backend/08-incident-response/incident-decision-log/" data-link-title="8.19 Incident Decision Log" data-link-desc="把事中假設、決策、證據、回退條件與責任人留下可復盤紀錄">8.5 Incident Decision Log</a> 跟 <a href="/blog/backend/07-security-data-protection/security-routing-from-case-to-service/" data-link-title="7.8 模組路由：問題到服務實作" data-link-desc="整理問題節點如何路由到部署、可靠性與事故處理章節">7.13 Security Routing</a> — 角色預定義是這些跨模組工作的前置。</p>
<h2 id="evidence-handoff">Evidence Handoff</h2>
<p>資料修復的 evidence handoff 要能支援 release gate 與 incident review。</p>
<table>
  <thead>
      <tr>
          <th>欄位</th>
          <th>內容</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>Source</td>
          <td>reconciliation query、provider report、audit log</td>
      </tr>
      <tr>
          <td>Time range</td>
          <td>差異發生窗口與修復窗口</td>
      </tr>
      <tr>
          <td>Query link</td>
          <td>mismatch sample、修復前後驗證</td>
      </tr>
      <tr>
          <td>Owner</td>
          <td>data owner、service owner、reviewer</td>
      </tr>
      <tr>
          <td>Data quality</td>
          <td>抽樣覆蓋率、延遲、未覆蓋資料</td>
      </tr>
      <tr>
          <td>Known gap</td>
          <td>尚未確認的 provider callback、低流量 tenant</td>
      </tr>
  </tbody>
</table>
<p>這份 handoff 要進入 <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/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">8.22 Incident Evidence Write-back</a>。</p>
<h2 id="判讀訊號">判讀訊號</h2>
<table>
  <thead>
      <tr>
          <th>訊號</th>
          <th>判讀重點</th>
          <th>對應動作</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td>對帳差異率持續上升</td>
          <td>上游邏輯有 bug、或時間窗對齊問題</td>
          <td>修上游 + 確認對帳時間窗</td>
      </tr>
      <tr>
          <td>同筆資料對帳 run-to-run 結果不同</td>
          <td>對帳 query 沒處理 in-flight 資料邊界</td>
          <td>排除最近 N 分鐘、或允許收斂多跑幾次</td>
      </tr>
      <tr>
          <td>修復後不一致再次出現</td>
          <td>沒修根因、只修了 symptom</td>
          <td>找根因、增加 invariant check</td>
      </tr>
      <tr>
          <td>修復影響超出預期範圍</td>
          <td>mapping 規則錯誤、二次污染</td>
          <td>立即停止修復、回退</td>
      </tr>
      <tr>
          <td>修復沒 dry-run 直接執行</td>
          <td>流程違規、事後無法佐證影響範圍</td>
          <td>事後 audit、把 dry-run 列入 gate</td>
      </tr>
      <tr>
          <td>Recovery 後 derived state 仍錯</td>
          <td>重建 derived 時 source 還沒修</td>
          <td>先修 source、再重建 derived</td>
      </tr>
      <tr>
          <td>Audit log 缺欄位</td>
          <td>事故時無法追究、難 rollback</td>
          <td>補 audit schema、加 reviewer 欄位</td>
      </tr>
      <tr>
          <td>高權限帳號在非 incident 時段啟用</td>
          <td>可能誤用或攻擊面、break-glass 沒回收</td>
          <td>立刻檢查 audit log、回收憑證</td>
      </tr>
      <tr>
          <td>跨服務 mismatch、各方都推卸</td>
          <td>對帳 owner 沒分派、責任空白</td>
          <td>補資料 owner / 對帳 owner / 執行 owner</td>
      </tr>
      <tr>
          <td>anomaly alert 跟對帳 mismatch 混報</td>
          <td>兩種訊號性質不同、reviewer 無法判讀</td>
          <td>拆 dashboard、deterministic 跟 statistical 分開</td>
      </tr>
  </tbody>
</table>
<h2 id="常見誤區">常見誤區</h2>
<p>把對帳當成「定期 batch job」、不關心 <em>當下不一致</em>。實時對帳跟 batch 對帳是 <em>不同工具</em>、不能互相替代。</p>
<p>把資料修復當成「一個工程師動手改」、沒 audit、沒 review、沒 rollback。資料修復本質是 production 操作、跟 deploy 同等嚴格。</p>
<p>把 PITR 當成 <em>常規修復工具</em>。PITR 影響大、適合 catastrophic event、不適合單筆資料修復。</p>
<p>把 derived state 不一致跟 canonical state 不一致 <em>混在一起</em> 處理。derived 是 <em>再生</em> 的、canonical 是 <em>永久</em> 的、處理流程完全不同。</p>
<p>把對帳結果跟 anomaly detection 結果放同一份報告。前者是 deterministic、後者是 statistical、混報會讓 incident reviewer 無法判斷必修跟可疑。對帳 mismatch 要有獨立追蹤面板、anomaly 走另一條路徑。</p>
<p>跳過 dry-run、直接 update。即使單筆修復、也要先 select 看到當前 row、確認 WHERE 條件命中預期。incident 壓力下尤其容易跳、結果反而把單點問題擴成批次污染。</p>
<p>把修復權限當常駐權限發放。長期 write 權限放在工程師帳號上、會在事故無關時段被誤用、且事後無法區分「正常工作」跟「非法修復」。修復權限要時效化、申請即用即收。</p>
<h2 id="案例對照">案例對照</h2>
<table>
  <thead>
      <tr>
          <th>案例</th>
          <th>reconciliation 重點</th>
      </tr>
  </thead>
  <tbody>
      <tr>
          <td><a href="/blog/backend/09-performance-capacity/cases/zomato-tidb-to-dynamodb-migration/" data-link-title="9.C20 Zomato：從 TiDB 遷移到 DynamoDB、吞吐 4 倍、延遲降 90%、成本減 50%" data-link-desc="Zomato 帳單系統從 TiDB 遷移到 DynamoDB、吞吐 2K→8K RPM、延遲降 90%、成本減 50%">9.C20 Zomato</a></td>
          <td>migration 期間用 shadow read 持續對帳</td>
      </tr>
      <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</a></td>
          <td>體育博彩 ledger、結算後對帳</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>跨市場銀行、每市場獨立對帳</td>
      </tr>
  </tbody>
</table>
<h2 id="實體服務討論承接點">實體服務討論承接點</h2>
<p>實體資料庫文章要承接本篇的 reconciliation 與 data repair 責任。PostgreSQL、MySQL、MSSQL 或其他資料庫的差異、應放在它們如何產生 validation query、保留 audit trail、支援 point-in-time recovery、處理 replica lag 與控制修復權限。</p>
<p>若服務需要高頻對帳、後續文章要比較查詢成本、索引策略與 replica 讀取延遲。若服務需要高風險資料修復、後續文章要比較 transaction log、backup/restore、row-level audit 與權限分離。若服務需要跨系統補償、後續文章要把資料庫能力接到 queue replay 與 incident decision log。</p>
<h2 id="跨模組路由">跨模組路由</h2>
<ol>
<li>與 1.3 的交接：transaction boundary 決定哪些不一致可避免 — <a href="/blog/backend/01-database/transaction-boundary/" data-link-title="1.3 Transaction 與一致性邊界" data-link-desc="交易邊界、isolation level、retry 策略、distributed transaction（2PC、Saga）與跨 region 強一致取捨">Transaction Boundary</a></li>
<li>與 1.5 的交接：audit 跟 access control — <a href="/blog/backend/01-database/red-team-data-layer/" data-link-title="1.5 攻擊者視角（紅隊）：資料層弱點判讀" data-link-desc="從資料存取邊界、外洩路徑與修復代價、盤點 database 的主要弱點">Red Team Data Layer</a></li>
<li>與 1.7 的交接：migration 後驗證 — <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 Evidence</a></li>
<li>與 1.8 的交接：canonical vs derived 是修復的前置 — <a href="/blog/backend/01-database/state-ownership-query-boundary/" data-link-title="1.8 State Ownership 與 Query Boundary" data-link-desc="正式狀態 vs 派生狀態的責任分層、CQRS / event sourcing / materialized view、四種 query 邊界">State Ownership</a></li>
<li>與 3.8 的交接：消息重放與補事件 — <a href="/blog/backend/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">Queue Consumer Retry / Replay</a></li>
<li>與 4.20 的交接：evidence handoff — <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>與 7.7 的交接：audit trail — <a href="/blog/backend/07-security-data-protection/audit-trail-and-accountability-boundary/" data-link-title="7.7 稽核追蹤與責任邊界" data-link-desc="以問題驅動方式整理高風險操作追蹤、可回查與責任切分">Audit Trail and Accountability Boundary</a></li>
<li>與 8.22 的交接：incident evidence write-back — <a href="/blog/backend/08-incident-response/incident-evidence-write-back/" data-link-title="8.22 Incident Evidence Write-back" data-link-desc="把事故證據、決策與復盤結論回寫到 observability、reliability 與 runbook">Incident Evidence Write-back</a></li>
</ol>
<h2 id="下一步路由">下一步路由</h2>
<p>要處理 migration 造成的資料差異、接著讀 <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/03-message-queue/queue-consumer-retry-replay-handoff/" data-link-title="3.8 Queue Consumer Retry 與 Replay Handoff（實作示範）" data-link-desc="以 order_created consumer 示範 queue 路徑如何交付 idempotency evidence、DLQ handling、replay runbook 與 incident decision log。">3.8 Queue Consumer Retry 與 Replay Handoff</a>。要設計跨服務 reconciliation 跟 saga compensation、接著讀 <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> 的 Saga 段。</p>
]]></content:encoded></item></channel></rss>