MD 状态:active 分类:系统与架构 更新:2026/5/31

架构上保证一致性的技术

[!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

判断顺序

  1. 能否放进一个本地事务? 能放就别分布式化,本地事务仍然是最简单、最可靠的方案。
  2. 是否真的需要强一致? 支付扣款、余额转账更接近强一致;发优惠券、发通知、更新搜索索引通常可以最终一致。
  3. 失败后能否补偿? 能补偿适合 Saga / TCC;不能补偿的动作要尽量后置,或引入人工审核。
  4. 是否允许中间状态暴露? 秒杀排队、订单待确认是可接受的中间状态;账户余额错误通常不可接受。
  5. 是否能对账? 不能对账的一致性方案是不完整的,因为你无法发现沉默失败。

关键组件 / 核心要素

技术解决什么问题核心代价
本地事务单数据库内的原子性边界不能跨库跨服务
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. 可靠消息:用消息驱动最终一致

最常见的模式是“本地事务 + 消息”:

  1. 订单服务在本地事务里创建订单。
  2. 同一个事务里写入 Outbox 消息表。
  3. 后台任务或 CDC 把 Outbox 消息投递到 MQ。
  4. 库存服务消费消息,幂等扣库存。
  5. 消费失败重试,长期失败进入死信队列。
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,而是由 OrderCreatedStockReservedPaymentSucceeded 等事件推导出来。

它的好处是可追溯、可重放、天然适合审计;代价是系统复杂度明显上升,需要处理事件版本、投影延迟、重放副作用。

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,例如 orderNorequestIdeventId
  • 状态机约束:状态只能按合法路径流转,避免“已取消订单又变成已支付”。
  • 幂等表 / Inbox:记录已处理消息,重复消息直接返回成功。
  • Outbox:业务数据和待发送事件同库同事务写入。
  • 重试策略:区分瞬时失败和永久失败,设置退避重试。
  • 死信队列:超过重试次数的消息不能静默丢弃。
  • 补偿接口:补偿动作也要幂等、可重试、可观测。
  • 对账任务:按业务主键周期性核对订单、支付、库存、消息状态。
  • 人工处理台:自动修复不了的异常要能查、能改、能留痕。
  • 监控告警:关注消息积压、死信数量、补偿失败率、对账差异数。

典型应用场景

  • 电商下单 — 订单创建、库存扣减、支付单创建、优惠券使用分属多个子域,需要通过本地事务、补偿和超时取消收敛。
  • 秒杀抢购 — Redis 预扣库存承接流量峰值,MQ 异步创建订单,DB 条件扣减防超卖,对账修复缓存和数据库差异。
  • 余额转账 — 资金类操作更偏强一致,常用冻结、确认、解冻的 TCC 思路,并辅以流水和对账。
  • 积分/优惠券发放 — 用户主流程不必等待,可用可靠消息最终发放;失败进入重试和补偿。
  • 跨系统同步 — CRM、ERP、支付渠道之间无法共享事务,只能通过事件、回调、对账维持一致。

延伸阅读

  • 想深入理解原理 → 2PC、3PC、CAP、BASE、事务隔离级别。
  • 想看工程实践 → TCC 空回滚/悬挂处理、Outbox、Inbox、死信队列、对账系统。
  • 想结合秒杀场景 → Redis Lua 预扣、消息削峰、库存补偿、订单超时取消。

关联笔记

前置知识微服务架构 · CAP定理 · 幂等性 同族概念事件驱动架构 · CQRS · 事件溯源 · Saga 应用场景秒杀系统 · 订单系统 · 支付系统