这是《Spring Boot 与 Spring Cloud 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 当一次业务操作跨多个服务或多个数据库时,本地事务无法保证全局原子性。分布式事务的核心不是“永远不出错”,而是定义清晰的中间状态、补偿路径和一致性边界。
21.1 问题模型
下单:
1. 创建订单
2. 预占库存
3. 调用支付
4. 发送履约指令
可能失败:
| 阶段 | 失败 | 处理 |
|---|---|---|
| 创建订单 | 本地失败 | 直接返回 |
| 预占库存 | 不足 | 取消订单 |
| 支付 | 超时 | 查询支付结果 |
| 履约 | 失败 | 退款或人工处理 |
关键问题:支付超时不知道成功还是失败时,不能盲目重试支付,必须先查证。
21.2 BASE 与最终一致
BASE:
Basically Available
Soft State
Eventually Consistent
互联网业务常接受短暂中间态:
订单状态:CREATED
库存状态:RESERVED
支付状态:PAYING
要求:
- 状态机合法;
- 幂等;
- 可重试;
- 可补偿;
- 有对账;
- 有超时处理。
最终一致不是不管,而是自动化补偿和可审计。
21.3 2PC / XA
流程:
Coordinator
-> prepare 所有参与者
-> 全部 yes 则 commit
-> 任一 no 则 rollback
优点:强一致。
缺点:
- 锁资源时间长;
- 协调者单点;
- 阻塞风险;
- 吞吐下降;
- 运维复杂。
适合金额账务、强一致库内跨库等场景,不适合高并发长链路。
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 = ?;
要求:
- Try 先做资源检查;
- Confirm/Cancel 幂等;
- 允许空回滚;
- 防悬挂;
- 事务日志持久化。
21.5 Saga
Saga 将长事务拆成一组本地事务,每步失败时执行逆向补偿。
正向:
CreateOrder -> ReserveStock -> ChargePayment -> CreateShipment
补偿:
CancelShipment -> RefundPayment -> ReleaseStock -> CancelOrder
两种编排:
| 模式 | 特点 |
|---|---|
| 编排式 | 中央协调器维护状态 |
| 协同式 | 服务监听事件并发布下一步 |
注意:
- 有些操作不可自动补偿,如已发货;
- Saga 中间状态对用户可见;
- 需要并发控制和锁;
- 需要状态表和重试任务;
- 补偿也必须幂等。
21.6 Outbox
Outbox 解决“本地事务成功但事件发送失败”的问题。
本地事务
写业务表
写 outbox_events
异步投递
扫描 outbox
发送消息
更新状态
优点:
- 保证事件不因发送失败丢失;
- 事件顺序可按聚合 ID 保证;
- 易重放;
- 与本地事务一致。
配合 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 回查本地事务状态
适用:
- 本地事务与消息发送需要一致性;
- 消费方可接受最终一致;
- 有回查接口;
- 消费方幂等。
与 Outbox 的区别:事务消息依赖消息中间件能力;Outbox 使用数据库表作为可靠缓冲,更通用但需要投递任务或 CDC。
21.10 方案选择
| 场景 | 建议 |
|---|---|
| 单服务多表 | 本地事务 |
| 服务间事件通知 | Outbox + Kafka |
| 库存预占 | TCC 或状态机补偿 |
| 长流程订单 | Saga |
| 强一致资金入账 | XA / TCC / 账务系统设计 |
| 已发货回滚 | 逆向流程,不是简单回滚 |
选择问题:
- 是否允许中间态;
- 是否允许短暂不一致;
- 失败概率;
- 补偿成本;
- 吞吐要求;
- 审计要求;
- 团队运维能力。
本章小结
分布式事务应以业务状态机为核心,结合本地事务、幂等、Outbox、Saga、TCC、事务消息和对账形成完整方案。强一致方案成本高,最终一致方案必须有补偿、重试、监控和人工处理边界。不要把分布式事务当成一个注解,而是当成一套业务流程可靠性设计。
思考题
- TCC 如何处理空回滚和悬挂?
- Saga 的补偿失败怎么办?
- Outbox 解决什么问题?
- 支付超时为什么必须查证后处理?
- 如何设计订单与支付的对账?