為什麼需要分散式事務#
單庫事務由 DB 自己保證 ACID。但電商不可能單庫:
下訂單流程:
訂單服務(DB-A) + 庫存服務(DB-B) + 賬戶服務(DB-C) + 優惠券服務(DB-D)任何一個失敗 → 其他要回滾。怎麼做?
這就是分散式事務問題。沒有完美解 ── 只有針對不同業務場景的多種方案。本章介紹五種主流:2PC、3PC、TCC、本地訊息表(Outbox Pattern)、Saga。
為什麼這麼難#
CAP 定理:分散式系統只能在 C(一致性)/ A(可用性)/ P(分區容忍)三選二。但 P 在實務上必須有(網路分區一定會發生)→ 真實取捨在 C 與 A 之間。
| 強一致性需求 | 例子 |
|---|---|
| 必須 ACID 嚴格 | 銀行轉帳、賬戶系統 |
| 最終一致即可 | 訂單與通知、訂單與購物車 |
| 補償可接受 | 訂單與配送、報表生成 |
業務需求差異 → 方案差異。
2PC:經典兩階段提交#
協調者(Coordinator)+ 多個參與者(Participants)。
階段 1(prepare):
Coord → 所有 Participants:「準備好嗎?」
Participants:本地執行(占資源、寫日誌),但不提交,回「OK / NO」
階段 2(commit / abort):
全部 OK → Coord 通知 commit
任一 NO → Coord 通知 abort時序圖:
Coord P1 P2 P3
│ ─prepare─→ │ │ │
│ │ <local exec> │
│ ←──ready── │ │ │
│ ─prepare─────────────────→│ │
│ ←──ready──────────────────│ │
│ ─prepare───────────────────────────────→│
│ ←──ready────────────────────────────────│
│
│ ─commit──→ │ │ │
│ ─commit────────────────→ │ │
│ ─commit───────────────────────────────→│優點:強一致性。 缺點:
- 同步阻塞:prepare 後 participants 必須持鎖等 phase 2
- 單點故障:Coordinator 掛了,prepared 的 participants 卡住
- 資料不一致風險:phase 2 廣播時部分網路斷了,部分 commit、部分沒收到
實務:XA 協議是 2PC 的具體實作,幾乎沒人在生產環境用。MySQL XA 在主從複製下還有錯誤紀錄。
3PC:增加一個 CanCommit 階段#
3PC 加了 CanCommit phase(早期協商)和 timeout 機制,能避免 2PC 的阻塞問題。但代價是更多 RTT、更複雜。實務上也少見。
TCC:補償式事務#
Try-Confirm-Cancel ── 把每個操作拆成三段:
Try : 預留資源,凍結但不真正執行
Confirm: 真正執行(必冪等)
Cancel : 釋放預留(必冪等)訂單例子:
下訂單:
Try:
訂單服務.create_pending_order
庫存服務.freeze_stock(sku, n)
優惠券.lock_coupon(coupon_id)
賬戶.freeze(user, amount)
All Try OK?
→ Confirm:
訂單.confirm_order
庫存.reduce_stock
優惠券.use_coupon
賬戶.debit
→ Any Try fail:
Cancel:
庫存.release_freeze
優惠券.unlock
賬戶.unfreeze關鍵要求:
- Try 階段預留資源:之後 Confirm 一定能成
- Confirm/Cancel 必須冪等:可能因網路重試多次
- Confirm/Cancel 必須最終成功:不行就無限重試 + 人工介入
優點:強一致性 + 較好的吞吐(沒有長時間鎖)。 缺點:
- 業務侵入極大 ── 每個操作要寫三個方法
- Cancel 邏輯難正確實作(已經 Try 了什麼狀態?)
- 框架(Seata、ServiceComb)幫忙但不能消除複雜度
適合:金融、交易、訂單核心流程。
本地訊息表(Outbox Pattern)#
訂單系統的多系統一致性,最多的場景其實是「訂單成功後通知其他系統」── 不需要分散式事務的強一致,最終一致即可。
問題重述#
def create_order():
db.begin()
db.insert_order(...)
mq.publish("order_created", ...) # ← 這裡可能失敗
db.commit()兩個操作不在同一事務 → 多種失敗組合:
- DB 成功、MQ 失敗 → 訂單存在但其他系統不知道
- MQ 成功、DB 失敗 → 其他系統以為下單成功
解:Outbox / 本地訊息表#
把訊息寫進和訂單同一個 DB 的 outbox 表,跟訂單一起提交:
CREATE TABLE outbox (
id BIGINT PK,
aggregate_id BIGINT,
event_type VARCHAR(50),
payload JSON,
status VARCHAR(20), -- 'pending', 'sent'
created_at TIMESTAMP
);def create_order():
with db.transaction():
db.insert_order(order)
db.insert(outbox, {
"event_type": "order_created",
"payload": serialize(order),
"status": "pending"
})
# 事務一起提交:訂單 + outbox 條目原子獨立的 worker 輪詢 outbox:
def outbox_dispatcher():
while True:
rows = db.query("SELECT * FROM outbox WHERE status='pending' LIMIT 100")
for row in rows:
try:
mq.publish(row.event_type, row.payload)
db.update("UPDATE outbox SET status='sent' WHERE id=%s", row.id)
except:
continue # 下次再試優點:
- 業務簡單,跟一般事務一樣
- 訊息保證最終發出(重試到成功)
- 不依賴跨服務的事務協調
缺點:
- worker 有延遲(秒級)
- 訊息只保證「at-least-once」,consumer 必須冪等
這是現代分散式系統最常用的「分散式事務」近似 ── 用最終一致性換掉強一致性。
CDC:把 Outbox 自動化#
Change Data Capture(變更資料擷取)── 用 binlog(MySQL)或 WAL(Postgres)作為事件源:
DB write → binlog → Debezium / Canal → Kafka → consumers這樣根本不需要 outbox 表 ── DB 變更本身就是事件。但要注意:
- binlog 事件粒度是「行變更」,不是業務語意
- 一次業務操作可能跨多行 ── 下游要重新組合
Saga 模式#
長流程的補償式:每個步驟都有對應的「補償動作」。
正向流程: A1 → A2 → A3 → A4
補償流程: A4_compensate ← A3_compensate ← A2_compensate ← A1_compensate如果 A3 失敗,倒著 compensate A2、A1。
兩種協調方式:
Choreography(編舞)#
每個服務監聽事件、自主決定下一步。
OrderCreated → Inventory 監聽 → 扣庫存 → InventoryReduced
↓
Payment 監聽 → 扣款 → PaymentSuccess
↓
OrderConfirmed優點:去中心化。 缺點:流程不可見、難 debug。
Orchestration(編排)#
中央 orchestrator 控制流程。
Orchestrator:
1. 呼叫 Order Service
2. 呼叫 Inventory
3. 呼叫 Payment
4. 任一失敗 → 倒著補償優點:流程清晰、好監控。 缺點:orchestrator 是焦點。
實作框架:Camunda、Temporal、Cadence、AWS Step Functions。
Saga 與 TCC 比:
| 維度 | TCC | Saga |
|---|---|---|
| 一致性 | Try 階段預留資源,較強 | 純補償,較弱(中間狀態可見) |
| 業務侵入 | 每個操作 3 方法 | 每個操作 + 補償方法 |
| 適合場景 | 短流程、高一致 | 長流程、跨多服務 |
哪個方案選哪個#
決策樹:
強一致性 + 短流程?
├─ 是 → TCC(適合金融核心)
└─ 否 →
最終一致就好?
├─ 是 → Outbox / CDC(最常用)
└─ 否 → 長流程 saga(業務流可見)90% 的電商場景用 Outbox / CDC 就夠了。剩下 10% 是核心交易流,可能用 TCC 或業務化的 saga。XA / 2PC 在 2025 年的生產環境基本不會看到。
冪等:所有方案的共同前提#
不論哪種分散式事務方案,消費端必須冪等。原因:
- 訊息可能重投(網路抖動、consumer crash 後 retry)
- TCC 的 Confirm/Cancel 可能重複呼叫
- Saga 的補償可能重試
冪等設計三選一:
- 業務天然冪等:例如「設定狀態為 X」── 多次設仍是 X
- 唯一鍵:用
(idempotency_key)欄位 + UNIQUE 索引去重 - 狀態機 + 嚴格轉移:只有特定源狀態才能轉到目標狀態
案例:完整下單流程#
把這章與第 1、3 章串起來:
1. 用戶提交訂單請求 (帶 idempotency_key)
2. 訂單服務:
begin transaction
- 查或建訂單(idempotency_key UNIQUE 處理重複)
- 寫 outbox: "order_created"
commit
3. outbox dispatcher:
- 發布 "order_created" 到 Kafka
4. consumers:
- 庫存服務 監聽 → 凍結庫存
- 優惠券服務 監聽 → 鎖券
- 支付服務 監聽 → 等待用戶支付
5. 用戶支付成功:
支付服務 → 發 "payment_completed"
訂單服務監聽 → 把訂單狀態改為 paid
庫存服務監聽 → 凍結轉實扣
優惠券服務監聽 → 鎖轉用掉
6. 任何步驟失敗:
按業務規則決定 cancel / retry / 人工每個服務內部用本地事務保證原子,跨服務用 Outbox + 訊息保證最終一致。冪等貫穿全程。
小結#
- 分散式事務是「業務取捨」的問題,不是「找完美方案」
- 強一致 + 短流程 → TCC
- 最終一致 → Outbox / CDC(90% 場景)
- 長流程 → Saga(編排或編舞)
- 強一致 + 重型 → 2PC / XA(多數情況不建議)
- 所有方案的共同前提:消費者冪等
下章看 Elasticsearch 怎麼做商品搜尋。