🎯 什麼情境該想到我

當一個業務動作要跨多個服務/資料庫、卻無法用單機交易保證「要嘛全成、要嘛全敗」時。

⚙️ 怎麼用(依一致性需求選方案,沒有銀彈)

  1. 可靠訊息佇列(本地訊息表 = Outbox):把業務寫入與事件記錄放同一本地交易,再由 relay 投遞 → 最終一致。最常用、最務實(見 工具-Transactional-Outbox-Pattern)。
  2. TCC(Try-Confirm-Cancel):業務層自己實作預留/確認/取消,強一致但侵入性高、開發成本大。
  3. Saga:把長交易拆成一連串本地交易 + 對應的補償動作,失敗就反向補償。適合長流程。
  4. 2PC/XA:強一致但同步阻塞、協調者單點,效能差,少用於高併發。

先問:這個場景需要強一致,還是最終一致就好?多數選最終一致(1)。

★ 先問三個問題(多數情況根本不需要分散式事務)

① ★ 這兩件事真的必須在「不同的服務/資料庫」嗎?
   → 常常是切錯了邊界。同一個聚合內的東西不該跨服務(見 [[工具-限界上下文]])

② ★ 真的需要「強一致」,還是「最終一致 + 使用者看得到狀態」就好?
   → 「下單後 3 秒庫存才扣」在業務上幾乎總是可以接受的

③ ★ 失敗時可以「人工處理」嗎?
   → 每天 3 筆異常 → 對帳報表 + 人工補償,★ 遠比建一套 Saga 框架划算

★ 最好的分散式事務是「不要有分散式事務」。
大多數「需要分散式事務」的需求,實際上是服務邊界切錯了的症狀。

★ 四種方案的完整對照

方案一致性複雜度可用性★ 什麼時候用
★ Outbox(本地訊息表)最終低高★ 預設選這個。90% 的場景
Saga最終中高長流程、多步驟、有明確的補償動作
TCC準強高中需要「預留資源」的場景(訂票、扣庫存)
2PC / XA強中★ 低幾乎不要用(見下)

★ Outbox:為什麼它是預設答案

❌ 雙寫問題(dual write)
BEGIN; UPDATE orders ...; COMMIT;
kafka.Publish(event)          ← ★ 這裡掛掉 → 訂單建了但沒人知道

✅ Outbox
BEGIN;
  INSERT INTO orders ...;
  INSERT INTO outbox (event) VALUES (...);   ← ★ 同一個本地交易,要嘛都成功要嘛都失敗
COMMIT;
-- 背景 relay(或 CDC 讀 WAL)把 outbox 推到訊息佇列

★ 它的天才之處:把「跨系統的原子性」化約成「單一資料庫的本地交易」,
而單一資料庫的交易是可靠的(見 工具-交易與隔離等級)。

代價:至少一次投遞 → ★ 消費端必須冪等(見 工具-MQ消費端防禦三原則)。

★ Saga:補償的設計才是難點

正向:  建訂單 → 扣款 → 扣庫存 → 出貨
補償:  取消訂單 ← 退款 ← 回補庫存 ← (已出貨無法補償)

三件必須想清楚的事:

問題說明
★ 補償不等於回滾退款 ≠ 沒扣過款(帳上留下兩筆紀錄、可能有手續費)。
Saga 沒有「原子性」,只有「最終達到某個可接受的狀態」
★ 有些步驟不可補償已出貨、已寄出的 email、已呼叫的第三方 API。
→ ★ 把不可逆的步驟排在最後,或改成「可逆的形式」(延遲執行、草稿)
★ 補償本身也會失敗退款 API 也可能掛。→ 補償必須可重試 + 冪等 + 最終有人工介入的出口(dead letter)

編排(orchestration)vs 協同(choreography):

OrchestrationChoreography
流程在哪★ 一個中央協調者(看得到整條流程)散在各服務的事件監聽裡
好處★ 流程可讀、可追蹤、易改鬆耦合,沒有中心
壞處協調者是新的耦合點★ 沒有人知道完整流程長什麼樣
建議★ 步驟 ≥ 3 就用編排只有兩步時可以協同

★ 為什麼 2PC 幾乎不該用

協調者 → 「準備好了嗎?」→ 所有參與者鎖住資源並回「好了」
       ★ 此時協調者掛掉 → 所有參與者「永遠持鎖等待」(in-doubt)
       → 資源被鎖住,無法自行決定,必須等協調者回來
問題說明
★ 協調者單點它掛了,參與者全部卡住
★ 同步阻塞整個 prepare→commit 期間所有參與者持鎖
可用性差任一參與者不可用,整個交易失敗(可用性是各方的乘積)
效能差至少兩輪網路往返 + 持鎖時間長

★ 它在單一資料庫叢集內(同一個 DBMS 的多個分片)還算堪用;跨異質系統基本上是災難。

★ 產品端要配合的事(技術解不了的部分)

最終一致性必須在產品層面被承認,不能偷偷做:

  • ★ 顯示中間狀態:「訂單處理中」而不是假裝已完成
  • ★ 提供查詢入口:使用者能看到目前進度
  • 定義補償後的使用者體驗:退款要通知、要說明原因
  • ★ 對帳機制:定期比對兩邊,找出不一致並告警——
    這是最被忽略但最重要的一環。沒有對帳,你永遠不知道系統有沒有出錯。

🧪 我實際套用的紀錄

  • 2026-07-15:(待填)

⚠️ 注意 / 什麼時候不適用

  • ★★ 先檢討服務邊界。頻繁需要跨服務事務 = 邊界切錯了(見 工具-服務邊界與演進式拆分)。
    合併回同一個服務往往是正確答案,而不是蓋一套 Saga 框架。
  • ★ 一定要有對帳。任何最終一致的系統都會飄移,只是你知不知道。 排定期對帳 + 告警 + 人工修補流程。
  • ★ 消費端冪等不是可選的。所有方案都是「至少一次」投遞。
  • 不可逆的步驟要排在最後,或延遲執行(見 可逆性、審計與人類把關 的 L3 降級技巧)。
  • Saga 的狀態要持久化。流程執行到一半服務重啟 → 要能從中斷處續跑(見 狀態模式)。
  • 不要用「分散式鎖」代替分散式事務。鎖不安全(見 工具-線性一致性與共識 的 fencing token)。
  • ★ 測試很難。要測「第 3 步失敗時補償有沒有正確執行」——故障注入是必要的(見 工具-注入故障與韌性演練)。
  • 監控要能看到「卡住的 Saga」:超過 N 分鐘還沒完成的流程要告警。

🔗 相關工具