微服务拆分精讲 / 第 10 章:分布式事务
第 10 章:分布式事务
分布式事务是微服务架构中最棘手的问题之一。没有银弹,只有在一致性、可用性和复杂性之间做权衡。
10.1 为什么需要分布式事务
10.1.1 问题场景
电商下单流程(跨多个服务):
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 库存服务 │ │ 支付服务 │ │ 积分服务 │
│ │ │ │ │ │ │ │
│ 创建订单 │───▶│ 扣减库存 │───▶│ 扣减余额 │───▶│ 增加积分 │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
问题:如果"扣减余额"失败,前面已执行的操作怎么办?
• 订单已创建 ✅ → 需要回滚
• 库存已扣减 ✅ → 需要恢复
• 余额扣减 ❌ → 失败点
• 积分增加 → 未执行
→ 需要一种机制保证所有操作要么全部成功,要么全部回滚
10.1.2 分布式事务 vs 本地事务
| 维度 | 本地事务 (ACID) | 分布式事务 |
|---|
| 原子性 | 数据库保证 | 需要额外机制 |
| 一致性 | 强一致 | 最终一致(多数方案) |
| 隔离性 | 数据库锁 | 难以保证 |
| 持久性 | WAL 日志 | 各服务独立保证 |
| 性能 | 高 | 较低 |
| 复杂度 | 低 | 高 |
10.1.3 CAP 定理的约束
CAP 定理:分布式系统最多同时满足其中两个
C (Consistency) 一致性
A (Availability) 可用性
P (Partition Tolerance) 分区容错性
C
/ \
/ \
/ \
A ───── P
网络分区必然存在 → 实际选择是 CP 或 AP
CP:放弃可用性(如 ZooKeeper、Etcd)
AP:放弃强一致性(如 Cassandra、DynamoDB)
微服务通常选择 AP + 最终一致性
10.2 分布式事务解决方案总览
10.2.1 方案对比
| 方案 | 一致性 | 性能 | 复杂度 | 适用场景 |
|---|
| 2PC/XA | 强一致 | 低 | 中 | 传统数据库事务 |
| TCC | 最终一致 | 中 | 高 | 资金操作 |
| Saga | 最终一致 | 高 | 中 | 长事务、跨服务流程 |
| 本地消息表 | 最终一致 | 高 | 低 | 异步场景 |
| 事务消息 | 最终一致 | 高 | 低 | MQ 场景 |
10.3 两阶段提交(2PC / XA)
10.3.1 流程
2PC (Two-Phase Commit) 流程:
┌──────────┐ ┌──────────┐
│ 协调者 │ │ 参与者 A │
│ (Coordinator)│ │ (数据库A) │
└─────┬────┘ └─────┬────┘
│ │
═══ 阶段1:准备 (Prepare) ════════════════════════════
│ │
│ 1. Prepare │
│────────────────────────▶│
│ │ 执行SQL,不提交
│ 2. Vote Yes/No │
│◀────────────────────────│
│ │
│ ┌──────────┐ │
│ │ 参与者 B │ │
│ │ (数据库B) │ │
│ └─────┬────┘ │
│ │ │
│ 1. Prepare │
│────────────────────────▶│
│ 2. Vote Yes/No │
│◀────────────────────────│
│ │
═══ 阶段2:提交/回滚 (Commit/Rollback) ════════════════
│ │
所有都 Vote Yes? │
│ │
Yes: │ 3. Commit │
│────────────────────────▶│ 提交事务
│ │
No: │ 3. Rollback │
│────────────────────────▶│ 回滚事务
10.3.2 2PC 的问题
| 问题 | 说明 |
|---|
| 同步阻塞 | 所有参与者在 Prepare 后锁定资源,等待 Commit |
| 单点故障 | 协调者崩溃导致所有参与者阻塞 |
| 数据不一致 | Commit 阶段部分参与者收不到消息 |
| 性能差 | 全程锁定,吞吐量低 |
⚠️ 结论:2PC 不推荐在微服务中使用。传统单体应用的分布式数据库可以考虑 XA,但微服务架构应选择更现代的方案。
10.4 TCC 模式
10.4.1 概念
TCC(Try-Confirm-Cancel)是一种补偿型分布式事务模式,将每个操作分为三个阶段:
| 阶段 | 说明 | 示例(扣减库存) |
|---|
| Try | 预留资源(检查+冻结) | 冻结 10 件库存 |
| Confirm | 确认提交(真正扣减) | 确认扣减冻结的 10 件 |
| Cancel | 取消释放(回退预留) | 释放冻结的 10 件库存 |
10.4.2 TCC 流程
TCC 执行流程:
┌────────┐ ┌────────┐ ┌────────┐
│ 订单服务│ │ 库存服务│ │ 支付服务│
│(发起者) │ │ │ │ │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
═══ Try 阶段 ═══════════════════════════════
│ │ │
│ Try 冻结库存 │ │
│────────────────▶│ │
│ Try 成功 ✅ │ │
│◀────────────────│ │
│ │ │
│ Try 冻结余额 │ │
│──────────────────────────────────▶│
│ Try 成功 ✅ │ │
│◀──────────────────────────────────│
│ │ │
═══ 全部 Try 成功 → Confirm 阶段 ════════════
│ │ │
│ Confirm 扣减 │ │
│────────────────▶│ │
│ Confirm 成功 ✅│ │
│ │ │
│ Confirm 扣款 │ │
│──────────────────────────────────▶│
│ Confirm 成功 ✅│ │
│ │ │
═══ 如果有 Try 失败 → Cancel 阶段 ════════════
│ │ │
│ Cancel 释放冻结 │ │
│────────────────▶│ │
│ Cancel 成功 ✅ │ │
10.4.3 TCC 实现要点
// TCC 接口定义
public interface InventoryTccService {
// Try: 冻结库存
@TwoPhaseBusinessAction(name = "deductInventory",
commitMethod = "confirm",
rollbackMethod = "cancel")
boolean tryDeduct(
@BusinessActionContextParameter(paramName = "orderId") String orderId,
@BusinessActionContextParameter(paramName = "productId") String productId,
@BusinessActionContextParameter(paramName = "quantity") int quantity
);
// Confirm: 确认扣减
boolean confirm(BusinessActionContext context);
// Cancel: 释放冻结
boolean cancel(BusinessActionContext context);
}
// 实现
@Service
public class InventoryTccServiceImpl implements InventoryTccService {
@Override
@Transactional
public boolean tryDeduct(String orderId, String productId, int quantity) {
// 1. 检查可用库存是否足够
int available = inventoryMapper.getAvailable(productId);
if (available < quantity) return false;
// 2. 冻结库存(available -= quantity, frozen += quantity)
inventoryMapper.freeze(productId, orderId, quantity);
return true;
}
@Override
@Transactional
public boolean confirm(BusinessActionContext context) {
String orderId = (String) context.getActionContext("orderId");
String productId = (String) context.getActionContext("productId");
// 确认扣减(frozen -= quantity, total -= quantity)
inventoryMapper.confirmDeduct(productId, orderId);
return true;
}
@Override
@Transactional
public boolean cancel(BusinessActionContext context) {
String orderId = (String) context.getActionContext("orderId");
String productId = (String) context.getActionContext("productId");
// 释放冻结(available += quantity, frozen -= quantity)
inventoryMapper.releaseFrozen(productId, orderId);
return true;
}
}
10.4.4 TCC 的空回滚和悬挂问题
| 问题 | 说明 | 解决方案 |
|---|
| 空回滚 | Try 没执行,Cancel 先到 | Cancel 时检查 Try 是否执行过 |
| 悬挂 | Cancel 先执行,Try 后到 | Try 时检查 Cancel 是否已执行 |
| 幂等 | Confirm/Cancel 重复调用 | 使用事务 ID 去重 |
空回滚:
Try 请求丢失 ──▶ 协调者超时 ──▶ 调用 Cancel
Cancel 发现没有冻结记录 → 记录"已回滚"标记
悬挂:
Cancel 先执行 ──▶ Try 后到达 ──▶ Try 发现"已回滚"标记 → 不执行
10.5 Saga 模式
10.5.1 概念
Saga 将长事务拆分为一系列本地事务,每个本地事务有对应的补偿操作。如果某一步失败,则反向执行之前所有步骤的补偿操作。
Saga 两种实现方式:
1. 编排式 (Choreography) — 事件驱动
2. 编排式 (Orchestration) — 中央协调
10.5.2 编排式 Saga(Choreography)
事件驱动的 Saga:
┌────────┐ OrderCreated ┌────────┐ PaymentCompleted ┌────────┐
│ 订单服务│ ──────────────▶ │ 支付服务│ ◀───────────────── │ │
│ │ │ │ │ │
│ 创建订单│ InventoryFrozen│ 扣减余额│ OrderConfirmed │ │
│ │ ◀────────────── │ │ ─────────────────▶ │ │
└────┬───┘ └────────┘ └────────┘
│ │
│ OrderCancelled │ PaymentFailed
▼ ▼
┌────────┐ ┌────────┐
│ 库存服务│ │ 订单服务│
│ 恢复库存│ │ 取消订单│
└────────┘ └────────┘
正向流程:Order → Payment → Inventory → Confirm
补偿流程:Inventory 恢复 → Payment 退款 → Order 取消
10.5.3 编排式 Saga(Orchestration)
中央协调的 Saga:
┌──────────────────────────────────────────────────────┐
│ Saga 协调器 (Orchestrator) │
│ │
│ ┌────────────────────────────────────────────────┐ │
│ │ Saga 定义: │ │
│ │ Step 1: 创建订单 → Compensate: 取消订单 │ │
│ │ Step 2: 冻结库存 → Compensate: 恢复库存 │ │
│ │ Step 3: 扣减余额 → Compensate: 退还余额 │ │
│ │ Step 4: 增加积分 → Compensate: 扣除积分 │ │
│ └────────────────────────────────────────────────┘ │
└──────────────────────────┬───────────────────────────┘
│
┌────────────────────┼────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ 订单服务 │ │ 库存服务 │ │ 支付服务 │
└──────────┘ └──────────┘ └──────────┘
协调器根据每一步的执行结果决定:
→ 成功:执行下一步
→ 失败:反向执行补偿操作
10.5.4 Saga 状态机
┌──────────┐
│ START │
└────┬─────┘
▼
┌──────────┐ 成功 ┌──────────┐ 成功 ┌──────────┐
│ Step 1 │ ───────▶ │ Step 2 │ ───────▶ │ Step 3 │
│ 创建订单 │ │ 冻结库存 │ │ 扣减余额 │
└────┬─────┘ └────┬─────┘ └────┬─────┘
│失败 │失败 │失败
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│COMPENSATE│ │COMPENSATE│ │COMPENSATE│
│ (无需补偿)│ │ 取消订单 │ │ 恢复库存 │
└────┬─────┘ │ 取消订单 │ │ 取消订单 │
│ └──────────┘ └──────────┘
▼
┌──────────┐
│ ABORTED │
└──────────┘
如果 Step 3 失败:
执行补偿 Step 2 (恢复库存) → 执行补偿 Step 1 (取消订单) → ABORTED
10.5.5 Saga vs TCC
| 维度 | Saga | TCC |
|---|
| 资源锁定 | 不锁定(正向操作) | Try 阶段锁定 |
| 一致性 | 最终一致 | 最终一致 |
| 补偿难度 | 需要补偿操作 | Cancel 就是补偿 |
| 性能 | 更好(无锁定) | 略差(Try 锁定) |
| 实现复杂度 | 中等 | 较高(处理空回滚/悬挂) |
| 适用场景 | 长流程业务 | 资金类操作 |
10.6 本地消息表
10.6.1 模式
本地消息表流程:
┌──────────────────────────────────────┐
│ 数据库事务 │
│ ┌──────────────────────────────────┐│
│ │ INSERT INTO orders ... ││
│ │ INSERT INTO local_messages ││ ← 同一个事务
│ │ (type, payload, status=PENDING)││
│ └──────────────────────────────────┘│
└──────────────────────────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 后台任务:扫描 local_messages 表 │
│ 发送 status=PENDING 的消息到 MQ │
│ 更新 status=SENT │
└───────────────────┬──────────────────┘
│
▼
┌──────────────────────────────────────┐
│ 消费者消费消息,处理业务逻辑 │
│ 处理成功后回调确认 │
└──────────────────────────────────────┘
10.6.2 与事务性发件箱的区别
两者本质相同,都是保证"业务操作"和"消息发送"的原子性:
| 维度 | 本地消息表 | 事务性发件箱 (Outbox) |
|---|
| 消息提取 | 定时任务轮询 | CDC (Debezium) 监听 Binlog |
| 实时性 | 秒级(取决于轮询间隔) | 毫秒级 |
| 数据库压力 | 有(频繁查询) | 低(只读 Binlog) |
| 实现难度 | 低 | 中 |
10.7 方案选型指南
分布式事务方案选型决策树:
需要强一致性吗?
│
├── 是 → 数据量小 + 性能要求不高?
│ ├── 是 → 考虑 2PC/XA(传统方案)
│ └── 否 → 考虑 TCC(冻结资源)
│
└── 否(最终一致性可接受)→
│
├── 有消息队列吗?
│ ├── 是 → 事务消息 / 本地消息表
│ └── 否 → Saga 模式
│
└── 流程长 + 参与者多?
├── 是 → Saga(编排式/协调式)
└── 否 → 本地消息表
10.8 业务场景:电商下单的分布式事务
采用 Saga 模式:
┌─────────────────────────────────────────────────────────────┐
│ Saga 协调器: OrderSaga │
│ │
│ Step 1: 创建订单 (order-service) │
│ → 补偿: 取消订单 │
│ │
│ Step 2: 扣减库存 (inventory-service) │
│ → 补偿: 恢复库存 │
│ │
│ Step 3: 扣减余额 (payment-service) │
│ → 补偿: 退还余额 │
│ │
│ Step 4: 增加积分 (points-service) │
│ → 补偿: 扣除积分 │
│ │
│ Step 5: 发送通知 (notification-service) │
│ → 补偿: 无需补偿(通知已发送不影响业务) │
│ │
│ 异常处理: │
│ • Step 3 失败 → 执行 Step 2 补偿 + Step 1 补偿 → ABORTED │
│ • Step 1 超时 → ABORTED(直接中止) │
└─────────────────────────────────────────────────────────────┘
⚠️ 注意事项
- 优先考虑是否真的需要分布式事务——很多场景可以通过业务设计避免
- 补偿操作必须幂等——因为补偿可能被重复执行
- 补偿不一定完美——有些操作(如发送短信)无法真正回滚
- Saga 的隔离性差——中间状态对外可见,需要额外处理
- TCC 的 Try 要轻量——Try 阶段不能做太重的操作
📖 扩展阅读
- Chris Richardson - Microservices Patterns Chapter 4 — Saga 模式详解
- Seata (seata.io) — 阿里开源的分布式事务框架
- Temporal.io — Saga 编排引擎
- Eventuate Tram — Chris Richardson 的事务性发件箱实现
- Distributed Sagas: A Protocol for Coordinating Microservices — Caitie McCaffrey
本章小结
| 方案 | 一致性 | 性能 | 复杂度 | 推荐场景 |
|---|
| 2PC/XA | 强 | 低 | 中 | 传统数据库(不推荐微服务) |
| TCC | 最终 | 中 | 高 | 资金/金融操作 |
| Saga | 最终 | 高 | 中 | 长流程业务 |
| 本地消息表 | 最终 | 高 | 低 | 异步解耦 |
📌 下一章:第 11 章:可观测性 — 链路追踪、日志聚合、指标监控的完整方案。