---
title: 分布式事务
url: https://doc.liz6.com/distributed-systems/03-replication-and-consistency/03-distributed-transactions
locale: zh
area: distributed-systems
tags:
- distributed-systems
- 复制与一致性
date: 2026-06-30
modified: 2026-07-11
description: 跨多个服务的一次写操作——没有全局 ACID。2PC 追求强一致但 coordinator 故障时参与者全部阻塞;Saga 和 TCC 用补偿事务换可用性,代价是应用层要自己写回滚逻辑。outbox pattern 和幂等性保证了"至少一次"在工程上的可行性。
---

# 分布式事务

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

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

## 2PC (Two-Phase Commit)

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

<svg viewBox="0 0 720 320" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="2PC 两阶段提交流程:PREPARE 投票阶段与 COMMIT/ABORT 决议阶段">
  <defs><marker id="m2pc" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="320" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">2PC 两阶段提交:PREPARE 投票 → COMMIT/ABORT 决议</text>

  <rect x="40" y="50" width="300" height="28" rx="6" fill="#4f46e5"/>
  <text x="190" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">① PREPARE 阶段 · 协调者发起投票</text>

  <rect x="52" y="90" width="276" height="44" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="109" font-size="12" font-weight="700" fill="#3730a3">协调者 → 各参与者</text>
  <text x="66" y="126" font-size="11" fill="#4f46e5">发送 PREPARE 请求</text>

  <rect x="52" y="140" width="276" height="60" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="66" y="159" font-size="12" font-weight="700" fill="#3730a3">参与者</text>
  <text x="66" y="176" font-size="11" fill="#4f46e5">执行操作、写 WAL,但不 commit</text>
  <text x="66" y="192" font-size="11" fill="#4f46e5">回复 YES(能提交)或 NO</text>

  <rect x="52" y="206" width="276" height="32" rx="6" fill="#e0e7ff"/>
  <text x="190" y="227" text-anchor="middle" font-size="12" font-weight="600" fill="#3730a3">收集所有回复,判断是否全部 YES</text>

  <rect x="380" y="50" width="300" height="28" rx="6" fill="#0d9488"/>
  <text x="530" y="69" text-anchor="middle" font-size="13" font-weight="700" fill="#ffffff">② 决议阶段 · 协调者广播结果</text>

  <rect x="392" y="90" width="276" height="54" rx="6" fill="#dcfce7" stroke="#4ade80"/>
  <text x="406" y="112" font-size="12" font-weight="700" fill="#166534">全部 YES → COMMIT</text>
  <text x="406" y="132" font-size="11" fill="#15803d">协调者广播 COMMIT,参与者提交</text>

  <rect x="392" y="154" width="276" height="54" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="406" y="176" font-size="12" font-weight="700" fill="#9a3412">任意 NO → ABORT</text>
  <text x="406" y="196" font-size="11" fill="#c2410c">协调者广播 ABORT,参与者回滚</text>

  <line x1="330" y1="172" x2="388" y2="172" stroke="#475569" stroke-width="1.8" marker-end="url(#m2pc)"/>

  <rect x="40" y="248" width="640" height="56" rx="8" fill="#ffedd5"/>
  <text x="56" y="270" font-size="12.5" fill="#9a3412">问题:coordinator 若在 PREPARE 之后、COMMIT 之前 crash,participant 就陷入不确定态——</text>
  <text x="56" y="290" font-size="12.5" fill="#9a3412">不能单方面 commit,也不能单方面 abort,只能阻塞等待协调者恢复,这就是 2PC 的 blocking protocol。</text>
</svg>

问题：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 的补偿。

<svg viewBox="0 0 720 330" xmlns="http://www.w3.org/2000/svg" font-family="-apple-system,'Source Han Sans CN','Microsoft YaHei',sans-serif" role="img" aria-label="Saga 分布式事务:四个本地事务步骤顺序执行,失败时按相反顺序执行补偿">
  <defs><marker id="msaga" markerWidth="10" markerHeight="8" refX="8" refY="3" orient="auto"><path d="M0,0 L8,3 L0,6 Z" fill="#475569"/></marker></defs>
  <rect width="720" height="330" fill="#ffffff"/>
  <text x="360" y="28" text-anchor="middle" font-size="17" font-weight="700" fill="#1f2933">Saga:book_trip() 拆成本地事务步骤,失败时按相反顺序执行补偿</text>

  <rect x="20" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="90" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 1</text>
  <text x="90" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_flight()</text>

  <rect x="200" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="270" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 2</text>
  <text x="270" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_hotel()</text>

  <rect x="380" y="60" width="140" height="50" rx="6" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="450" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#3730a3">Step 3</text>
  <text x="450" y="98" text-anchor="middle" font-size="11" fill="#4f46e5">book_car()</text>

  <rect x="560" y="60" width="140" height="50" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="630" y="80" text-anchor="middle" font-size="12" font-weight="700" fill="#9a3412">Step 4</text>
  <text x="630" y="98" text-anchor="middle" font-size="9.5" fill="#c2410c">process_payment() 失败</text>

  <line x1="160" y1="85" x2="200" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>
  <line x1="340" y1="85" x2="380" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>
  <line x1="520" y1="85" x2="560" y2="85" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <line x1="90" y1="110" x2="90" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>
  <line x1="270" y1="110" x2="270" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>
  <line x1="450" y1="110" x2="450" y2="138" stroke="#475569" stroke-width="1.4" marker-end="url(#msaga)"/>

  <rect x="20" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="90" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">补偿: cancel_flight()</text>

  <rect x="200" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="270" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">补偿: cancel_hotel()</text>

  <rect x="380" y="140" width="140" height="40" rx="6" fill="#ffedd5"/>
  <text x="450" y="164" text-anchor="middle" font-size="10.5" font-weight="700" fill="#9a3412">补偿: cancel_car()</text>

  <text x="20" y="207" font-size="12" font-weight="700" fill="#1f2933">process_payment() 失败 → 按相反顺序执行已完成步骤的补偿:</text>

  <rect x="20" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="120" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">process_payment() 失败</text>

  <line x1="220" y1="233" x2="260" y2="233" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <rect x="260" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="360" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">① cancel_hotel()</text>

  <line x1="460" y1="233" x2="500" y2="233" stroke="#475569" stroke-width="1.6" marker-end="url(#msaga)"/>

  <rect x="500" y="216" width="200" height="34" rx="6" fill="#ffedd5" stroke="#f97316"/>
  <text x="600" y="238" text-anchor="middle" font-size="11" font-weight="700" fill="#9a3412">② cancel_flight()</text>

  <rect x="20" y="258" width="680" height="56" rx="8" fill="#eef2ff" stroke="#c7d2fe"/>
  <text x="36" y="280" font-size="12.5" fill="#3730a3">Saga 没有隔离性——其他事务可能看到中间态,但它不阻塞:任何时刻系统都在 progressing。</text>
  <text x="36" y="300" font-size="12.5" fill="#3730a3">两种编排模式:Choreography(去中心、靠事件驱动)vs Orchestration(中心协调器、逻辑集中易管理)。</text>
</svg>

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*
