🎯 什麼情境該想到我
當一個業務動作要跨多個服務/資料庫、卻無法用單機交易保證「要嘛全成、要嘛全敗」時。
⚙️ 怎麼用(依一致性需求選方案,沒有銀彈)
- 可靠訊息佇列(本地訊息表 = Outbox):把業務寫入與事件記錄放同一本地交易,再由 relay 投遞 → 最終一致。最常用、最務實(見 工具-Transactional-Outbox-Pattern)。
- TCC(Try-Confirm-Cancel):業務層自己實作預留/確認/取消,強一致但侵入性高、開發成本大。
- Saga:把長交易拆成一連串本地交易 + 對應的補償動作,失敗就反向補償。適合長流程。
- 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):
| Orchestration | Choreography | |
|---|---|---|
| 流程在哪 | ★ 一個中央協調者(看得到整條流程) | 散在各服務的事件監聽裡 |
| 好處 | ★ 流程可讀、可追蹤、易改 | 鬆耦合,沒有中心 |
| 壞處 | 協調者是新的耦合點 | ★ 沒有人知道完整流程長什麼樣 |
| 建議 | ★ 步驟 ≥ 3 就用編排 | 只有兩步時可以協同 |
★ 為什麼 2PC 幾乎不該用
協調者 → 「準備好了嗎?」→ 所有參與者鎖住資源並回「好了」
★ 此時協調者掛掉 → 所有參與者「永遠持鎖等待」(in-doubt)
→ 資源被鎖住,無法自行決定,必須等協調者回來
| 問題 | 說明 |
|---|---|
| ★ 協調者單點 | 它掛了,參與者全部卡住 |
| ★ 同步阻塞 | 整個 prepare→commit 期間所有參與者持鎖 |
| 可用性差 | 任一參與者不可用,整個交易失敗(可用性是各方的乘積) |
| 效能差 | 至少兩輪網路往返 + 持鎖時間長 |
★ 它在單一資料庫叢集內(同一個 DBMS 的多個分片)還算堪用;跨異質系統基本上是災難。
★ 產品端要配合的事(技術解不了的部分)
最終一致性必須在產品層面被承認,不能偷偷做:
- ★ 顯示中間狀態:「訂單處理中」而不是假裝已完成
- ★ 提供查詢入口:使用者能看到目前進度
- 定義補償後的使用者體驗:退款要通知、要說明原因
- ★ 對帳機制:定期比對兩邊,找出不一致並告警——
這是最被忽略但最重要的一環。沒有對帳,你永遠不知道系統有沒有出錯。
🧪 我實際套用的紀錄
- 2026-07-15:(待填)
⚠️ 注意 / 什麼時候不適用
- ★★ 先檢討服務邊界。頻繁需要跨服務事務 = 邊界切錯了(見 工具-服務邊界與演進式拆分)。
合併回同一個服務往往是正確答案,而不是蓋一套 Saga 框架。 - ★ 一定要有對帳。任何最終一致的系統都會飄移,只是你知不知道。 排定期對帳 + 告警 + 人工修補流程。
- ★ 消費端冪等不是可選的。所有方案都是「至少一次」投遞。
- 不可逆的步驟要排在最後,或延遲執行(見 可逆性、審計與人類把關 的 L3 降級技巧)。
- Saga 的狀態要持久化。流程執行到一半服務重啟 → 要能從中斷處續跑(見 狀態模式)。
- 不要用「分散式鎖」代替分散式事務。鎖不安全(見 工具-線性一致性與共識 的 fencing token)。
- ★ 測試很難。要測「第 3 步失敗時補償有沒有正確執行」——故障注入是必要的(見 工具-注入故障與韌性演練)。
- 監控要能看到「卡住的 Saga」:超過 N 分鐘還沒完成的流程要告警。
🔗 相關工具
- 工具-Transactional-Outbox-Pattern —— ★ 預設方案的完整做法
- 工具-交易與隔離等級 —— 單機交易是 Outbox 的基礎
- 工具-MQ消費端防禦三原則 —— 冪等與重試
- 工具-服務邊界與演進式拆分 —— ★ 先檢討邊界
- 工具-線性一致性與共識 —— 為什麼分散式鎖不安全
- 工具-預寫日誌與崩潰復原 —— CDC 作為 Outbox 的替代
- 狀態模式 —— Saga 的狀態持久化
- 工具-注入故障與韌性演練 —— 補償路徑要被測試