為什麼需要分散式事務#

單庫事務由 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 比:

維度TCCSaga
一致性Try 階段預留資源,較強純補償,較弱(中間狀態可見)
業務侵入每個操作 3 方法每個操作 + 補償方法
適合場景短流程、高一致長流程、跨多服務

哪個方案選哪個#

決策樹:

強一致性 + 短流程?
  ├─ 是 → TCC(適合金融核心)
  └─ 否 →
      最終一致就好?
        ├─ 是 → Outbox / CDC(最常用)
        └─ 否 → 長流程 saga(業務流可見)

90% 的電商場景用 Outbox / CDC 就夠了。剩下 10% 是核心交易流,可能用 TCC 或業務化的 saga。XA / 2PC 在 2025 年的生產環境基本不會看到。

冪等:所有方案的共同前提#

不論哪種分散式事務方案,消費端必須冪等。原因:

  • 訊息可能重投(網路抖動、consumer crash 後 retry)
  • TCC 的 Confirm/Cancel 可能重複呼叫
  • Saga 的補償可能重試

冪等設計三選一:

  1. 業務天然冪等:例如「設定狀態為 X」── 多次設仍是 X
  2. 唯一鍵:用 (idempotency_key) 欄位 + UNIQUE 索引去重
  3. 狀態機 + 嚴格轉移:只有特定源狀態才能轉到目標狀態

案例:完整下單流程#

把這章與第 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 怎麼做商品搜尋。