架构上保证一致性的技术
[!abstract] 一句话定义 架构一致性技术是一组在跨服务、跨数据库、跨消息系统时,让业务状态最终回到正确结果的设计模式;它不只是事务技术,而是事务、消息、幂等、补偿、对账一起组成的工程闭环。
为什么需要它?
在单体应用里,下单、扣库存、扣余额可以放进一个数据库事务;但在微服务里,订单、库存、支付可能属于不同服务和数据库。网络会超时,消息会重复,服务会宕机,某一步成功而下一步失败是常态。架构一致性技术要解决的就是:系统局部失败后,业务结果还能不能被纠正回来。
核心直觉
把分布式业务想成一次跨部门流程:销售部开订单、仓库锁库存、财务扣款、客服发通知。没有一个人能同时控制所有部门,所以不能只靠一句“大家一起提交”。更现实的做法是:
- 关键步骤先留下可追踪的凭证。
- 每个部门的动作都能重复执行而不出错。
- 出错时有反向补偿动作。
- 最后有人定期对账,发现不一致就修正。
这就是架构一致性的核心:不假设每一步都会成功,而是假设失败一定会发生,然后设计恢复路径。
它是怎么工作的?
一致性方案通常不是单一技术,而是按照业务风险选择组合。
总体分层
flowchart TD
A["业务请求"] --> B{"是否必须强一致?"}
B -->|是| C["本地事务 / 2PC / TCC"]
B -->|否| D["最终一致性"]
D --> E["可靠消息 / Outbox"]
D --> F["Saga 补偿事务"]
D --> G["事件溯源 / CQRS"]
E --> H["幂等消费 + 重试 + 死信"]
F --> H
G --> H
H --> I["定时对账与人工修复"]
C --> I
判断顺序
- 能否放进一个本地事务? 能放就别分布式化,本地事务仍然是最简单、最可靠的方案。
- 是否真的需要强一致? 支付扣款、余额转账更接近强一致;发优惠券、发通知、更新搜索索引通常可以最终一致。
- 失败后能否补偿? 能补偿适合 Saga / TCC;不能补偿的动作要尽量后置,或引入人工审核。
- 是否允许中间状态暴露? 秒杀排队、订单待确认是可接受的中间状态;账户余额错误通常不可接受。
- 是否能对账? 不能对账的一致性方案是不完整的,因为你无法发现沉默失败。
关键组件 / 核心要素
| 技术 | 解决什么问题 | 核心代价 |
|---|---|---|
| 本地事务 | 单数据库内的原子性 | 边界不能跨库跨服务 |
| 2PC / XA | 多资源强一致提交 | 阻塞、性能差、协调者复杂 |
| TCC | 跨服务业务级强一致 | 侵入业务,需要 Try/Confirm/Cancel 三套接口 |
| Saga | 长事务的分步执行与补偿 | 只保证最终一致,中间状态会暴露 |
| 可靠消息 | 本地状态变更与消息发送一致 | 需要重试、幂等、死信处理 |
| Outbox 本地消息表 | 避免“数据库提交成功但消息没发出” | 增加消息表、扫描器或 CDC |
| Inbox 消费表 | 避免消息重复消费导致重复扣减 | 每个消费者要记录去重键 |
| 幂等设计 | 让重复请求、重复消息安全 | 需要业务唯一键和状态机约束 |
| 补偿事务 | 失败后执行反向业务动作 | 不是所有动作都可完美撤销 |
| 对账任务 | 发现并修复长期不一致 | 一致性从实时变成可观测、可修复 |
典型方案
1. TCC:业务级两阶段提交
TCC 把一个业务动作拆成三段:
- Try:预留资源,例如冻结余额、锁定库存。
- Confirm:正式提交,例如扣减余额、确认出库。
- Cancel:释放资源,例如解冻余额、释放库存。
sequenceDiagram
participant O as 订单服务
participant S as 库存服务
participant P as 支付服务
O->>S: Try 锁定库存
O->>P: Try 冻结资金
alt 所有 Try 成功
O->>S: Confirm 扣减库存
O->>P: Confirm 扣款
O->>O: 订单确认
else 任一 Try 失败
O->>S: Cancel 释放库存
O->>P: Cancel 解冻资金
O->>O: 订单失败
end
TCC 适合余额、库存、额度这类能“冻结/确认/释放”的资源。它比 2PC 更贴近业务,也更可控,但代价是代码侵入强,每个参与者都要实现空回滚、防悬挂、幂等 Confirm/Cancel。
[!danger] TCC 的三个坑 空回滚:Cancel 先到,但 Try 没执行过,也必须安全返回。
悬挂:Cancel 已执行后,迟到的 Try 不能再成功。
重复提交:Confirm / Cancel 可能被重试,必须幂等。
2. Saga:用补偿动作保证长事务最终一致
Saga 把一个长事务拆成多个本地事务。每一步成功后推进下一步;中途失败时,从后往前执行补偿。
flowchart LR
A["创建订单"] --> B["扣库存"]
B --> C["创建支付单"]
C --> D["发优惠券"]
D --> E["完成"]
C -->|失败| B2["补偿:恢复库存"]
B2 --> A2["补偿:取消订单"]
Saga 适合流程长、参与服务多、允许短暂中间状态的业务,比如订单履约、旅行预订、审批流。它的弱点是补偿不等于回滚:邮件发出不能“撤回”,物流单创建后也可能需要人工处理。
3. 可靠消息:用消息驱动最终一致
最常见的模式是“本地事务 + 消息”:
- 订单服务在本地事务里创建订单。
- 同一个事务里写入 Outbox 消息表。
- 后台任务或 CDC 把 Outbox 消息投递到 MQ。
- 库存服务消费消息,幂等扣库存。
- 消费失败重试,长期失败进入死信队列。
flowchart TD
A["订单服务本地事务"] --> B["写订单表"]
A --> C["写 Outbox 消息表"]
C --> D["投递器 / CDC"]
D --> E["MQ"]
E --> F["库存服务消费"]
F --> G{"消费成功?"}
G -->|是| H["记录 Inbox / ACK"]
G -->|否| I["重试 / 死信 / 告警"]
它解决的是经典问题:数据库提交成功后,服务还没来得及发 MQ 就宕机。Outbox 让“业务状态”和“待发送消息”在同一个本地事务里落库,之后再异步投递。
4. 事件溯源:以事件日志作为事实来源
事件溯源不直接把当前状态当成唯一事实,而是记录导致状态变化的一串事件。订单不是只有一行 status = PAID,而是由 OrderCreated、StockReserved、PaymentSucceeded 等事件推导出来。
它的好处是可追溯、可重放、天然适合审计;代价是系统复杂度明显上升,需要处理事件版本、投影延迟、重放副作用。
5. 对账与修复:最终一致性的最后防线
如果没有对账,最终一致性只是“希望最终一致”。对账任务负责发现这些问题:
- 支付成功但订单仍未支付。
- 库存已扣但订单创建失败。
- Redis 预扣库存与数据库库存不一致。
- MQ 消息进入死信但无人处理。
成熟系统通常会把对账结果写入异常表,支持自动补偿和人工介入。越关键的业务,对账越不能省。
与相关概念的关系
[!info] vs 微服务架构 微服务把业务拆开后,一致性问题才会变得突出。服务越自治,越不能依赖单个数据库事务兜底。
[!info] vs CAP定理 CAP 讲的是分布式系统在网络分区下的一致性与可用性取舍;TCC、Saga、可靠消息是落到业务系统里的工程解法。
[!info] vs 事件驱动架构 事件驱动常用来实现最终一致,但“发了事件”不等于“一致性已解决”。还需要 Outbox、幂等、重试、死信和对账。
[!note] 依赖于 幂等性 分布式系统中超时后通常只能重试,而重试必然带来重复执行。没有幂等,可靠消息和补偿事务都会变成风险源。
如何选型?
| 场景 | 推荐方案 | 原因 |
|---|---|---|
| 单体应用、单数据库 | 本地事务 | 简单可靠,不要过度设计 |
| 跨库但强一致要求高 | TCC 或谨慎使用 2PC | 需要业务级资源预留和确认 |
| 订单、履约、审批等长流程 | Saga | 每步可独立提交,失败后补偿 |
| 下单后发积分、优惠券、通知 | 可靠消息 / Outbox | 允许异步完成,吞吐更好 |
| 秒杀、抢购、库存高并发 | Redis 预扣 + MQ + DB 最终扣减 + 对账 | 用缓存抗峰值,用数据库兜底 |
| 审计要求极强的金融/账务系统 | 事件溯源 + 对账 | 保留完整事实链,便于追溯 |
| 搜索索引、缓存刷新 | 最终一致 + 重试 | 短暂不一致可接受 |
常见误解与陷阱
[!danger] 误以为:用了 MQ 就保证最终一致 实际上:MQ 只负责传递消息,不保证业务正确。生产端可能没发出,消费端可能重复执行,消费失败可能长期堆积。
[!danger] 误以为:补偿就是数据库回滚 实际上:补偿是新的业务动作,可能失败,也可能无法完全恢复原状。比如退款不是“撤销扣款”,而是产生一笔反向账务。
[!danger] 误以为:最终一致就是可以不一致 实际上:最终一致要求系统有明确的收敛机制。没有重试、死信、对账、告警,就只是“失败后没人知道”。
[!danger] 误以为:TCC 一定比 Saga 更高级 实际上:TCC 更强,但也更重。能接受中间状态的长流程用 Saga 往往更自然;需要冻结资源的关键交易才适合 TCC。
工程落地清单
- 业务唯一键:每个请求要有可追踪的业务 ID,例如
orderNo、requestId、eventId。 - 状态机约束:状态只能按合法路径流转,避免“已取消订单又变成已支付”。
- 幂等表 / Inbox:记录已处理消息,重复消息直接返回成功。
- Outbox:业务数据和待发送事件同库同事务写入。
- 重试策略:区分瞬时失败和永久失败,设置退避重试。
- 死信队列:超过重试次数的消息不能静默丢弃。
- 补偿接口:补偿动作也要幂等、可重试、可观测。
- 对账任务:按业务主键周期性核对订单、支付、库存、消息状态。
- 人工处理台:自动修复不了的异常要能查、能改、能留痕。
- 监控告警:关注消息积压、死信数量、补偿失败率、对账差异数。
典型应用场景
- 电商下单 — 订单创建、库存扣减、支付单创建、优惠券使用分属多个子域,需要通过本地事务、补偿和超时取消收敛。
- 秒杀抢购 — Redis 预扣库存承接流量峰值,MQ 异步创建订单,DB 条件扣减防超卖,对账修复缓存和数据库差异。
- 余额转账 — 资金类操作更偏强一致,常用冻结、确认、解冻的 TCC 思路,并辅以流水和对账。
- 积分/优惠券发放 — 用户主流程不必等待,可用可靠消息最终发放;失败进入重试和补偿。
- 跨系统同步 — CRM、ERP、支付渠道之间无法共享事务,只能通过事件、回调、对账维持一致。
延伸阅读
- 想深入理解原理 → 2PC、3PC、CAP、BASE、事务隔离级别。
- 想看工程实践 → TCC 空回滚/悬挂处理、Outbox、Inbox、死信队列、对账系统。
- 想结合秒杀场景 → Redis Lua 预扣、消息削峰、库存补偿、订单超时取消。
关联笔记
前置知识:微服务架构 · CAP定理 · 幂等性 同族概念:事件驱动架构 · CQRS · 事件溯源 · Saga 应用场景:秒杀系统 · 订单系统 · 支付系统