目录
正在加载目录…
专栏文章
专栏文章
分布式系统系列
1. 分布式一致性 2. 分布式事务 3. 分布式协调与选举 4. 分布式系统设计模式 5. 金融级分布式系统实践

分布式系统设计模式

发布于 2026-05-28 14:04 · 最后编辑于 2026-07-31 15:55 · 字数 3,946 👁 73 次阅读

本文系统梳理分布式系统的核心设计模式:从分布式的本质挑战(网络分区/部分失败/时钟偏斜),到隔离、重试、熔断等弹性模式,再到 Event Sourcing、CQRS、Outbox 等数据一致性模式。读完能在工程中识别问题并套用合适的模式,而不是每次重新发明轮子。

相关文章分布式一致性 · 分布式事务 · 分布式协调与选举 · 金融级分布式系统实践

目录

章节说明
分布式系统的本质挑战网络分区/部分失败/时钟偏斜的根本原因
弹性模式:隔离与限流Bulkhead 舱壁、令牌桶/漏桶
弹性模式:重试与熔断Retry + Backoff + Jitter、Circuit Breaker 三状态机
数据一致性模式Outbox、Event Sourcing、CQRS
Saga 补偿模式编排式 vs 协调式,与 TCC 对比
Sidecar 与 Leader Election基础设施解耦模式
模式选型速查场景 → 推荐模式

分布式系统的本质挑战

分布式系统的根本难点不是性能,而是部分失败(Partial Failure)——单机要么全挂要么全好,分布式系统可以只有一部分坏掉,而你不知道哪部分坏了。

dist patterns overview

三大核心挑战

挑战含义为什么难处理
网络分区(Network Partition)节点间消息丢失或高延迟,节点各自认为对方挂了无法区分"对方挂了"和"网络断了"
部分失败(Partial Failure)任意节点在任意时刻可能崩溃,且崩溃前可能已执行了部分操作操作可能执行了但响应未返回(幂等性问题的根源)
时钟偏斜(Clock Skew)不同机器的物理时钟不同步,无法用时间戳判断事件先后导致"谁先写"问题,是并发冲突的根源

为什么单机方案在分布式下失效

单机:事务要么全成功要么全失败(ACID 保证)
分布式:
  - 请求发出后网络断了,不知道对方有没有收到
  - 对方收到了,执行到一半机器宕机
  - 对方执行完了,但响应在回来路上丢了
→ 结果:操作可能执行了 0 次、1 次或多次

核心结论:分布式系统的设计模式,本质上都是在应对"不确定性"——通过幂等性(多次执行等价于一次)、补偿(撤销已执行的操作)和最终一致性(允许短暂不一致但最终收敛)来处理这种不确定性。

弹性模式:隔离与限流

Bulkhead 舱壁隔离

背景:船舱设计了多个隔水舱,某个舱进水不会导致整艘船沉没。分布式系统中,一个下游服务变慢会导致调用它的线程池耗尽,进而拖垮整个服务(级联故障)。

两种实现方式

方式原理适用场景
线程池隔离每个下游服务使用独立线程池,池满时直接拒绝而不影响其他服务需要严格隔离,调用耗时较长
信号量隔离用计数器限制并发数,超限直接拒绝,不新建线程调用耗时短,不希望线程切换开销
线程池隔离示意:
┌─────────────────────────────────────┐
│           应用服务                   │
│  ┌──────────┐  ┌──────────┐         │
│  │ 服务A池  │  │ 服务B池  │  ...    │
│  │ 10线程   │  │ 10线程   │         │
│  └────┬─────┘  └────┬─────┘         │
└───────┼─────────────┼───────────────┘
        ↓             ↓
      服务A          服务B(变慢)

关键点:服务 B 变慢导致其线程池耗尽,但服务 A 的线程池完全不受影响。没有隔离时,服务 B 变慢会逐渐占满所有线程,最终整个应用无响应。

限流:令牌桶 vs 漏桶

算法原理特点
漏桶(Leaky Bucket)请求放入固定容量的桶,以固定速率流出;桶满则丢弃严格匀速输出,平滑流量;无法应对突发
令牌桶(Token Bucket)以固定速率向桶中放令牌,每个请求消耗一个令牌;桶空则拒绝允许一定程度的突发(桶中积累的令牌)

选型原则

  • 发送方限速(防止自己发太快)→ 漏桶
  • 接收方保护(允许突发但有上限)→ 令牌桶
  • 需要动态调整处理速率 → 令牌桶(调整放令牌速率即可)

弹性模式:重试与熔断

Retry + Backoff + Jitter

为什么需要重试:网络抖动、服务短暂不可用是常态,大多数失败是暂时性的。

为什么需要 Backoff(退避):立即重试只会加重已经过载的服务。

为什么需要 Jitter(抖动):所有客户端同时退避相同时间后同时重试,会产生"惊群效应"(Thundering Herd)。

朴素重试(错误):
t=0 失败 → t=0.1s 重试 → t=0.1s 重试 → ...(持续冲击)

指数退避(改进):
t=0 失败 → t=1s 重试 → t=2s 重试 → t=4s 重试 ...

指数退避 + Jitter(正确):
t=0 失败 → t=1±0.5s 重试 → t=2±1s 重试 → t=4±2s 重试
(各客户端的重试时间随机散开,避免惊群)

实现要点

  • 只对幂等操作重试(GET、PUT),非幂等操作(POST)需要先确保幂等性
  • 设置最大重试次数,避免无限重试
  • 区分可重试错误(超时、5xx)和不可重试错误(4xx 客户端错误)

Circuit Breaker 熔断器

背景:重试会让已经过载的服务雪上加霜。当下游持续失败时,应该"断开电路",快速失败而不是继续等待。

三状态机

flowchart LR
    CLOSED["关闭(正常)<br/>请求正常通过<br/>统计失败率"] -->|失败率超阈值| OPEN["打开(熔断)<br/>直接拒绝所有请求<br/>快速失败"]
    OPEN -->|等待冷却时间| HALF["半开(探测)<br/>放行少量请求<br/>探测服务是否恢复"]
    HALF -->|探测请求成功| CLOSED
    HALF -->|探测请求失败| OPEN
    style CLOSED fill:#cfc,stroke:#060
    style OPEN fill:#fcc,stroke:#c00
    style HALF fill:#ffc,stroke:#660
状态行为转换条件
Closed(关闭)正常通过,统计失败率失败率超过阈值 → Open
Open(打开)直接拒绝,快速失败冷却时间到 → Half-Open
Half-Open(半开)放行少量探测请求成功 → Closed;失败 → Open

关键参数

  • 失败率阈值(如 50%)
  • 统计时间窗口(如 60s 内的请求)
  • 冷却时间(Open 状态持续多久)
  • 半开时的探测请求数

Circuit Breaker vs Retry 的关系:两者配合使用。Retry 应对偶发性失败;Circuit Breaker 应对持续性失败,防止 Retry 放大问题。熔断器打开时,Retry 应该直接停止。

数据一致性模式

Outbox Pattern(本地消息表)

问题:服务需要同时完成数据库写入和消息发布,这是两个不同的系统,无法用单一事务保证原子性。

错误做法:
1. 写数据库 ✓
2. 发消息   ✗(网络故障)
→ 数据库有数据,但下游没收到消息

Outbox 解法:将消息写入同一个数据库的 outbox 表,与业务操作在同一事务中完成。再由独立的投递器异步发送。

sequenceDiagram
    participant S as 业务服务
    participant DB as 数据库
    participant P as 消息投递器
    participant MQ as 消息队列
    S->>DB: BEGIN TRANSACTION
    S->>DB: 写业务数据
    S->>DB: 写 outbox 表(status=PENDING)
    S->>DB: COMMIT(原子保证)
    loop 轮询 outbox
        P->>DB: 查询 PENDING 消息
        P->>MQ: 发送消息
        P->>DB: 更新 status=SENT
    end

关键点

  • 业务数据和消息记录在同一事务中,天然原子
  • 消息投递失败可重试(消息仍在 outbox 表中)
  • 消费方必须做幂等处理(消息可能重复投递)
  • 适合已有关系型数据库的场景,不引入新的基础设施

Event Sourcing 事件溯源

核心思想:不存储"当前状态",而是存储导致状态变化的事件序列。当前状态通过重放事件序列计算得出。

三个核心概念

概念含义类比
Command(命令)外部发出的指令,可能被拒绝"我想转账 100 元"
Event(事件)命令被验证通过后产生的事实,一定会执行"已转账 100 元(transferred)"
State(状态)所有历史事件累积的结果账户余额 = 所有存取款事件之和
数学本质:
Sn = f(Sn-1, en)   → 当前状态 = 函数(前一状态, 当前事件)
Sn = S0 + e1 + e2 + ... + en  → 状态 = 初始状态 + 所有事件之和

事件溯源的核心约束(不可违反):

  • 自动机执行不能有随机性(保证可重现)
  • 事件必须有全序(顺序不同结果不同)
  • 事件只追加不修改(immutable)

快照(Snapshot)优化

无快照:恢复状态需重放所有历史事件(可能有百万条)
有快照:从最近快照 + 快照后的增量事件恢复(只需重放少量事件)

快照频率 → 决定恢复时间(不是事件总数)
金融系统:日切(每日 0 点)天然是打快照的时机

适用场景

  • 需要完整审计日志(金融、医疗、合规)
  • 需要时光机(回溯历史任意时刻的状态)
  • 业务规则复杂,命令→事件的转换有意义

不适用场景

  • 简单 CRUD,没有复杂业务规则
  • 对查询延迟极度敏感(需要额外的读模型)

CQRS(读写分离)

背景:Event Sourcing 的写模型(事件队列 + 状态机)不适合复杂查询。CQRS 将写模型和读模型彻底分离,各自优化。

flowchart LR
    C[Client] -->|Command| W[写模型<br/>事件队列 + 状态机]
    C -->|Query| R[读模型<br/>投影视图]
    W -->|复制事件| R
    style W fill:#ffc,stroke:#660
    style R fill:#cfc,stroke:#060

读模型(投影)

  • 消费写模型的事件,维护专门优化查询的数据结构
  • 可以有多个不同的读模型(如:一个用于列表查询,一个用于全文检索)
  • 读模型是只读的,数据来自事件流

一致性读:普通读模型可能有延迟,需要强一致时,将查询作为一个空命令走写模型的共识路径,利用线性一致性保证读到最新状态。

Saga 补偿模式

Saga 将一个跨服务的长事务拆分为一系列本地事务,每个本地事务有对应的补偿事务(逆操作)。

两种协调模式

模式原理优点缺点
编排式(Choreography)每个服务完成后发布事件,下一个服务监听并响应无中心协调者,松耦合流程分散,难以追踪整体状态
指挥式(Orchestration)中央 Saga Orchestrator 按顺序调用各服务流程集中,易于监控和调试引入中心化协调者
sequenceDiagram
    participant O as Saga Orchestrator
    participant S1 as 订单服务
    participant S2 as 库存服务
    participant S3 as 支付服务
    O->>S1: 创建订单
    S1-->>O: 成功
    O->>S2: 扣减库存
    S2-->>O: 成功
    O->>S3: 扣款
    S3-->>O: 失败(余额不足)
    Note over O: 触发补偿
    O->>S2: 恢复库存(补偿)
    O->>S1: 取消订单(补偿)

Saga vs TCC 对比

维度TCCSaga
隔离性较好(Try 阶段预留资源)较差(中间状态对外可见)
业务侵入高(3 个接口)中(补偿接口)
适用时长短事务(秒级)长事务(分钟/小时级)
典型场景支付扣款旅行预订(机票+酒店+租车)

Saga 的"脏读"问题:Saga 执行过程中,其他事务可能读到中间状态(如库存已扣但支付未完成)。解决方案:

  • 业务层面标记"预留中"状态
  • 读操作过滤掉"进行中"的记录
  • 接受最终一致性,不追求强隔离

Sidecar 与 Leader Election

Sidecar 模式

思想:将服务的基础设施关注点(日志收集、服务发现、流量控制、mTLS)从业务代码中剥离,部署为同 Pod 的辅助容器(Sidecar)。

┌─────────────────────────────┐
│           Pod               │
│  ┌──────────┐  ┌──────────┐ │
│  │  业务    │  │ Sidecar  │ │
│  │  容器    │◄─►│ (Envoy)  │ │
│  └──────────┘  └──────────┘ │
└─────────────────────────────┘

好处

  • 业务代码无需关心服务网格、追踪、限流等基础设施
  • 基础设施升级不需要重新发布业务服务
  • 多语言服务共享同一套基础设施能力

Leader Election 模式

场景:集群中需要一个主节点来协调工作(定时任务只执行一次、写操作只在主节点执行)。

实现方式

方式原理特点
基于 ZooKeeper创建临时节点,成功者为 Leader可靠,节点宕机自动释放锁
基于 etcd/Raft利用共识算法天然选主强一致,生产推荐
基于 RedisSET NX + 过期时间简单,但有极小概率的锁丢失风险

注意事项

  • Leader 宕机后,新 Leader 选出前系统暂不可用(这是 CP 系统的代价)
  • 防止脑裂(Split Brain):新 Leader 必须等旧 Leader 的租约到期
  • 客户端需要处理"Leader 切换期间的请求失败"(重试 + 重新发现 Leader)

模式选型速查

问题场景推荐模式核心原因
下游服务变慢拖垮整体Bulkhead 舱壁隔离故障隔离,防止级联
下游持续失败,重试无效Circuit Breaker 熔断快速失败,给下游恢复时间
偶发网络抖动导致失败Retry + Backoff + Jitter等待抖动后重试
事务 + 消息发布原子性Outbox Pattern利用同一数据库事务
需要完整审计/时光机Event Sourcing事件不可变,可重放
写模型查询性能差CQRS读写模型各自优化
跨服务长事务Saga(补偿)最终一致性,无分布式锁
跨服务短事务,需较强隔离TCC预留资源,业务层强一致
基础设施与业务解耦Sidecar关注点分离
集群需要唯一协调者Leader Election共识算法保证唯一性

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。
登录注册 后才能发表评论