SpringNotes

第 21 章:分布式事务

zjc 于 2026-01-21 发布

这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 当一次业务操作跨多个服务或多个数据库时,本地事务无法保证全局原子性。分布式事务的核心不是“永远不出错”,而是定义清晰的中间状态、补偿路径和一致性边界。

21.1 问题模型

下单:

1. 创建订单
2. 预占库存
3. 调用支付
4. 发送履约指令

可能失败:

阶段 失败 处理
创建订单 本地失败 直接返回
预占库存 不足 取消订单
支付 超时 查询支付结果
履约 失败 退款或人工处理

关键问题:支付超时不知道成功还是失败时,不能盲目重试支付,必须先查证。

21.2 BASE 与最终一致

BASE:

Basically Available
Soft State
Eventually Consistent

互联网业务常接受短暂中间态:

订单状态:CREATED
库存状态:RESERVED
支付状态:PAYING

要求:

  1. 状态机合法;
  2. 幂等;
  3. 可重试;
  4. 可补偿;
  5. 有对账;
  6. 有超时处理。

最终一致不是不管,而是自动化补偿和可审计。

21.3 2PC / XA

流程:

Coordinator
  -> prepare 所有参与者
  -> 全部 yes 则 commit
  -> 任一 no 则 rollback

优点:强一致。

缺点:

  1. 锁资源时间长;
  2. 协调者单点;
  3. 阻塞风险;
  4. 吞吐下降;
  5. 运维复杂。

适合金额账务、强一致库内跨库等场景,不适合高并发长链路。

21.4 TCC

三个阶段:

阶段 含义 示例
Try 预留资源 冻结库存
Confirm 确认提交 扣减冻结库存
Cancel 取消释放 解冻库存

表设计:

stock
  available
  frozen

Try:

update stock
set available = available - 10, frozen = frozen + 10
where sku_id = ? and available >= 10;

Confirm:

update stock
set frozen = frozen - 10
where sku_id = ?;

Cancel:

update stock
set available = available + 10, frozen = frozen - 10
where sku_id = ?;

要求:

  1. Try 先做资源检查;
  2. Confirm/Cancel 幂等;
  3. 允许空回滚;
  4. 防悬挂;
  5. 事务日志持久化。

21.5 Saga

Saga 将长事务拆成一组本地事务,每步失败时执行逆向补偿。

正向:
CreateOrder -> ReserveStock -> ChargePayment -> CreateShipment

补偿:
CancelShipment -> RefundPayment -> ReleaseStock -> CancelOrder

两种编排:

模式 特点
编排式 中央协调器维护状态
协同式 服务监听事件并发布下一步

注意:

  1. 有些操作不可自动补偿,如已发货;
  2. Saga 中间状态对用户可见;
  3. 需要并发控制和锁;
  4. 需要状态表和重试任务;
  5. 补偿也必须幂等。

21.6 Outbox

Outbox 解决“本地事务成功但事件发送失败”的问题。

本地事务
  写业务表
  写 outbox_events

异步投递
  扫描 outbox
  发送消息
  更新状态

优点:

  1. 保证事件不因发送失败丢失;
  2. 事件顺序可按聚合 ID 保证;
  3. 易重放;
  4. 与本地事务一致。

配合 CDC(Debezium 等)可以减少扫描和轮询压力。

21.7 幂等设计

唯一业务 ID:

orderId + operation
eventId
paymentFlowId

数据库唯一约束:

create table payment_instructions (
  payment_id varchar(64) primary key,
  order_id varchar(64) not null,
  amount decimal(18,2) not null,
  status varchar(20) not null,
  created_at timestamp not null
);

状态机:

INIT -> PAYING -> SUCCESS
            \-> FAILED
            \-> CLOSED

只允许合法迁移:

update payment_instructions
set status = 'SUCCESS'
where payment_id = ? and status = 'PAYING';

影响行数为 0 表示状态已迁移或指令不存在。

21.8 对账

自动补偿不能覆盖所有异常,必须对账。

数据源:

订单库
支付渠道账单
库存流水
账户流水
消息日志

对账流程:

1. 拉取日切账单
2. 按业务 ID 关联
3. 找金额、状态、时间差异
4. 自动修复可确定差异
5. 人工处理例外
6. 记录处理结果

指标:

recon_total
recon_diff_total
recon_auto_fixed_total
recon_manual_total
recon_max_lag

21.9 事务消息

RocketMQ 支持事务消息:

发送 half 消息
  -> 执行本地事务
  -> commit / rollback
  -> broker 回查本地事务状态

适用:

  1. 本地事务与消息发送需要一致性;
  2. 消费方可接受最终一致;
  3. 有回查接口;
  4. 消费方幂等。

与 Outbox 的区别:事务消息依赖消息中间件能力;Outbox 使用数据库表作为可靠缓冲,更通用但需要投递任务或 CDC。

21.10 方案选择

场景 建议
单服务多表 本地事务
服务间事件通知 Outbox + Kafka
库存预占 TCC 或状态机补偿
长流程订单 Saga
强一致资金入账 XA / TCC / 账务系统设计
已发货回滚 逆向流程,不是简单回滚

选择问题:

  1. 是否允许中间态;
  2. 是否允许短暂不一致;
  3. 失败概率;
  4. 补偿成本;
  5. 吞吐要求;
  6. 审计要求;
  7. 团队运维能力。

本章小结

分布式事务应以业务状态机为核心,结合本地事务、幂等、Outbox、Saga、TCC、事务消息和对账形成完整方案。强一致方案成本高,最终一致方案必须有补偿、重试、监控和人工处理边界。不要把分布式事务当成一个注解,而是当成一套业务流程可靠性设计。

思考题

  1. TCC 如何处理空回滚和悬挂?
  2. Saga 的补偿失败怎么办?
  3. Outbox 解决什么问题?
  4. 支付超时为什么必须查证后处理?
  5. 如何设计订单与支付的对账?