分布式系统学习笔记(四):分布式事务

写在前面

本文讲分布式事务——跨多个服务/数据库如何保证数据一致。这是分布式系统最难的问题之一。本文梳理各种方案(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 协议。