阶段二 · 服务治理与一致性

分布式数据一致性

一句话总结

微服务拆分后,跨服务不再有数据库事务.
分布式一致性的核心问题是:如何在"没有全局事务"的前提下,保证跨服务业务操作的最终正确性.
答案不是分布式事务,而是 Saga + 补偿 + 最终一致性.

从一个问题出发

电商下单流程:创建订单,扣减库存,扣款.单体时代,三步在一个数据库事务里:

BEGIN;
INSERT INTO orders (...);
UPDATE inventory SET stock = stock - 1 WHERE product_id = 'p1';
UPDATE accounts SET balance = balance - 299 WHERE user_id = 'u1';
COMMIT;
-- 要么全成功,要么全回滚

拆成微服务后,订单,库存,支付各有自己的数据库.没有跨库事务--怎么保证三步要么全成功,要么全回滚?

场景:订单创建成功,库存扣减成功,支付失败

单体:ROLLBACK → 库存恢复,订单删除
微服务:
订单服务已 COMMIT
库存服务已 COMMIT
支付服务失败
→ 怎么回滚前两步?它们已经各自 COMMIT 了!

为什么不用分布式事务(2PC)

经典的两阶段提交(2PC,Two-Phase Commit)确实能实现跨库事务:

sequenceDiagram
participant TM as Transaction Manager
participant DB1 as 订单 DB
participant DB2 as 库存 DB
participant DB3 as 支付 DB

TM->>DB1: Prepare
TM->>DB2: Prepare
TM->>DB3: Prepare
DB1-->>TM: Ready
DB2-->>TM: Ready
DB3-->>TM: Ready
TM->>DB1: Commit
TM->>DB2: Commit
TM->>DB3: Commit

但微服务场景下 2PC 有致命问题:

问题 具体表现
同步阻塞 Prepare 后所有参与者锁住资源等待 Commit--一个慢节点卡住整条链路
单点故障 TM 挂了,所有参与者永远锁着资源等待指令
性能差 全程持锁,吞吐量极低
强耦合 所有参与者必须支持 XA 协议,跨异构系统困难
不适合长事务 涉及外部 API(如支付渠道回调)可能等几分钟--锁几分钟不现实

微服务中不要用 2PC

2PC 适合同构数据库之间的短事务(如同一个 MySQL 集群内的分库分表).
跨微服务时,服务自治,异构存储,长流程,网络不可靠--2PC 的假设全部被打破.
正确的答案是放弃强一致性,拥抱最终一致性.

最终一致性(Eventual Consistency)

核心思想:不要求所有服务在同一时刻一致,但保证经过一段时间后达到一致状态.

强一致性 = 银行柜台转账,柜员确认双方余额同时更新后才放你走.
最终一致性 = 跨行转账,你转出的钱不会立刻到达对方账户(可能几秒到几小时),但最终一定会到.
中间状态(你少了钱但对方还没收到)是允许的.

在微服务中接受最终一致性意味着:

  • 每个服务只保证自己内部的强一致性(本地事务)
  • 跨服务的一致性通过异步消息 + 补偿机制保证
  • 系统可能存在短暂的中间状态--业务设计必须容忍这一点

Saga 模式

Saga 是微服务分布式一致性的标准解决方案.核心思想:把一个长事务拆成多个本地事务,每个本地事务有对应的补偿操作.如果某一步失败,逐步执行前面步骤的补偿来"回滚".

基本结构

正向流程:T1 → T2 → T3 → T4(全部成功 = 业务完成)

T3 失败时的补偿流程:
T1 → T2 → T3(失败) → C2 → C1

T1 = 创建订单 C1 = 取消订单
T2 = 扣减库存 C2 = 恢复库存
T3 = 扣款 C3 = 退款

两种编排方式

编排式(Orchestration) 协同式(Choreography)
协调者 有一个中央 Saga 编排器 没有中央协调者,事件驱动
通信 编排器 → 各服务(命令) 服务之间发布/订阅事件
流程可见性 编排器里一目了然 分散在各服务,难以追踪全局
耦合度 编排器知道所有参与者 服务之间不直接知道彼此
适用场景 流程复杂(> 4 步),需要条件分支 流程简单(2-3 步),参与者解耦

编排式 Saga 示例

sequenceDiagram
participant O as Saga Orchestrator
participant OS as 订单服务
participant IS as 库存服务
participant PS as 支付服务

O->>OS: 创建订单(pending)
OS-->>O: 订单已创建
O->>IS: 扣减库存
IS-->>O: 库存已扣减
O->>PS: 扣款
PS-->>O: 扣款失败!
Note over O: 触发补偿
O->>IS: 恢复库存(补偿)
IS-->>O: 库存已恢复
O->>OS: 取消订单(补偿)
OS-->>O: 订单已取消
// Saga 编排器伪代码
func CreateOrderSaga(ctx context.Context, req CreateOrderRequest) error {
// Step 1: 创建订单
order, err := orderService.Create(ctx, req)
if err != nil { return err }

// Step 2: 扣减库存
err = inventoryService.Reserve(ctx, order.Items)
if err != nil {
// 补偿 Step 1
orderService.Cancel(ctx, order.ID)
return err
}

// Step 3: 扣款
err = paymentService.Charge(ctx, order.UserID, order.Total)
if err != nil {
// 补偿 Step 2 + Step 1
inventoryService.Release(ctx, order.Items)
orderService.Cancel(ctx, order.ID)
return err
}

// 全部成功 → 确认订单
orderService.Confirm(ctx, order.ID)
return nil
}

协同式 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 流程:
1. 协调者发起 Try: 订单.Try + 库存.Try + 支付.Try
2. 全部 Try 成功 → 发起 Confirm: 订单.Confirm + 库存.Confirm + 支付.Confirm
3. 任何 Try 失败 → 发起 Cancel: 对已 Try 成功的发 Cancel

vs Saga:
Saga 是正向执行 → 失败时补偿
TCC 是先预留 → 全部预留成功后才确认

TCC vs Saga

Saga TCC
隔离性 低(中间状态可见) 高(Try 阶段只是预留)
侵入性 低(只需正向+补偿) 高(每个服务要实现 Try/Confirm/Cancel)
性能 好(每步只调一次) 差(每步调两次:Try + Confirm)
适用 大多数场景 资源预留场景(库存冻结,余额冻结)

大多数场景用 Saga 即可

TCC 的隔离性更好(预留阶段不影响其他事务),但实现成本很高--每个参与者都要支持三个接口.
除非你的业务对"超卖"极度敏感(如限量商品秒杀),否则 Saga + 幂等补偿足够.

实用一致性模式

发件箱模式(回顾)

03 篇详细讲过--确保"本地事务 + 发消息"的原子性.这是 Saga 编排的基础:每一步完成后通过 outbox 可靠发出事件/命令.

幂等接收方

Saga 的每一步(包括补偿)都可能因为重试而被调用多次.接收方必须幂等--03 篇的去重表模式直接复用.

状态机驱动

将业务实体建模为状态机,每次状态变迁对应一个 Saga 步骤:

// 订单状态机
const (
OrderPending = "pending" // 已创建,等待库存确认
OrderConfirmed = "confirmed" // 库存+支付确认
OrderCancelled = "cancelled" // 补偿完成
OrderCompleted = "completed" // 最终完成
)

// 状态变迁只允许合法路径
var validTransitions = map[string][]string{
OrderPending: {OrderConfirmed, OrderCancelled},
OrderConfirmed: {OrderCompleted, OrderCancelled},
}

func (o *Order) TransitionTo(newState string) error {
allowed := validTransitions[o.State]
if !contains(allowed, newState) {
return fmt.Errorf("invalid transition: %s → %s", o.State, newState)
}
o.State = newState
return nil
}

读己所写(Read Your Writes)

用户提交订单后立刻跳转到订单详情页--但此时异步流程可能还没完成(库存还没扣减).解决方案:

  • 乐观 UI:前端先展示"处理中"状态,异步轮询/WebSocket 推送最终状态
  • 同步核心链路:关键确认步骤同步完成后再返回,非核心步骤异步
  • 读写同源:创建后的读请求路由到主库而非从库(避免复制延迟导致读不到)

一致性与可用性的取舍

CAP 定理在微服务中的实际影响:

选择 含义 典型场景
强一致(CP) 牺牲可用性保证一致 银行转账核心账务
最终一致(AP) 牺牲即时一致保证可用 电商订单,社交 feed,推荐

实践准则

80% 的业务场景接受最终一致性:用户不会发现几百毫秒的延迟.
20% 的核心场景(资金,库存上限)需要更强的保证--用 TCC 预留或同步确认.
不要为了追求"完美一致"而让系统性能和可用性崩塌.

Saga 失败处理

补偿本身也可能失败--这是 Saga 最复杂的部分:

场景 处理方式
补偿操作网络超时 重试(补偿必须幂等所以重试安全)
补偿操作持续失败 告警 + 人工介入
Saga 编排器自身故障 持久化 Saga 状态,重启后从断点恢复
消息丢失(协同式) 定时任务扫描"卡住"的订单,重新触发

Saga 状态必须持久化

Saga 编排器必须将当前执行到哪一步,各步结果存入数据库.
如果编排器进程崩溃,重启后能从持久化状态恢复执行.
内存中的 Saga 状态 = 进程挂了就丢了 = 业务不一致.

// Saga 状态持久化 + 恢复(核心逻辑)
type SagaState struct {
SagaID string
CurrentStep int
StepResults []StepResult // 每步的执行结果
Status string // "running" / "compensating" / "completed" / "failed"
}

func (s *SagaOrchestrator) Resume(ctx context.Context, sagaID string) error {
// 从数据库恢复状态
state, _ := s.repo.Load(ctx, sagaID)

if state.Status == "compensating" {
// 从断点继续补偿
return s.compensateFrom(ctx, state, state.CurrentStep)
}
// 从断点继续正向执行
return s.executeFrom(ctx, state, state.CurrentStep)
}

快速回顾

  • 微服务无跨库事务:放弃 2PC,拥抱最终一致性
  • Saga:长事务拆成本地事务序列 + 补偿操作,是标准解法
  • 编排式(中央协调器)适合复杂流程;协同式(事件驱动)适合简单流程
  • 补偿必须幂等,补偿不是"撤销"而是新的正向操作
  • TCC:先预留再确认,隔离性好但实现成本高,仅在资源敏感场景使用
  • 状态机驱动 Saga 步骤,保证状态变迁的合法性
  • Saga 状态持久化是必须的--内存状态 = 崩溃即丢失
  • 80% 场景接受最终一致,20% 核心场景用 TCC / 同步确认

动手练习

  1. 设计 Saga 补偿流程:画出"创建订单"Saga 的正向流程(创建订单→扣库存→扣款→发通知)和补偿流程
  2. 实现 Saga 编排器:用 Go 编排 3 个步骤(可以 mock 服务),第 3 步随机失败时触发前 2 步的补偿
  3. 加入状态持久化:将 Saga 执行状态写入数据库,模拟中途进程崩溃后恢复执行
  4. 评估一致性需求:你当前工作中的哪些跨服务操作需要一致性保证?哪些可以接受最终一致,哪些需要更强保证?