这篇笔记围绕 Spring Cloud 在微服务体系中的基础职责展开,主线仍然是注册中心、服务调用、负载均衡、容错、网关、配置中心与消息总线,同时补充当前主流生态中的常见演进方向,用一篇笔记把核心组件之间的关系串起来。
文中同时保留
Eureka、Ribbon、Hystrix、Zuul等经典 Netflix 组件,也补充Nacos、Gateway、OpenFeign、Sentinel等较新的组合,阅读时需要把“经典方案的设计思想”和“当前项目里的主流选型”区分开来看。
参考资料:
官方项目:Spring Cloud 、 Spring Cloud Gateway 、 Spring Cloud OpenFeign 、 Spring Cloud Config 、 Spring Cloud Bus
生态文档:Spring Cloud Netflix 、 Nacos 、 Sentinel
理论与补充资料:CAP理论中的P到底是个什么意思 、 分布式系统的CAP理论 、 JavaGuide - Spring Cloud
[TOC]
CAP 理论
CAP 是分布式系统里最常被提到的基础理论之一,它讨论的不是“系统平时好不好用”,而是:
当网络分区发生时,一个分布式系统无法同时满足强一致性(C)和可用性(A)。
先看三个概念:
- C(Consistency,一致性): 所有节点对外表现得像“只有一份最新数据”。一次写入成功后,之后的每次读取都应该读到这次写入后的最新值,否则就返回错误或拒绝服务
- A(Availability,可用性): 每个没有故障的节点,都必须在有限时间内对请求给出明确响应,不能一直等待,更不能直接不处理
- P(Partition tolerance,分区容错性): 节点之间即使因为网络故障而无法通信,系统仍然要继续工作
什么是“强一致性”
很多人第一次看 CAP 时都会困惑:到底是什么内容要保持一致?
这里的一致性,指的是分布式系统中多副本数据的一致性,或者说,客户端从不同节点读到的结果是否一致。
比如同一份业务数据被保存在多个节点中:
- 用户账户是否已经注册
- 商品库存还剩多少
- 订单状态是否已经支付
- 服务注册中心里某个实例是否还在线
如果系统满足 CAP 里的 C,那么客户端无论访问哪个节点,看到的都应该是同一份“最新结果”。
它的具体表现通常是:
- 用户刚在节点 A 完成注册,马上去节点 B 查询,也必须能查到这个账户
- 库存在节点 A 被扣减后,其他节点不能继续把旧库存返回给客户端
- 一个实例已经下线,任意节点对外提供的注册信息都不应继续把它当作可用实例
换句话说,强一致性强调的是“读到的一定是最新写入结果”。
如果做不到这一点,而是允许一段时间内不同节点返回不同结果,那就不是 CAP 语境下的强一致性。
下面有三个节点(它们组成一个集群),此时三个节点都能够相互通信:

由于分布式系统依赖网络通信,而网络本身就可能出现延迟、丢包、机房隔离等问题,所以节点之间一旦无法互相通信,整个集群就会被切成几个彼此不连通的区域。
这就是网络分区(Partition)。

现在假设网络分区已经发生,此时有一个请求过来,想要注册一个账户:

这时节点一和节点三无法通信,系统就必须做选择。
选择 A:优先保证可用性
如果系统决定:请求来了先处理,先给用户成功响应,那么用户的注册请求可以在当前能够通信的那一侧完成。
这样做的好处是:
- 用户不会因为网络分区而立刻看到报错
- 服务仍然能对外提供读写能力
但问题也很明显:
- 节点一和节点三暂时无法同步数据
- 用户此时访问不同节点,可能看到不同结果
- 某些节点已经知道“这个账户注册成功了”,某些节点还不知道
这种情况就是:选择了 A,牺牲了 C。
也就是说,系统继续对外提供服务,但短时间内不能保证所有节点都看到同一份最新数据。
选择 C:优先保证强一致性
如果系统决定:只要数据还不能在所有关键节点之间达成一致,就暂时不接这个请求,那么它就会拒绝本次注册,或者让请求等待到网络恢复后再处理。
这样做的结果是:
- 一旦请求成功,所有节点看到的一定是统一的最新结果
- 不会出现“某些节点注册成功、某些节点还不知道”的情况
但代价是:
- 分区期间部分请求可能失败
- 用户会感知到服务不可用,或者响应明显变慢
这种情况就是:选择了 C,牺牲了 A。
为什么 P 几乎是必选项
在实际分布式系统中,网络故障不是“会不会发生”的问题,而是“什么时候发生”的问题。
所以,P 往往不是主观选择,而是客观约束。只要系统是分布式部署的,就必须接受网络可能被分区这个事实。
因此,CAP 更准确的理解应该是:
在出现网络分区时,系统必须在 C 和 A 之间做取舍。
注意,这并不是说:
- 选择了 AP,系统就完全不要一致性
- 选择了 CP,系统就完全不可用
更准确地说是:
- AP 系统: 在分区期间优先响应请求,通常只能保证最终一致性
- CP 系统: 在分区期间优先保证数据正确,允许部分请求失败或超时
一致性的常见层次
CAP 里的 C 指的是强一致性。而在工程实践中,一致性通常还有不同层次:
- 强一致性: 写入一旦成功,之后任意节点上的读取都必须返回最新值
- 弱一致性: 读取不保证拿到最新值
- 最终一致性(Eventual Consistency): 允许短时间不一致,但在没有新的更新后,数据最终会达到一致
很多互联网系统之所以能做到更高可用,本质上就是接受了“短时间内不一致”,用最终一致性来换取更好的服务连续性。
什么是“可用性”
可用性不是泛泛地指“系统差不多能用”,而是一个更严格的概念:
对于每一个发到非故障节点的请求,系统都必须在有限时间内返回响应。
这里的“响应”有两个重点:
- 不能无限等待
- 不能因为要等其他分区同步完成,就一直卡住
它的具体表现通常是:
- 服务还能够正常接收请求
- 用户能够在可接受时间内拿到结果
- 即使部分节点或链路异常,系统也尽量不整体中断
所以在工程里,人们常常用可用率来衡量服务质量,比如 99.9%、99.99%。本质上都是在描述:系统有多少时间可以持续提供服务。

一句话总结
CAP 不是在说“三个里永远只能选两个”,而是在说:
当网络分区发生时,分布式系统无法同时做到“所有节点马上看到同一份最新数据”和“所有请求都持续成功返回”。
因此,CAP 理论描述的核心矛盾是:
在容忍网络分区的前提下,强一致性和高可用性不能同时被绝对满足。
Spring Cloud 是什么

Spring Cloud 不是单独的某一个中间件,而是一组围绕微服务基础设施的组件集合。
如果说 Spring Boot 更关注单个应用如何快速开发和自动配置,那么 Spring Cloud 更关注的是多个服务拆开之后,如何完成注册、发现、调用、治理、监控与协同。
它本身并不是“重新发明一套分布式系统”,而是把不同领域里已经成熟的能力按照 Spring 体系的方式做了统一抽象和集成。于是,服务发现、配置管理、网关、链路追踪、消息驱动这些原本分散的问题,可以在同一套编程模型里组织起来。
从历史上看,很多人第一次接触 Spring Cloud,往往接触到的是 Spring Cloud Netflix 这一代组合,例如 Eureka、Ribbon、Hystrix、Feign、Zuul。
而从当前项目实践看,更常见的则是 Nacos、OpenFeign、Spring Cloud Gateway、Sentinel、Micrometer Tracing 等组合。因此,学习 Spring Cloud 时,既要理解经典组件背后的设计思想,也要知道这些能力在新生态中的演进方向。
Spring Cloud 主要解决什么问题
| 能力域 | 经典方案 | 当前常见方案 | 解决的问题 |
|---|---|---|---|
| 服务注册与发现 | Eureka | Nacos、Consul | 服务实例动态上下线后,调用方如何找到可用实例 |
| 服务调用 | Feign + Ribbon | OpenFeign + Spring Cloud LoadBalancer | 服务之间如何以统一方式发起远程调用 |
| 服务保护 | Hystrix | Sentinel、Resilience4j | 下游故障、超时、雪崩时如何隔离、熔断、降级 |
| 网关 | Zuul | Spring Cloud Gateway | 统一入口、统一路由、统一鉴权与限流 |
| 配置管理 | Spring Cloud Config | Nacos Config、Spring Cloud Config | 配置如何集中管理、动态刷新 |
| 可观测性 | Sleuth + Zipkin | Micrometer Tracing、OpenTelemetry | 跨服务请求如何关联日志和定位瓶颈 |
| 消息驱动 | Spring Cloud Stream、Bus | Spring Cloud Stream、Bus | 异步解耦、事件广播、配置刷新如何落地 |
graph LR
A[客户端] --> B[网关 Gateway/Zuul]
B --> C[业务服务]
C --> D[注册中心 Eureka/Nacos]
C --> E[配置中心 Config/Nacos]
C --> F[服务保护 Hystrix/Sentinel]
C --> G[链路追踪 Sleuth/Zipkin]
C --> H[消息系统 Stream/Bus]
本文相关的常见项目
| 项目名称 | 项目职能 |
|---|---|
| Spring Cloud Netflix | 整合 Eureka、Ribbon、Hystrix、Zuul 等经典组件,是大量早期教程和老项目的基础 |
| Spring Cloud Config | 提供集中式配置管理,支持远程配置拉取、加解密与刷新机制 |
| Spring Cloud Bus | 把配置变更等事件广播到整个服务集群 |
| Spring Cloud OpenFeign | 提供声明式 HTTP 客户端,降低服务间调用代码量 |
| Spring Cloud Gateway | 提供统一入口、路由、过滤、限流等网关能力 |
| Spring Cloud Stream | 对 Kafka、RabbitMQ 等消息中间件做统一抽象 |
| Spring Cloud Sleuth | 为分布式链路追踪提供埋点与上下文传播能力 |
| Spring Cloud Consul / Zookeeper | 提供其他注册中心与配置管理实现 |
组件演进补充
把 Spring Cloud 放到今天的项目实践里看,它并不是只有一套固定组合,而是经历过明显的组件演进。
很多早期教程里常见的是这一套:
Eureka:注册中心Ribbon:客户端负载均衡Hystrix:熔断降级Feign:声明式调用Zuul:网关
而在现在更常见的项目里,大家更容易看到的是:
Nacos:注册中心 + 配置中心Spring Cloud Gateway:网关OpenFeign:声明式服务调用Sentinel或Resilience4j:限流、熔断、降级Sleuth/Zipkin或Micrometer Tracing:链路追踪Spring Cloud Stream:消息驱动微服务
也就是说,这篇笔记前半部分讲到的是 Spring Cloud Netflix 时代的经典组件,而后面补充的内容,会更偏向 现在项目里更常见的 Spring Cloud / Spring Cloud Alibaba 组合。
两套东西不是互相否定,而是代表了不同阶段的主流方案。
Eureka 服务注册中心
分布式拆分子服务后,子系统之间的通讯必然成为需要考虑的问题
子系统与子系统之间不再位于同一个进程中,服务调用自然变成远程调用。最直接的做法是把目标服务的 IP 和端口写死在配置里,例如通过 HttpClient 或 RestTemplate 直接访问某个地址。
但只要服务实例一旦扩容、缩容、迁移或重启,静态地址就会迅速变成维护负担。典型问题往往有两类:
功能实现一:A 服务需要调用 B 服务
在 A 服务的代码里面调用 B 服务,显式通过 IP 地址调用:http://123.123.123.123:8888/java3y/3
功能实现二:A 服务调用 B 服务,B 服务调用 C 服务,C 服务调用 D 服务
在 A 服务的代码里面调用 B 服务,显式通过 IP 地址调用:http://123.123.123.123:8888/java3y/3,(同样地)B->C,C->D
… …
一旦 B 服务的 IP 地址变化,所有依赖它的服务都需要修改配置;服务数量越多,维护这些静态地址的成本就越高。
因此,服务注册与服务发现就成了微服务体系最基础的一层。它解决的问题不是“如何调用 HTTP”,而是“如何在服务实例动态变化时,仍然稳定地找到可用目标”。
在早期 Spring Cloud Netflix 体系里,最典型的注册中心实现就是 Eureka。
创建一个E服务,将 A、B、C、D 四个服务的信息都注册到 E 服务上,E 服务维护这些已经注册进来的信息

A、B、C、D 四个服务都可以从 Eureka 获取注册表。这样一来,服务之间不再依赖具体的 IP 地址,而是通过服务名完成调用。
- 拿到注册清单后,根据服务名选择具体实例
- 服务实例的 IP 可以变化,但服务名通常保持稳定
提供注册表服务的节点称为 Eureka Server,注册到注册中心并消费注册表的节点统称为 Eureka Client。
Eureka 注册中心的三种角色:
-
Eureka Server: 通过Register、Get、Renew 等接口提供服务的注册和发现
-
Application Service (Service Provider) 服务提供方把自身的服务实例注册到Eureka Server 中
-
Application Client (Service Consumer) 服务调用方通过 Eureka Server 获取服务列表,消费服务
Eureka Server 的典型配置:
1
2
3
4
eureka:
client:
register-with-eureka: false
fetch-registry: false
这里的含义是:注册中心本身不需要把自己注册到其他注册中心,也不需要再去拉取注册表。
Eureka Client 可以是服务提供者,也可以是服务消费者,很多服务同时承担这两种角色。
需要区分两个配置项:
register-with-eureka:是否把当前实例注册到 Eurekafetch-registry:是否从 Eureka 拉取服务注册表
因此,一个纯消费端即使不注册自己,也依然可以通过拉取注册表来发现其他服务。
1
2
3
4
5
6
eureka:
client:
register-with-eureka: false
fetch-registry: true
service-url:
defaultZone: http://eureka7001.com:7001/eureka/,http://eureka7002.com:7002/eureka/,http://eureka7003.com:7003/eureka/
Eureka 的治理机制:
服务提供者:
- 服务注册: 启动的时候会通过发送 REST 请求的方式将自己注册到 Eureka Server上,同时带上了自身服务的一些元数据信息
- 服务续约: 注册完成后,服务提供者会通过心跳持续向 Eureka Server 续约
- 服务下线: 当服务实例正常关闭时,会主动发送下线请求,通知注册中心移除自己
服务消费者:
- 获取服务: 启动时从注册中心拉取服务清单
- 服务调用: 根据服务名和实例元数据选择目标节点,通常优先选择同 Zone 的实例
Eureka Server(服务注册中心):
- 失效剔除: 默认按周期扫描注册表,把长时间未续约的实例剔除出去
- 自我保护: 当短时间内心跳失败比例异常升高时,Eureka 会暂时放宽摘除策略,避免因为网络抖动把大量健康实例误删

Eureka、ZooKeeper、Nacos 对比
前面讲完 CAP 之后,再看注册中心就会顺很多。
因为注册中心本质上也要面对同一个问题:
- 网络分区发生时,还要不要继续返回服务列表
- 返回的服务列表,是不是必须保证每个节点都完全一致
- 如果一致性和可用性冲突,应该优先保哪一个
这也是为什么大家总喜欢把 Eureka、ZooKeeper、Nacos 放在一起比较。
先说结论
如果只从“作为服务注册中心”这个角度看,可以先记住下面这句话:
- Eureka: 更偏 AP,优先保证服务发现可用
- ZooKeeper: 更偏 CP,优先保证注册数据一致
- Nacos: 更灵活,既能做注册中心,也能做配置中心;在服务发现上可以根据实例类型呈现不同取向
不过要注意,这里的“偏 AP / 偏 CP”是从典型使用方式和设计取向来理解的,不是简单粗暴地给整个产品贴一个绝对标签。
为什么 Eureka 更偏 AP
Eureka 的设计目标之一,就是让服务发现尽量不要中断。
它采用的是 Peer to Peer 的对等复制思路,没有像 ZooKeeper 那样强依赖一个需要完成选举的中心 Leader。多个 Eureka Server 之间互相同步注册信息,每个节点都可以对外提供服务。
它的具体表现是:
- 某个 Eureka Server 宕机后,客户端可以切换到其他节点
- 服务实例的注册信息会在各个 Eureka 节点之间复制
- 当短时间内心跳丢失过多时,Eureka 会进入自我保护模式
这里的关键点在于:
Eureka 在异常情况下更倾向于“先别轻易把实例删掉,先保证还能返回服务列表”。
这意味着什么?
- 好处是:注册中心不容易因为网络抖动就把整个服务发现能力搞挂
- 代价是:返回的数据可能不是绝对最新的
比如某个服务实例实际上已经不可用了,但在短时间内,Eureka 仍可能把它保留在注册表里。这会导致客户端拿到旧数据,但至少还能继续发起调用,而不是整个注册中心直接不可用。
所以说,Eureka 更偏 AP,不是因为它完全不要一致性,而是因为它在分区或异常场景下,更愿意用“短时间可能不一致”来换“服务还能继续被发现”。
为什么 ZooKeeper 更偏 CP
ZooKeeper 本质上是一个分布式协调组件,它最擅长的事情其实是:
- 维护一致的元数据
- 做分布式协调
- 做选主、锁、配置同步
它的核心设计强调的是:
- 数据有序
- 写请求按严格顺序提交
- 集群需要基于多数派机制达成一致
因此,当 ZooKeeper 集群出现网络分区、Leader 选举、或者可用节点数不足半数时,系统会优先保证一致性,而不是优先保证每个请求都成功。
这在服务发现里的直观表现就是:
- 如果 ZooKeeper 正在选主,客户端可能暂时拿不到最新服务列表
- 如果集群中没有形成多数派,请求可能失败
- 它宁可暂时不提供服务,也不愿意返回一个可能有问题的结果
所以说,ZooKeeper 更偏 CP,不是因为它完全不可用,而是因为一旦一致性无法保证,它会优先收紧对外能力。
这套思路非常适合:
- 分布式锁
- 领导者选举
- 配置一致性要求高的协调场景
但如果只是拿它来做服务注册中心,就会出现一个现实问题:
对于服务发现来说,客户端往往更在乎“先找到一个还能调用的实例”,而不是“注册表在每一秒都绝对一致”。
也正因为如此,作为纯服务注册中心时,ZooKeeper 的使用体验通常不如 Eureka 那么“抗抖动”。
为什么说 Nacos 更灵活
Nacos 和前两者最大的不同,不只是“它也能做注册中心”,而是:
它同时把服务发现、配置管理、服务管理整合到了一个平台里。
从 Spring Cloud 生态的实际使用习惯来看,Nacos 经常既承担:
- 注册中心 的角色
- 配置中心 的角色
这让它在微服务项目里非常常见。
如果只看服务发现这一块,Nacos 并不是简单地永远偏 AP 或永远偏 CP,而是要结合实例类型来看:
- 临时实例(ephemeral,默认更常见): 更偏 AP,依赖客户端心跳,适合动态上下线的微服务实例
- 持久实例(persistent): 更偏 CP,适合对实例信息持久化和一致性要求更高的场景
这也是 Nacos 更灵活的原因之一:
它没有像“Eureka 就是这样、ZooKeeper 就是那样”那么单一,而是给了不同场景不同选择。
另外还要补一句:
当 Nacos 被当作配置中心使用时,关注点又和服务发现不一样。
配置数据通常比“某个实例此刻是否在线”更强调持久化和一致性,所以它作为配置中心时,思路会比纯服务发现场景更偏向“配置不能乱、配置不能丢”。
作为注册中心,三者怎么理解
如果把它们都放回“服务注册中心”这个具体场景里,可以概括为:
Eureka
- 目标更偏向“服务发现别中断”
- 更能接受短时间注册信息不一致
- 更适合把“可用性”放在前面的服务发现体系
ZooKeeper
- 目标更偏向“元数据必须一致”
- 对 Leader、半数机制、一致性要求更敏感
- 更适合协调类场景,不是天然最优的服务注册中心选择
Nacos
- 既能做注册中心,又能做配置中心
- 在服务注册场景下能力更完整,生态里也更常见
- 能根据实例类型在可用性和一致性之间做不同权衡
一个更实用的理解方式
选型时更关键的问题并不是“谁最好”,而是“当前场景更在意什么”。
如果更在意的是:
- 注册中心不要轻易不可用
- 即使偶尔拿到旧实例,也比完全拿不到服务列表更能接受
那么思路会更接近 Eureka。
如果更在意的是:
- 元数据必须严格一致
- 宁可短时间失败,也不能接受不一致结果
那么思路会更接近 ZooKeeper。
如果希望:
- 注册中心和配置中心尽量统一
- Spring Cloud Alibaba 生态集成更自然
- 后续还想要命名空间、分组、动态配置这些能力
那么很多项目会更倾向于 Nacos。
简单总结
- Eureka 更偏 AP: 因为它在异常场景下优先保证服务发现还能继续,允许短时间注册信息不是最新
- ZooKeeper 更偏 CP: 因为它优先保证数据一致,必要时宁可暂时不提供服务
- Nacos 更灵活: 既是注册中心,也是配置中心;在服务发现上能根据不同实例类型体现不同取向
从今天的 Spring Cloud 实战来看:
- 老的 Spring Cloud Netflix 项目里常见
Eureka - 偏协调组件、分布式治理场景里常见
ZooKeeper - 现在国内 Spring Cloud / Spring Cloud Alibaba 项目里,
Nacos往往是更常见的选择
Ribbon 客户端负载均衡
通过 Eureka 这类注册中心,调用方已经可以拿到某个服务的多个实例列表。接下来的问题就变成:同一个服务有多个实例时,当前请求应该落到哪一台机器上。
在早期 Spring Cloud Netflix 体系中,最常见的选择就是 Ribbon。它工作的方式不是在服务端放一个统一调度器,而是在调用方本地基于服务列表做负载均衡决策。
可以先看一个通过服务名调用的 RestTemplate 例子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// 传统方式直接写死 IP,不利于扩容和迁移
//private static final String REST_URL_PREFIX = "http://localhost:8001";
// 服务实例名
private static final String REST_URL_PREFIX = "http://MICROSERVICECLOUD-DEPT";
/**
* postForObject(url, request, responseType)
* 分别表示:请求地址、请求体、响应类型。
*/
@Autowired
private RestTemplate restTemplate;
@RequestMapping(value = "/consumer/dept/add")
public boolean add(Dept dept) {
return restTemplate.postForObject(REST_URL_PREFIX + "/dept/add", dept, Boolean.class);
}
服务提供者通常不会只部署一个实例,而是会做成集群,以便提升吞吐和可用性。

当一个服务有多个实例时,请求如何在这些实例之间分配,就是负载均衡问题。
Spring Cloud 既可以配合 Nginx 这类服务端负载均衡器,也可以在客户端使用 Ribbon 做本地负载均衡。
负载均衡又区分了两种类型:
客户端负载均衡(Ribbon)
- 服务实例清单保存在客户端
- 调用方从注册中心拉取服务列表,再在本地根据算法选择目标实例
服务端负载均衡(Nginx)
- 服务实例清单由代理层维护
- 请求先进入代理,再由代理转发到后端实例

Ribbon 细节:
Ribbon 默认提供轮询等负载均衡策略,也支持自定义规则。
1
2
3
4
5
6
7
8
9
@Configuration
public class MySelfRule {
@Bean
public IRule myRule() {
// return new RoundRobinRule(); // 轮询
// return new RandomRule(); // 随机
return new RandomRule_ZY(); // 自定义规则
}
}
自定义策略时,通常是继承 AbstractLoadBalancerRule 并重写 choose(ILoadBalancer lb, Object key)。
需要注意两点:
Ribbon只是客户端负载均衡组件,本身不等同于整个系统的 CAP 取舍- 客户端重试虽然能提升成功率,但必须结合接口幂等性,否则可能放大重复调用风险
另外,在较新的 Spring Cloud 版本里,Ribbon 已经逐步退出主流,新项目更常见的是 Spring Cloud LoadBalancer。
Nginx 和 Ribbon 的对比
同样是“负载均衡”,Nginx 和 Ribbon 的职责位置并不一样。


| 维度 | Nginx | Ribbon / LoadBalancer |
|---|---|---|
| 负载均衡位置 | 服务端代理层 | 客户端本地 |
| 服务列表维护位置 | 代理层维护后端地址 | 调用方从注册中心拉取 |
| 流量入口 | 所有请求先经过代理 | 请求在本地选好实例后直达目标 |
| 典型用途 | 对外统一入口、反向代理、静态资源、TLS 终止 | 服务间调用时按实例做本地负载均衡 |
| 与注册中心关系 | 可以无关,也可以联动 | 通常直接依赖注册中心提供的实例列表 |
Hystrix 服务容错保护
调用多个远程服务时,当某个服务出现延迟:

在高并发的情况下,由于单个服务的延迟,可能导致所有的请求都处于延迟状态,甚至在几秒钟就使服务处于负载饱和的状态,资源耗尽,直到不可用,最终导致这个分布式系统都不可用,这就是“雪崩”

针对上述问题, Spring Cloud Hystrix 实现了断路器、线程隔离等一系列服务保护功能
- Fallback(失败快速返回): 当某个服务单元发生故障(类似用电器发生短路)之后,通过断路器的故障监控(类似熔断保险丝),向调用方返回一个错误响应,而不是长时间的等待。这样就不会使得线程因调用故障服务被长时间占用不释放,避免了故障在分布式系统中的蔓延
- 资源/依赖隔离(线程池隔离): 它会为每一个依赖服务创建一个独立的线程池,这样就算某个依赖服务出现延迟过高的情况,也只是对该依赖服务的调用产生影响,而不会拖慢其他的依赖服务
Hystrix 中常被提到的一组默认熔断参数包括:
- 请求量阈值
20 - 休眠时间窗
5s -
错误百分比阈值
50% - 每当 20 个请求中,有 50% 失败时,熔断器就会打开,此时再调用此服务,将会直接返回失败,不再调远程服务
- 直到 5s 钟之后,重新检测该触发条件,判断是否把熔断器关闭,或者继续打开
熔断:
是服务雪崩的一种有效解决方案。当指定时间窗内的请求失败率达到设定阈值时,系统将通过断路器直接切断这条调用链。可以使用 @HystrixCommand 为方法增加超时、熔断和降级能力。
1
2
3
4
5
6
@HystrixCommand(
commandProperties = {@HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds",value = "1200")}
)
public List<Xxx> getXxxx() {
// ...省略代码逻辑
}
降级
降级的目标是在下游异常时提供一个可控的兜底结果,而不是把错误直接放大到整个系统。
例如热点接口因为访问量激增导致后端压力过大时,可以降级返回“系统繁忙,请稍后再试”之类的占位结果,而不是继续把线程和连接耗尽。
1
2
3
4
5
6
7
8
9
@HystrixCommand(fallbackMethod = "getHystrixNews")
@GetMapping("/get/news")
public String getNews(@PathVariable("id") int id) {
// 调用新闻服务,省略具体远程调用代码
}
public String getHystrixNews(int id) {
return "当前访问量较高,新闻详情暂时不可用";
}
需要补充一点:Hystrix 现在更多承担“理解熔断、隔离、降级模型”的学习价值。在较新的项目里,更常见的替代组合是 Sentinel 或 Resilience4j。
Feign / OpenFeign 声明式服务调用
在服务之间频繁发生 HTTP 调用时,如果每次都手写 RestTemplate 或 HttpClient,代码会迅速变得重复而分散。
Feign 的价值就在于把“远程调用”抽象成接口声明。早期 Spring Cloud 文档常写 Feign,当前更常见的名称则是 OpenFeign,核心思想并没有变化。
它本质上是一个声明式 HTTP 客户端:调用方只定义接口、请求路径和参数绑定规则,底层由框架生成实际调用逻辑。
Feign 服务绑定
1
2
3
4
5
6
7
8
9
10
11
@FeignClient(name = "MICROSERVICECLOUD-DEPT", fallback = DeptClientServiceFallback.class)
public interface DeptClientService {
@GetMapping("/dept/get/{id}")
Dept get(@PathVariable("id") Long id);
@GetMapping("/dept/list")
List<Dept> list();
@PostMapping("/dept/add")
boolean add(@RequestBody Dept dept);
}
Feign 中使用降级:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Component
public class DeptClientServiceFallback implements DeptClientService {
@Override
public Dept get(Long id) {
return new Dept()
.setDeptno(id)
.setDname("id=" + id + " 的部门信息暂时不可用,已触发服务降级")
.setDb_source("no this database in MySQL");
}
@Override
public List<Dept> list() {
return Collections.emptyList();
}
@Override
public boolean add(Dept dept) {
return false;
}
}
调用示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@RestController
public class DeptController_Consumer {
@Autowired
private DeptClientService deptClientService;
@GetMapping("/consumer/dept/get/{id}")
public Dept get(@PathVariable("id") Long id) {
return deptClientService.get(id);
}
@GetMapping("/consumer/dept/list")
public List<Dept> list() {
return deptClientService.list();
}
@PostMapping("/consumer/dept/add")
public boolean add(@RequestBody Dept dept) {
return deptClientService.add(dept);
}
}
Zuul 网关服务
如果没有网关,外部流量通常会直接打到各个微服务:

这种架构至少会暴露两个问题:
- 路由维护成本高: 外层代理需要感知所有服务实例的地址变化
- 横切逻辑重复: 鉴权、签名、限流、日志等逻辑容易在每个服务中重复实现

每个服务都有自己的 IP 地址,Nginx 想要正确请求转发到服务上,就必须维护着每个服务实例的地址
更麻烦的是,服务实例地址会变化,服务边界也可能持续调整。
例如购物车和订单都需要登录校验,如果没有统一入口,这些校验逻辑就很容易在多个服务里重复出现。
API 网关 的作用,就是把这些对外统一能力前移到系统入口层。在 Spring Cloud Netflix 体系中,对应的经典实现就是 Spring Cloud Zuul。
Zuul 主要解决的是两类问题:
- 与 Eureka 配合后,Zuul 可以根据服务名做动态路由,不再由外层静态维护每个实例地址
- 所有请求先经过网关,因此鉴权、过滤、限流、灰度等逻辑可以统一放在入口层处理
在经典组合里,Zuul 还可以与 Ribbon、Hystrix 协同工作,因此它并不只是“转发请求”,而是具备网关治理能力的一层基础设施。
Zuul 过滤功能
过滤器是 Zuul 最核心的扩展点之一。因为所有流量都会经过网关,所以限流、灰度发布、权限控制、日志采集这类逻辑都可以通过过滤器接入。

过滤器类型:Pre、Route、Post、Error
Pre:请求转发之前执行Route:负责路由转发过程Post:响应返回之前执行Error:异常处理阶段执行
下面用一个最基础的日志示例说明过滤器结构:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
@Component
public class PreRequestFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return 0;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
ctx.set("startTime", System.currentTimeMillis());
return null;
}
}
@Slf4j
@Component
public class AccessLogFilter extends ZuulFilter {
@Override
public String filterType() {
return FilterConstants.POST_TYPE;
}
@Override
public int filterOrder() {
return FilterConstants.SEND_RESPONSE_FILTER_ORDER - 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext context = RequestContext.getCurrentContext();
HttpServletRequest request = context.getRequest();
Long startTime = (Long) context.get("startTime");
String uri = request.getRequestURI();
long duration = System.currentTimeMillis() - startTime;
log.info("uri: {}, duration: {}ms", uri, duration);
return null;
}
}
Zuul 令牌桶限流
令牌桶只是网关限流里的典型做法之一,这里用一个最常见的示例说明思路。

令牌桶限流:
系统会以固定速率向桶中放入令牌,请求到来时先尝试获取令牌;获取失败则拒绝,获取成功则放行。
在 Zuul 中,这类逻辑通常放在前置过滤器里。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@Component
@Slf4j
public class RouteFilter extends ZuulFilter {
// 定义一个令牌桶,每秒产生2个令牌,即每秒最多处理2个请求
private static final RateLimiter RATE_LIMITER = RateLimiter.create(2);
@Override
public String filterType() {
return FilterConstants.PRE_TYPE;
}
@Override
public int filterOrder() {
return -5;
}
@Override
public Object run() throws ZuulException {
log.info("放行");
return null;
}
@Override
public boolean shouldFilter() {
RequestContext context = RequestContext.getCurrentContext();
if(!RATE_LIMITER.tryAcquire()) {
log.warn("访问量超载");
// 指定当前请求未通过过滤
context.setSendZuulResponse(false);
// 向客户端返回响应码429,请求数量过多
context.setResponseStatusCode(429);
return false;
}
return true;
}
}
同一类过滤器机制还可以扩展到权限校验、灰度发布、审计日志等场景。
Zuul 自身也可能成为单点,因此生产环境通常会把网关做成集群,再由外层负载均衡器统一接入。
Zuul 的路由功能
如果系统里已经有多个 Consumer 和 Provider,再加入 Zuul 后,入口结构通常会变成“客户端 -> 网关 -> 内部服务”。

Zuul 注册到 Eureka 后,可以根据注册表中的服务元数据做动态路由映射。例如原本直接调用 localhost:8001/studentInfo/update,可以改为通过网关统一入口访问。
Zuul 最基本的配置:
1
2
3
4
5
6
server:
port: 9000
eureka:
client:
service-url:
defaultZone: http://localhost:9997/eureka
然后在启动类上加入 @EnableZuulProxy 即可开启网关代理能力。
Zuul 统一前缀:
1
2
zuul:
prefix: /zuul
此时访问路径会变成 localhost:9000/zuul/consumer1/studentInfo/update。
路由策略配置:
如果直接把服务名暴露在外部路径上,虽然方便,但并不总是合适。更常见的做法是自定义外部路由路径。
1
2
3
4
zuul:
routes:
consumer1: /FrancisQ1/**
consumer2: /FrancisQ2/**
此时可以通过 localhost:9000/zuul/FrancisQ1/studentInfo/update 访问对应服务。
服务名屏蔽:
如果希望外部完全看不到真实服务名,还需要显式屏蔽服务名路由。
1
2
zuul:
ignore-services: "*"
路径屏蔽:
Zuul 还支持按 URI 模式屏蔽路径,用于限制某些内部接口直接暴露出去。
1
2
3
zuul:
ignore-patterns:
- /**/auto/**
这样,匹配 auto 路径规则的请求就不会再被转发。
** 代表匹配多级任意路径 *代表匹配一级任意路径
敏感请求头屏蔽:
默认情况下,像 Cookie、Set-Cookie 这类敏感请求头会被 Zuul 屏蔽;如果业务需要,也可以显式调整这部分转发策略。
Zuul 小结
Zuul 和 Feign 都能和 Ribbon、Hystrix 协同工作,但二者处在完全不同的层级。

| 组件 | 所处位置 | 主要职责 |
|---|---|---|
| Zuul | 系统入口层 | 统一接入、路由转发、鉴权、过滤、限流 |
| Feign / OpenFeign | 服务内部调用层 | 以声明式接口方式发起服务间 HTTP 调用 |
| Ribbon / LoadBalancer | 调用方本地 | 在多个服务实例之间选择一个目标节点 |
Spring Cloud Config 分布式配置中心
随着业务的扩展,服务会越来越多。每个服务都有自己的配置文件,既然是配置文件,配置的东西难免会有些改动。比如每个服务中写的数据库配置,配置文件中的密码需要更换,那就得三个都要重新更改

在分布式系统中,某一个基础服务信息变更,都很可能会引起一系列的更新和重启
Spring Cloud Config 提供了一套集中式配置管理方案。它由两部分组成:
- Config Server: 负责从 Git、SVN、本地文件系统等配置仓库读取配置并对外暴露
-
Config Client: 启动时从远端拉取配置,并据此初始化自身应用
- 更直观地说,就是把配置文件放到统一位置管理,例如 Git 仓库
- 服务启动时不再只依赖本地
application.yml,而是可以先到配置中心取配置

Spring Cloud Config 的默认仓库实现通常使用 Git,也支持 SVN 或本地 native 模式。
- 支持配置内容的加密与解密
- 如果希望配置修改后尽量少重启服务,通常会配合
Spring Cloud Bus做动态刷新 - 在传统 Spring Cloud 配置模型里,
bootstrap.yml早于application.yml加载,常用于配置配置中心地址等“启动早期参数” - 在较新的 Spring Boot / Spring Cloud 版本中,外部配置加载方式已经逐步演进,实际项目里还会看到
spring.config.import等新写法
Spring Cloud Bus 事件总线
如果直接修改远程配置仓库中的配置文件,已经启动的应用并不会自动感知这些变化。
在简单场景里,可以通过 Webhook 触发刷新流程;但在生产环境中,更常见的方式是通过 Spring Cloud Bus + 消息中间件 把刷新事件广播到整个集群。
Bus 的重点不在于“拉配置”,而在于“传播事件”。
Spring Cloud Bus 用于将服务和服务实例与分布式消息系统连接起来,在集群中传播状态变化事件,例如配置刷新事件。
sequenceDiagram
participant Admin as 运维操作
participant Repo as 配置仓库
participant Bus as Spring Cloud Bus
participant A as 服务实例A
participant B as 服务实例B
Admin->>Repo: 提交配置变更
Admin->>Bus: 触发 busrefresh
Bus-->>A: 广播刷新事件
Bus-->>B: 广播刷新事件
A->>Repo: 重新拉取配置
B->>Repo: 重新拉取配置
因此,Bus 更适合被理解为框架层的事件广播机制。配置刷新只是其中最典型的一个应用。
如果某个 Bean 需要在配置变化后重新注入属性,常见做法是在相关 Bean 上使用 @RefreshScope。

Spring Cloud Gateway 网关服务
前面已经讲过 Zuul,但如果是现在的 Spring Cloud 项目,实际更常见的网关组件往往是 Spring Cloud Gateway。
可以把它理解成:
Spring Cloud 生态里更现代的一代网关实现。
它主要解决的问题和 Zuul 一样,依然是:
- 统一入口
- 统一路由
- 统一鉴权
- 统一限流
- 统一日志与监控
但 Spring Cloud Gateway 的设计更贴近现在的 Spring 技术栈,它基于 Spring WebFlux 和响应式编程模型实现,更适合承担高并发场景下的网关职责。
Gateway 的核心概念
Spring Cloud Gateway 的核心可以拆成三部分:
- Route(路由): 请求最终要转发到哪里
- Predicate(断言): 什么样的请求命中这条路由
- Filter(过滤器): 在请求和响应的前后做什么处理
如果用一句话概括,就是:
先用 Predicate 判断“这个请求该不该走这条路”,再用 Filter 决定“转发前后还要做哪些操作”。
Gateway 能做什么
它在工程里的常见用途包括:
- 根据路径、请求头、方法、参数进行路由匹配
- 给请求统一加鉴权、签名、日志、灰度标记
- 统一做限流、熔断、重试、降级
- 配合注册中心实现基于服务名的动态路由
- 对外隐藏内部微服务真实地址
一个最常见的路由例子
1
2
3
4
5
6
7
8
9
10
spring:
cloud:
gateway:
routes:
- id: order-service
uri: lb://order-service
predicates:
- Path=/api/order/**
filters:
- StripPrefix=1
这段配置表示:
- 当请求路径匹配
/api/order/** - Gateway 就把请求转发给
order-service lb://表示通过注册中心拿到服务实例,并结合负载均衡进行转发StripPrefix=1表示转发前把前缀/api去掉
Gateway 和 Zuul 的关系
两者关系可以概括为:
- Zuul: Spring Cloud Netflix 时代常见的网关方案
- Gateway: 现在 Spring Cloud 生态里更常见的网关方案
如果是老项目,依然可能看到 Zuul;
如果是新项目,很多时候会优先考虑 Gateway。
一句话总结
Gateway 本质上就是微服务系统的统一入口,它把原本分散在各个服务里的通用逻辑,前移到了网关层统一处理。
Sentinel 流量治理与熔断降级
前面讲 Hystrix 时,重点是服务熔断和降级。
在当前很多 Spring Cloud Alibaba 项目中,更常见的流量治理组件往往是 Sentinel。
Sentinel 是阿里开源的流量治理组件,在 Spring Cloud Alibaba 生态里使用非常普遍。
Sentinel 解决什么问题
微服务系统一旦遇到下面这些情况,就很容易出问题:
- 突发流量过大
- 某个下游服务响应变慢
- 某个热点接口被打爆
- 整体系统负载过高
这些问题如果没有保护机制,就会逐步演变成:
- 接口超时
- 线程池打满
- 服务雪崩
- 整个调用链被拖垮
Sentinel 的核心价值,就是:
从“流量”这个入口出发,对系统做保护。
Sentinel 常见能力
它最常见的能力有:
- 流量控制: 限制某个接口或资源的 QPS、线程数
- 熔断降级: 当异常比例、响应时间达到阈值时,自动触发保护
- 热点参数限流: 比如某个热点商品 ID、热点用户 ID 的请求特别多时单独限流
- 系统保护: 根据系统负载、CPU、入口流量等维度做整体保护
- 实时监控: 配合 Sentinel Dashboard 观察资源调用情况和规则命中情况
怎么理解“资源”和“规则”
学习 Sentinel 时,有两个词一定要搞清楚:
- 资源: 需要被保护的对象,例如一个接口、一个方法、一次远程调用
- 规则: 对这个资源施加的保护条件,例如 QPS 超过 100 就触发限流
也就是说,Sentinel 的思路是:
先标记资源,再为资源配置规则。
一个简单示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SentinelResource(
value = "createOrder",
blockHandler = "handleBlock",
fallback = "handleFallback"
)
public String createOrder(Long productId) {
return "下单成功";
}
public String handleBlock(Long productId, com.alibaba.csp.sentinel.slots.block.BlockException ex) {
return "请求过多,请稍后再试";
}
public String handleFallback(Long productId, Throwable ex) {
return "系统繁忙,请稍后重试";
}
这段代码里:
createOrder是被保护的资源blockHandler用来处理被限流、被熔断这类 Sentinel 规则触发的场景fallback用来处理业务执行过程中抛出的异常
Sentinel 和 Hystrix 怎么理解
学习时可以这样对应:
- Hystrix: 更偏“服务容错”这个角度
- Sentinel: 更偏“流量治理 + 服务保护”这个角度
Sentinel 也支持熔断降级,但它比 Hystrix 更强调:
- 流量控制
- 热点防护
- 系统自适应保护
- 控制台实时观察与动态规则配置
因此在很多 Spring Cloud Alibaba 项目里,经常会看到这样的组合:
OpenFeign负责远程调用Gateway负责统一入口Sentinel负责限流、熔断、降级和系统保护
一句话总结
Hystrix 更像“故障发生后的保护器”,而 Sentinel 更像“在流量入口就提前做治理和保护的守门员”。
Spring Cloud Sleuth / Zipkin 链路追踪
微服务拆分后,调用链会变长。一次请求可能会经过:
用户请求 -> 网关 -> 订单服务 -> 库存服务 -> 支付服务 -> 消息队列 -> 通知服务
此时最典型的问题包括:
- 某次请求到底经过了哪些服务
- 哪一跳最慢
- 错误是在哪个服务里发生的
- 日志应该怎么串起来看
这时就需要 分布式链路追踪。
什么是 Trace 和 Span
链路追踪里最核心的两个概念是:
- Trace: 一次完整请求的全链路
- Span: 链路中的一个具体操作单元
可以把它理解成:
- 一次用户下单是一个
Trace - 订单服务查库存、调用支付、发送消息,这些每一步都是一个
Span
Sleuth 和 Zipkin 分别干什么
- Spring Cloud Sleuth: 负责在应用内部自动埋点,并把
traceId、spanId传递下去 - Zipkin: 负责收集、存储、展示这些调用链数据
所以它们通常是配合使用的:
- Sleuth 负责“产生链路数据”
- Zipkin 负责“查看链路数据”
它最直观的价值是什么
引入链路追踪后,直接得到的能力包括:
- 一个请求在多个服务之间都有统一的
traceId - 日志可以按同一个请求串起来看
- 可以看到每一跳的耗时
- 可以定位性能瓶颈和异常节点
例如日志里看到:
1
2
3
[order-service,traceId=8f3a...,spanId=1ab2...]
[inventory-service,traceId=8f3a...,spanId=8cd1...]
[payment-service,traceId=8f3a...,spanId=9ef0...]
就可以判断这几条日志属于同一次请求。
需要注意的一点
在较新的 Spring 生态里,链路追踪能力已经逐步向 Micrometer Tracing / OpenTelemetry 方向演进。
不过在学习 Spring Cloud 微服务体系时,Sleuth + Zipkin 依然是理解“链路追踪到底在做什么”的非常经典的一套组合。
一句话总结
链路追踪的本质,就是把一次跨多个服务的请求重新串成一条完整可观察的调用链。
Spring Cloud Stream 消息驱动微服务
前面讲的很多内容,都是同步调用视角下的微服务协作。
但在真实系统里,不少场景更适合用异步消息来解耦。
比如:
- 下单成功后通知库存扣减
- 支付成功后发送短信或邮件
- 大促场景下做削峰填谷
- 业务事件广播给多个下游系统
这时就会用到消息中间件,比如 Kafka、RabbitMQ。而 Spring Cloud Stream 的作用,就是:
把应用和底层消息中间件之间做一层抽象。
为什么需要 Stream
如果直接操作 Kafka、RabbitMQ 的原生 API,开发时往往要关心很多细节:
- 连接和客户端配置
- 消费者组
- Topic / Exchange / Queue
- 消息序列化和反序列化
- 重试、分区、失败处理
Spring Cloud Stream 通过 Binder(绑定器)抽象,把这些差异屏蔽在业务代码之外。
可以把它概括为:
- 业务代码只关心“我发什么消息、我收什么消息”
- Binder 负责把这些消息对接到具体的 Kafka 或 RabbitMQ
Stream 的核心概念
最重要的几个概念是:
- Binder: 对接具体消息中间件的适配层
- Binding: 应用和消息中间件之间的绑定关系
- Producer: 生产消息的一方
- Consumer: 消费消息的一方
一个函数式风格的简单例子
1
2
3
4
@Bean
public Consumer<String> orderPaidConsumer() {
return message -> System.out.println("收到支付成功消息:" + message);
}
如果配合配置:
1
2
3
4
5
6
7
8
spring:
cloud:
function:
definition: orderPaidConsumer
stream:
bindings:
orderPaidConsumer-in-0:
destination: order-paid-topic
那么应用就会订阅 order-paid-topic,并把消息交给 orderPaidConsumer 处理。
Stream 的价值
它在工程上的好处主要是:
- 让业务代码和具体 MQ 实现解耦
- 更容易在不同中间件之间切换
- 更适合事件驱动、异步削峰、最终一致性的场景
Bus 和 Stream 的关系
很多人会把 Spring Cloud Bus 和 Spring Cloud Stream 混在一起,其实它们关注点不一样:
- Spring Cloud Stream: 面向业务消息流
- Spring Cloud Bus: 面向服务之间的事件广播,比如配置刷新
可以理解成:
- Stream 更像“业务层消息编程模型”
- Bus 更像“框架层事件传播机制”
一句话总结
Spring Cloud Stream 不是一个消息队列本身,而是一层把业务代码和消息中间件解耦的抽象。
Spring Cloud 全家桶常见组合
放到实际项目里,常见组合大致可以分成两类。
经典 Netflix 组合
Eureka:注册中心Ribbon:客户端负载均衡Feign:服务调用Hystrix:熔断降级Zuul:网关Config + Bus:配置中心与动态刷新
这套组合非常适合理解 Spring Cloud 微服务的基本思想,也是很多老项目的真实现状。
现在更常见的组合
Nacos:注册中心 + 配置中心Gateway:统一网关入口OpenFeign:声明式服务调用Sentinel:流量治理、限流、熔断、降级Sleuth/Zipkin或Micrometer Tracing:链路追踪Stream:消息驱动微服务
如果再往前走一步,很多项目还会继续引入:
Seata:分布式事务RocketMQ / Kafka:异步解耦与削峰Kubernetes:容器编排与服务治理
按职责记忆
整套 Spring Cloud 全家桶可以按职责拆成几个维度来看:
- 服务发现: Eureka、Nacos
- 服务调用: Ribbon、Feign、OpenFeign
- 网关层: Zuul、Gateway
- 服务保护: Hystrix、Sentinel、Resilience4j
- 配置管理: Config、Nacos、Bus
- 可观测性: Sleuth、Zipkin、Micrometer Tracing
- 消息驱动: Stream
这样记,会比死背组件名字更容易建立整体认知。