写在前面
本文讲分布式事务——跨多个服务/数据库如何保证数据一致。这是分布式系统最难的问题之一。本文梳理各种方案(2PC、TCC、Saga、本地消息表、事务消息),以及它们各自的取舍。
一、问题:本地事务不够用了
1
2
3
4
5
6
7
8
9
10
11
12
13
| 单库:用数据库事务(ACID)
BEGIN; 扣钱; 加积分; COMMIT; -- 要么全成功,要么全回滚
微服务/分库后:
下单 = 订单服务(订单库)+ 库存服务(库存库)+ 积分服务(积分库)
三个独立数据库,本地事务管不到彼此
问题:
订单创建成功,库存扣减失败 → 数据不一致
积分服务宕机 → 订单已建但积分没加
网络超时 → 不知道对方成功没
需要分布式事务:跨库/跨服务保证最终一致
|
二、2PC(两阶段提交)
1
2
3
4
5
6
7
8
9
10
11
| 协调者(Coordinator)协调多个参与者(Participant):
阶段1:Prepare(准备)
协调者问所有参与者:"能不能提交?"
参与者执行操作、锁资源、写日志,回复 Yes/No
阶段2:Commit / Rollback
全部 Yes → 协调者发 Commit,参与者提交
任一 No → 协调者发 Rollback,参与者回滚
XA 协议就是 2PC
|
1
2
3
4
5
6
7
8
| 优点:强一致(所有参与者要么全提交要么全回滚)
缺点:
✗ 同步阻塞:prepare 阶段所有参与者锁资源,直到 commit
✗ 协调者单点:协调者挂了,参与者卡死
✗ 数据不一致:commit 阶段部分参与者收到、部分没收到
✗ 性能差:两轮网络 + 锁资源
实际很少用(性能太差),银行等强一致场景偶用
|
3PC(三阶段提交)
1
2
3
| 2PC + CanCommit 阶段 + 超时机制
减少阻塞、降低不一致
但更复杂、轮次更多,实际很少用
|
三、TCC(Try-Confirm-Cancel)
业务层面的两阶段,每个服务实现三个方法:
1
2
3
4
5
6
7
8
9
10
| Try — 预留资源(冻结库存、冻结金额)
Confirm — 确认提交(扣减冻结的)
Cancel — 取消(释放冻结的)
下单 TCC:
Try: 订单(创建待确认)、库存(冻结10个)、积分(预加100)
全部 Try 成功 → Confirm
Confirm:订单(确认)、库存(扣减冻结)、积分(实加)
任一 Try 失败 → Cancel
Cancel: 订单(取消)、库存(解冻)、积分(撤销)
|
1
2
3
4
5
6
7
8
9
10
11
| 优点:
✓ 无全局锁,性能好
✓ 最终一致
缺点:
✗ 业务侵入大(每个服务要写 Try/Confirm/Cancel 三套逻辑)
✗ 要保证 Confirm/Cancel 幂等(可能重试)
✗ 要处理空回滚、悬挂(Try 没到却收到 Cancel)
适用:核心交易(支付、扣库存),对一致性要求高
框架:Seata-TCC、Hmily
|
四、Saga
把长事务拆成一串本地事务,每步有对应的补偿操作:
1
2
3
4
5
6
7
8
9
10
11
12
| 下单 Saga:
T1 创建订单 ── 失败补偿 C1(取消订单)
T2 扣减库存 ── 失败补偿 C2(恢复库存)
T3 加积分 ── 失败补偿 C3(扣积分)
T4 发优惠券 ── 失败补偿 C4(收回券)
正向:T1 → T2 → T3 → T4
T3 失败 → 反向补偿:C2 → C1(已执行的逐步回滚)
两种实现:
编排式(Choreography):服务间事件驱动,无中心
协调式(Orchestration):有中心协调器(推荐)
|
1
2
3
4
5
6
7
8
9
10
11
| 优点:
✓ 每步是本地事务,无长锁
✓ 适合长流程业务
缺点:
✗ 没有隔离性(中间状态可见,可能脏读)
✗ 补偿逻辑复杂(不是所有操作都能完美补偿,如发短信)
✗ 需保证补偿幂等
适用:长流程业务(旅行预订、订单履约)
框架:Seata-Saga、Camunda
|
五、本地消息表(最终一致,最常用)
把分布式事务降级为"本地事务 + 消息",保证最终一致:
1
2
3
4
5
6
7
8
9
10
11
12
| 思路:
A 服务执行本地业务 + 写一条"消息"到本地消息表(同一个本地事务)
→ 本地事务保证业务和消息一起成功
后台定时扫描消息表 → 投递到 MQ / 调用 B 服务
→ B 处理成功 → 删除/标记消息
→ 失败重试
A(扣钱)+ 本地消息表(加积分消息) ──同事务──→
↓ 定时扫描
MQ / 调用 B
↓
B(加积分)→ ACK → 删除消息
|
1
2
3
4
5
6
7
8
9
10
11
| 优点:
✓ 简单可靠,不用重型框架
✓ 最终一致
✓ 解耦(A 不直接依赖 B)
缺点:
✗ 消息表要自己维护
✗ 定时扫描有延迟
✗ B 必须幂等(消息可能重复投递)
适用:绝大多数最终一致场景(最接地气)
|
六、事务消息(RocketMQ)
类似本地消息表,但 MQ 内置支持,不用自己维护消息表:
1
2
3
4
5
6
7
8
| RocketMQ 事务消息:
1. 发送「半消息」到 MQ(消费者看不到)
2. 执行本地事务(扣钱)
3. 本地事务成功 → 提交半消息(消费者可见)
本地事务失败 → 回滚半消息
4. 如果没收到提交/回滚 → MQ 回查本地事务状态
本质:把"本地事务 + 发消息"做成原子
|
1
2
3
| 优点:不用维护消息表,MQ 托管
缺点:依赖 RocketMQ(其他 MQ 没这功能)
适用:用 RocketMQ 的项目
|
七、方案对比
1
2
3
4
5
6
7
| 方案 一致性 性能 复杂度 业务侵入 适用
────────────────────────────────────────────────────────
2PC/3PC 强 差 中 低 强一致(银行)
TCC 强 好 高 高 核心交易
Saga 最终 好 中 中 长流程业务
本地消息表 最终 好 低 中 大多数场景
事务消息 最终 好 低 中 RocketMQ 项目
|
八、最佳实践:尽量避免分布式事务
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| 分布式事务成本高,最优解是"不引入它":
1. 合理的服务/库拆分
强相关的数据放一起(同库同事务)
只有真正独立的才拆服务
2. 最终一致 + 幂等
大多数场景不需要强一致,最终一致 + 重试幂等就够
3. 事件驱动解耦
服务间用事件/消息,而非同步调用
天然异步、可重试
4. 业务容忍
设计上容忍短暂不一致(如积分晚几秒到账)
能用本地事务就用本地事务
实在要分布式 → 优先最终一致(本地消息表/事务消息)
核心强一致 → TCC
|
九、小结
- 2PC:强一致但阻塞、单点、性能差,少用
- TCC:Try/Confirm/Cancel 业务两阶段,性能好但侵入大
- Saga:长事务拆串 + 补偿,适合长流程,无隔离
- 本地消息表:本地事务 + 消息 + 重试,最常用的最终一致方案
- 事务消息:RocketMQ 内置的原子"业务+发消息"
- 核心:强一致用 TCC/2PC,最终一致用消息表/事务消息
- 最佳实践:尽量避免分布式事务,优先最终一致 + 幂等
下一篇讲复制与 Gossip 协议。