本页目录

分布式事务

跨多个服务的一次写操作——没有全局 ACID。2PC 追求强一致但 coordinator 故障时参与者全部阻塞;Saga 和 TCC 用补偿事务换可用性,代价是应用层要自己写回滚逻辑。outbox pattern 和幂等性保证了"至少一次"在工程上的可行性。

单机数据库中事务是 ACID 的——BEGIN/COMMIT/ROLLBACK 即可。分布式系统涉及多个服务的写操作,没有全局的 ACID 事务。

2PC (Two-Phase Commit)

最基础的分布式事务协议——阻塞式,无协调器故障容错。

2PC 两阶段提交:PREPARE 投票 → COMMIT/ABORT 决议 ① PREPARE 阶段 · 协调者发起投票 协调者 → 各参与者 发送 PREPARE 请求 参与者 执行操作、写 WAL,但不 commit 回复 YES(能提交)或 NO 收集所有回复,判断是否全部 YES ② 决议阶段 · 协调者广播结果 全部 YES → COMMIT 协调者广播 COMMIT,参与者提交 任意 NO → ABORT 协调者广播 ABORT,参与者回滚 问题:coordinator 若在 PREPARE 之后、COMMIT 之前 crash,participant 就陷入不确定态—— 不能单方面 commit,也不能单方面 abort,只能阻塞等待协调者恢复,这就是 2PC 的 blocking protocol。

问题:coordinator crash 在 PREPARE 之后、COMMIT 之前 → participant 处于 uncertain state——它不能单方面 commit(不知道其他 participant 是否 YES),也不能单方面 abort(其他可能已经 commit 了)。⁠参与者阻塞直到 coordinator 恢复——这就是"blocking protocol"。

实践中:XA 事务(2PC 的实现)被用在数据库跨分片事务中,但处理 coordinator crash 需要管理员手动 heuristic commit/abort。

Saga

将长事务拆为多个本地事务 steps,每步有自己的补偿操作(compensating action)。某步失败时,以相反顺序执行已完成的 steps 的补偿。

Saga:book_trip() 拆成本地事务步骤,失败时按相反顺序执行补偿 Step 1 book_flight() Step 2 book_hotel() Step 3 book_car() Step 4 process_payment() 失败 补偿: cancel_flight() 补偿: cancel_hotel() 补偿: cancel_car()

process_payment() 失败 → 按相反顺序执行已完成步骤的补偿:

process_payment() 失败 ① cancel_hotel() ② cancel_flight() Saga 没有隔离性——其他事务可能看到中间态,但它不阻塞:任何时刻系统都在 progressing。 两种编排模式:Choreography(去中心、靠事件驱动)vs Orchestration(中心协调器、逻辑集中易管理)。

Saga 没有隔离性(其他事务可以看到中间态),但它不阻塞——任何时刻系统都在 progressing。

编排模式

  • Choreography: 每个 service 消费上一个 step 的 event,发布自己完成/失败 event — 无中心编排器
  • Orchestration: 中心 Saga 协调器告诉各 service "执行 step" 或 "执行补偿" — 逻辑集中,容易管理

TCC (Try-Confirm-Cancel)

Saga 的变体,适用更"刚性"的资源预留:

Try:     预留资源 (检查库存、冻结余额) — 资源**未消耗**,只是标记为 "待使用"
Confirm: 实际消耗资源 (扣库存、扣款)
Cancel:  释放预留 (释放库存、解冻余额)

与 Saga 的区别:Saga 的 step 直接修改了数据(book_flight 真的订了票),补偿是 undo。TCC 的 Try 只预留,Confirm 才是真正修改——⁠任何时刻都可以 Cancel 而不需要在后续执行 undo

幂等性

分布式系统中最重要的非功能性要求:f(x) = f(f(x))——重试操作不影响结果。

实现方式:
  1. unique idempotency key: client 生成 UUID, server 存储 (key) → (已处理/未处理)
     → 重复 key → 返回缓存的结果, 不重新执行
  2. database constraints: INSERT INTO orders VALUES (order_id=123, ...)
     → 重试 → UNIQUE constraint violation → 忽略 or read existing
  3. token-based: server 给 client token, client 用 token 标记请求
     → 重复 token → 拒绝

不幂等的例子:
  UPDATE users SET balance = balance + 100  -- 重试 = 加了 2 次!
  改法: UPDATE users SET balance = ? WHERE version = ? (optimistic lock)

Outbox Pattern

事务性写数据库 + 发消息给消息队列——如何保证两者原子?不能先写 DB 再发消息(DB 写成功但消息发送失败 → inconsistent)。

[本地事务]:
  INSERT INTO orders (...);
  INSERT INTO outbox (event_type, payload, created_at) VALUES ('order_created', '...', NOW());
  COMMIT;  ← 两者在同一个本地事务中!

[Outbox Poller]:
  SELECT * FROM outbox ORDER BY id → 发到 Kafka/MQ → 成功 → 删除 from outbox

保证:order 写入 + event 产生是原子的(same transaction)。Outbox poller 做 at-least-once delivery。

参考

  • 论文⁠: "Sagas" (Garcia-Molina & Salem, 1987)
  • 微服务事务⁠: microservices.io/patterns/data/saga.html
  • 幂等⁠: stripe.com/docs/idempotency (生产级实现)

Keywords: 2PC, XA, Saga, TCC, compensating action, idempotency, outbox pattern, distributed transaction