一句话总结
微服务拆分后,跨服务不再有数据库事务.
分布式一致性的核心问题是:如何在"没有全局事务"的前提下,保证跨服务业务操作的最终正确性.
答案不是分布式事务,而是 Saga + 补偿 + 最终一致性.
从一个问题出发
电商下单流程:创建订单,扣减库存,扣款.单体时代,三步在一个数据库事务里:
|
拆成微服务后,订单,库存,支付各有自己的数据库.没有跨库事务--怎么保证三步要么全成功,要么全回滚?
|
为什么不用分布式事务(2PC)
经典的两阶段提交(2PC,Two-Phase Commit)确实能实现跨库事务:
|
但微服务场景下 2PC 有致命问题:
| 问题 | 具体表现 |
|---|---|
| 同步阻塞 | Prepare 后所有参与者锁住资源等待 Commit--一个慢节点卡住整条链路 |
| 单点故障 | TM 挂了,所有参与者永远锁着资源等待指令 |
| 性能差 | 全程持锁,吞吐量极低 |
| 强耦合 | 所有参与者必须支持 XA 协议,跨异构系统困难 |
| 不适合长事务 | 涉及外部 API(如支付渠道回调)可能等几分钟--锁几分钟不现实 |
微服务中不要用 2PC
2PC 适合同构数据库之间的短事务(如同一个 MySQL 集群内的分库分表).
跨微服务时,服务自治,异构存储,长流程,网络不可靠--2PC 的假设全部被打破.
正确的答案是放弃强一致性,拥抱最终一致性.
最终一致性(Eventual Consistency)
核心思想:不要求所有服务在同一时刻一致,但保证经过一段时间后达到一致状态.
强一致性 = 银行柜台转账,柜员确认双方余额同时更新后才放你走.
最终一致性 = 跨行转账,你转出的钱不会立刻到达对方账户(可能几秒到几小时),但最终一定会到.
中间状态(你少了钱但对方还没收到)是允许的.
在微服务中接受最终一致性意味着:
- 每个服务只保证自己内部的强一致性(本地事务)
- 跨服务的一致性通过异步消息 + 补偿机制保证
- 系统可能存在短暂的中间状态--业务设计必须容忍这一点
Saga 模式
Saga 是微服务分布式一致性的标准解决方案.核心思想:把一个长事务拆成多个本地事务,每个本地事务有对应的补偿操作.如果某一步失败,逐步执行前面步骤的补偿来"回滚".
基本结构
|
两种编排方式
| 编排式(Orchestration) | 协同式(Choreography) | |
|---|---|---|
| 协调者 | 有一个中央 Saga 编排器 | 没有中央协调者,事件驱动 |
| 通信 | 编排器 → 各服务(命令) | 服务之间发布/订阅事件 |
| 流程可见性 | 编排器里一目了然 | 分散在各服务,难以追踪全局 |
| 耦合度 | 编排器知道所有参与者 | 服务之间不直接知道彼此 |
| 适用场景 | 流程复杂(> 4 步),需要条件分支 | 流程简单(2-3 步),参与者解耦 |
编排式 Saga 示例
|
|
协同式 Saga 示例
|
每个服务监听事件并做出反应--没有中央编排器.简单流程合适,但步骤一多就难以追踪全局状态.
选择建议
流程 ≤ 3 步且参与者固定 → 协同式(简单解耦).
流程 > 3 步或有条件分支 → 编排式(清晰可控).
实践中大多数订单/支付流程用编排式--可追溯,可重试,状态清晰.
补偿设计
补偿不是真正的"回滚"--它是一个新的正向操作,将业务语义恢复到之前的状态.
补偿设计原则
- 补偿操作必须幂等:可能被重复调用(重试,消息重复投递).幂等实现方式详见 03 篇的去重表模式
- 补偿可能失败:需要自己的重试机制和最终人工介入
- 补偿有语义:不是 DELETE 回滚行."取消订单"会生成一条取消记录,而非删除订单
- 考虑不可逆操作:如果已经发了通知邮件--补偿是"发一封道歉邮件",而非时光倒流
补偿不等于撤销
有些操作没有完美的补偿.
已经扣了用户的钱 → 补偿是退款(可能有手续费差异).
已经发了短信 → 没法收回.
设计 Saga 时必须考虑每一步的补偿是否存在,是否有副作用.
TCC 模式
TCC(Try-Confirm-Cancel)是 Saga 的变体,把每个步骤分成三个阶段:
| 阶段 | 含义 | 示例(库存) |
|---|---|---|
| Try | 预留资源(但不真正扣减) | 冻结 1 件库存(available-1, frozen+1) |
| Confirm | 确认提交(真正扣减) | 确认扣减(frozen-1) |
| Cancel | 取消预留(释放资源) | 解冻(frozen-1, available+1) |
|
TCC vs Saga
| Saga | TCC | |
|---|---|---|
| 隔离性 | 低(中间状态可见) | 高(Try 阶段只是预留) |
| 侵入性 | 低(只需正向+补偿) | 高(每个服务要实现 Try/Confirm/Cancel) |
| 性能 | 好(每步只调一次) | 差(每步调两次:Try + Confirm) |
| 适用 | 大多数场景 | 资源预留场景(库存冻结,余额冻结) |
大多数场景用 Saga 即可
TCC 的隔离性更好(预留阶段不影响其他事务),但实现成本很高--每个参与者都要支持三个接口.
除非你的业务对"超卖"极度敏感(如限量商品秒杀),否则 Saga + 幂等补偿足够.
实用一致性模式
发件箱模式(回顾)
03 篇详细讲过--确保"本地事务 + 发消息"的原子性.这是 Saga 编排的基础:每一步完成后通过 outbox 可靠发出事件/命令.
幂等接收方
Saga 的每一步(包括补偿)都可能因为重试而被调用多次.接收方必须幂等--03 篇的去重表模式直接复用.
状态机驱动
将业务实体建模为状态机,每次状态变迁对应一个 Saga 步骤:
|
读己所写(Read Your Writes)
用户提交订单后立刻跳转到订单详情页--但此时异步流程可能还没完成(库存还没扣减).解决方案:
- 乐观 UI:前端先展示"处理中"状态,异步轮询/WebSocket 推送最终状态
- 同步核心链路:关键确认步骤同步完成后再返回,非核心步骤异步
- 读写同源:创建后的读请求路由到主库而非从库(避免复制延迟导致读不到)
一致性与可用性的取舍
CAP 定理在微服务中的实际影响:
| 选择 | 含义 | 典型场景 |
|---|---|---|
| 强一致(CP) | 牺牲可用性保证一致 | 银行转账核心账务 |
| 最终一致(AP) | 牺牲即时一致保证可用 | 电商订单,社交 feed,推荐 |
实践准则
80% 的业务场景接受最终一致性:用户不会发现几百毫秒的延迟.
20% 的核心场景(资金,库存上限)需要更强的保证--用 TCC 预留或同步确认.
不要为了追求"完美一致"而让系统性能和可用性崩塌.
Saga 失败处理
补偿本身也可能失败--这是 Saga 最复杂的部分:
| 场景 | 处理方式 |
|---|---|
| 补偿操作网络超时 | 重试(补偿必须幂等所以重试安全) |
| 补偿操作持续失败 | 告警 + 人工介入 |
| Saga 编排器自身故障 | 持久化 Saga 状态,重启后从断点恢复 |
| 消息丢失(协同式) | 定时任务扫描"卡住"的订单,重新触发 |
Saga 状态必须持久化
Saga 编排器必须将当前执行到哪一步,各步结果存入数据库.
如果编排器进程崩溃,重启后能从持久化状态恢复执行.
内存中的 Saga 状态 = 进程挂了就丢了 = 业务不一致.
|
快速回顾
- 微服务无跨库事务:放弃 2PC,拥抱最终一致性
- Saga:长事务拆成本地事务序列 + 补偿操作,是标准解法
- 编排式(中央协调器)适合复杂流程;协同式(事件驱动)适合简单流程
- 补偿必须幂等,补偿不是"撤销"而是新的正向操作
- TCC:先预留再确认,隔离性好但实现成本高,仅在资源敏感场景使用
- 状态机驱动 Saga 步骤,保证状态变迁的合法性
- Saga 状态持久化是必须的--内存状态 = 崩溃即丢失
- 80% 场景接受最终一致,20% 核心场景用 TCC / 同步确认
动手练习
- 设计 Saga 补偿流程:画出"创建订单"Saga 的正向流程(创建订单→扣库存→扣款→发通知)和补偿流程
- 实现 Saga 编排器:用 Go 编排 3 个步骤(可以 mock 服务),第 3 步随机失败时触发前 2 步的补偿
- 加入状态持久化:将 Saga 执行状态写入数据库,模拟中途进程崩溃后恢复执行
- 评估一致性需求:你当前工作中的哪些跨服务操作需要一致性保证?哪些可以接受最终一致,哪些需要更强保证?