CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
Spring Cloud 微服务入门/课时/熔断与容错
RawBlame
课时目录10 节

Spring Cloud 微服务入门

  1. 01微服务与 Spring Cloud
  2. 02架构模式与术语
  3. 03服务注册与 Eureka
  4. 04Spring Cloud Gateway
  5. 05Cloud Config 配置中心
  6. 06OpenFeign 服务调用
  7. 07熔断与容错
  8. 08Spring Data 扩展
  9. 09Micrometer 与可观测性
  10. 10综合练习与部署直觉

熔断与容错

断路器、降级、Resilience4j 直觉。

35 分钟

熔断与容错

这一课你将能做到

  1. 给上一课的 ProblemClient 配上 Resilience4j 熔断器和 fallback,problem-service 挂掉时学习面板照常返回兜底数据
  2. 说出断路器三状态(CLOSED → OPEN → HALF_OPEN)的转换条件,并能通过 actuator 端点现场观察状态翻转
  3. 分清熔断、降级、限流各解决什么问题,能画出「超时 → 熔断 → 降级 → 限流」的雪崩防护组合拳

为什么要学

上一课配好了 Feign 和超时,但只解决「等太久就报错」。想象线上场景:problem-service 因慢 SQL 卡住,响应从 50ms 恶化到 8s。user-service 里每个进学习面板的请求都卡在 Feign 调用上 8 秒,Tomcat 默认 200 个工作线程几秒内被占满——这时连不需要题目数据的接口(登录、查个人信息)也全部无响应。problem 服务的故障,把完全无辜的 user 服务拖死了,这就是服务雪崩。

超时只是把「无限等」变成「等 N 秒后报错」,线程依然被占用。断路器(Circuit Breaker)再加一层:发现下游持续失败就主动「跳闸」,之后的请求不再真正调用下游(不再占用线程等超时),而是立刻走降级逻辑返回兜底数据。等下游缓过来,再放少量请求「试探」,成功了就恢复。

Spring Cloud CircuitBreaker 是抽象层,Spring Cloud 2023.0.1 的默认实现是 Resilience4j(Netflix Hystrix 已进入维护模式,新项目不用)。

先建立图像

                user-service 调用 problemClient.getProblem(id)
                                  │
                    ┌─── 断路器当前状态? ───┐
                    ▼           ▼           ▼
                 CLOSED       OPEN      HALF_OPEN
                 (闭合)     (打开)    (半开试探)
                    │           │           │
              正常发起调用   直接走fallback  放行少量
              统计失败率     (不占线程)    试探请求
                    │           │           │
          失败率≥50%?           │      试探成功?
            是│       │否       │        是│     │否
              ▼       ▼         │          ▼     ▼
            OPEN    继续CLOSED   │       CLOSE  OPEN
              │                 │
        等 10s 冷却 ─────────────▶ 转入 HALF_OPEN
状态行为离开条件
CLOSED(闭合)正常调用,用滑动窗口统计失败率失败率 ≥ 阈值 → OPEN
OPEN(打开)拒绝调用,直接走 fallback,不占用线程冷却时间到 → HALF_OPEN
HALF_OPEN(半开)只放行少量试探请求达标 → CLOSED,不达标 → OPEN

再记住三兄弟的分工——它们经常被混为一谈:

手段针对什么动作谁来做
熔断下游持续故障跳闸,停止调用,防止故障传染调用方(Resilience4j)
降级
任何「拿不到正常结果」的场景
返回兜底数据/默认值/缓存旧值
业务代码(fallback 方法)
限流流量超过本服务容量拒绝/排队多余请求,保护自己网关或本服务(如 Bucket4j、Sentinel)

降级不是独立开关,它是熔断、超时、异常发生时的兜底动作;熔断是触发降级的机制之一。

跟着做

步骤 1:user-service 添加依赖

在上一课 openfeign、eureka-client、loadbalancer 的基础上追加:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

步骤 2:打开 Feign 的熔断开关

spring:
  cloud:
    openfeign:
      circuitbreaker:
        enabled: true     # 打开后每个 Feign Client 都会被断路器包裹

步骤 3:编写 fallback 并挂到 Feign 接口上

改写 ProblemClient(fallback 指向一个实现了同接口的 Spring Bean):

package com.example.user.client;

import com.example.user.dto.ProblemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient(
        name = "problem-service",
        fallback = ProblemClientFallback.class   // 兜底实现
)
public interface ProblemClient {

    @GetMapping("/problems/{id}")
    ProblemDTO getProblem(@PathVariable("id") Long id);
}

fallback 类——注意必须加 @Component,方法签名与接口完全一致:

package com.example.user.client;

import com.example.user.dto.ProblemDTO;
import org.springframework.stereotype.Component;

@Component
public class ProblemClientFallback implements ProblemClient {

    @Override
    public ProblemDTO getProblem(Long id) {
        // 降级:返回兜底数据,而不是让异常炸穿学习面板
        return new ProblemDTO(id, "题目信息暂不可用,请稍后刷新", "UNKNOWN");
    }
}

步骤 4:配置 Resilience4j 实例参数

resilience4j:
  circuitbreaker:
    instances:
      problem-service:                 # 实例名 = Feign Client 的服务名
        slidingWindowSize: 10          # 滑动窗口:统计最近 10 次调用
        minimumNumberOfCalls: 5        # 窗口内不足 5 次不计算失败率(防止偶发误判)
        failureRateThreshold: 50       # 失败率 ≥ 50% 就跳闸(OPEN)
        waitDurationInOpenState: 10s   # OPEN 持续 10 秒后进入 HALF_OPEN
        permittedNumberOfCallsInHalfOpenState: 3   # 半开状态放行 3 个试探请求
        automaticTransitionFromOpenToHalfOpenEnabled: true  # 到点自动转半开(否则等下一个请求触发)
        slowCallDurationThreshold: 3s  # 超过 3s 的调用记为"慢调用"
        slowCallRateThreshold: 80      # 慢调用占比 ≥ 80% 也跳闸

步骤 5:注解版 @CircuitBreaker(非 Feign 场景)

不是走 Feign 的调用(比如 RestClient 调第三方 API、本地慢操作),用注解手动包裹:

package com.example.user.service;

import com.example.user.dto.ProblemDTO;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import org.springframework.stereotype.Service;

@Service
public class ProblemService {

    private final ProblemClient problemClient;

    public ProblemService(ProblemClient problemClient) {
        this.problemClient = problemClient;
    }

    @CircuitBreaker(name = "problem-service", fallbackMethod = "getProblemFallback")
    public ProblemDTO getProblem(Long id) {
        return problemClient.getProblem(id);
    }

    /** 签名要求:与原方法一致,最后多一个 Throwable 参数 */
    public ProblemDTO getProblemFallback(Long id, Throwable t) {
        return new ProblemDTO(id, "题目信息暂不可用", "UNKNOWN");
    }
}

步骤 6:用 actuator 观察熔断器状态(强烈建议亲手做)

management:
  endpoints:
    web:
      exposure:
        include: health,circuitbreakers

访问 http://localhost:8081/actuator/circuitbreakers,能看到每个实例的实时状态。

实验流程:

  1. 一切正常时访问 http://localhost:8081/dashboard/1001/problem/1,状态是 CLOSED;
  2. 手动停掉 problem-service,继续连续访问(超过 5 次)——前几次报超时,之后立刻变成兜底文案,状态翻成 OPEN,且响应时间从 8 秒级骤降到毫秒级(不再傻等超时);
  3. 等 10 秒后状态变为 HALF_OPEN;把 problem-service 重新启动,试探请求成功,状态回到 CLOSED。

讲清楚要点

  1. 滑动窗口怎么计数:Resilience4j 默认按「次数」维护一个环形数组(slidingWindowSize: 10,只看最近 10 次调用);也可配 slidingWindowType: TIME_BASED 改为按时间窗口统计。窗口内调用数达到 minimumNumberOfCalls 才开始计算失败率,避免上线头几个请求偶发失败就误跳闸。
  2. 状态机转换条件(必背):CLOSED 状态下,滑动窗口内失败率 ≥ failureRateThreshold(或慢调用率 ≥ slowCallRateThreshold)→ OPEN;OPEN 持续 waitDurationInOpenState → HALF_OPEN;HALF_OPEN 中试探请求失败率低于阈值 → CLOSED,否则再次 → OPEN。
  3. OPEN 期间的请求零成本:连下游都不碰,直接进 fallback——这就是熔断保护调用方线程池的核心价值,也是它和「超时重试」的本质区别。
  4. Feign 集成方式有两条路:开 spring.cloud.openfeign.circuitbreaker.enabled=true 后用 @FeignClient(fallback=...)(整套接口的兜底)或 fallbackFactory(能拿到异常原因,排查问题更好用);非 Feign 场景用 @CircuitBreaker(name, fallbackMethod) 注解。注意 fallback 触发的前提是断路器介入,只配 fallback 类、不开 circuitbreaker.enabled 是不生效的。
  5. 熔断 ≠ 降级 ≠ 限流:熔断是「要不要调用下游」的开关;降级是「拿不到正常结果时返回什么」的业务动作;限流是「保护自己不被流量打垮」,放在网关或入口层,三者组合使用。
  6. Resilience4j 是一套工具箱:除了 CircuitBreaker 还有 Bulkhead(舱壁隔离,限制并发调用数,防止单个下游吃光线程池)、TimeLimiter(超时限制,配合 Reactive 使用)、Retry(有限重试,只适合瞬时抖动,下游真挂时重试是火上浇油)。

你可能会懵的地方

  • fallback 什么时候不生效? ①没开 spring.cloud.openfeign.circuitbreaker.enabled=true;②fallback 类没加 @Component(或没被扫描到);③fallback 方法签名与接口不一致;④抛出的是 FeignException 之外的、被你自己的代码 catch 掉的异常——断路器根本没机会介入。逐项排查。

  • 熔断会不会误伤?下游只是抖了一下就跳闸? minimumNumberOfCalls: 5 + 失败率阈值 50% 意味着至少要「最近 10 次里挂掉一半」才跳闸,偶发一两次失败不触发。参数调优的本质就是权衡「保护灵敏度」和「误伤率」,没有万能值。

  • 降级返回兜底数据,用户体验受损了怎么办? 降级设计是产品问题不只是技术问题:能缓存的用上次的 Redis 缓存旧值,能算默认值的给默认值,实在不行的才提示「稍后重试」。原则是核心链路可用、次要信息可缺——学习面板少了题目副标题,远好过整个页面 500。

  • Retry 和 CircuitBreaker 一起用,顺序是? Retry 在外层、CircuitBreaker 在内层:先重试(比如重试 2 次),重试耗尽的结果计入熔断统计。这样瞬时抖动被重试消化,持续故障才触发熔断。反过来配会让每次重试都被熔断拦截,重试形同虚设。

  • OPEN 状态下所有请求都降级,有没有例外? 默认所有请求都走 fallback。HALF_OPEN 只放行 permittedNumberOfCallsInHalfOpenState 个试探请求,其余照样降级。这就是为什么冷却时间不能设太长——否则下游恢复了,流量还要排队等试探。

错法对照

错法问题正解
只配了 fallback 类,不开 circuitbreaker.enabled断路器没接入 Feign,降级永远不触发两个配置成对出现
waitDurationInOpenState: 60s 且不支持半开下游已恢复却要等满 60s,白降级 1 分钟10s 左右 + 自动转半开试探
在 fallback 里继续调用同一个下游降级方法本身又卡住,线程照样被占fallback 只用本地数据/缓存,不发远程调用
用 Retry 兜底下游宕机每个请求都傻等 3 次重试,放大故障重试只留给瞬时抖动,持续故障靠熔断
无脑 failureRateThreshold: 10轻微抖动就跳闸,频繁误伤正常流量失败率 50% 起步,配合 minimumNumberOfCalls

卡点急救

  1. 启动报 No fallback instance of type ... found:fallback 类忘了 @Component,或它所在的包不在 @ComponentScan 范围内。
  2. 熔断器状态永远是 CLOSED,下游挂了也不降级:确认访问的是同一个实例名(@FeignClient(name) == yml 里 resilience4j.circuitbreaker.instances 的 key);确认请求真的发出去了(minimumNumberOfCalls 没达到之前不统计)。
  3. 想看到 HALF_OPEN 却总是直接 CLOSED:automaticTransitionFromOpenToHalfOpenEnabled 没开时,要等下一个请求到来才检查冷却时间;开了之后状态会准时翻转,刷新 actuator 端点观察。

面试常问

  • 断路器三状态的转换条件?(必背) CLOSED:统计滑动窗口内的调用失败率(或慢调用比例),达到 failureRateThreshold 转 OPEN;OPEN:不再发起真实调用、直接降级,持续 waitDurationInOpenState 后转 HALF_OPEN;HALF_OPEN:放行 permittedNumberOfCallsInHalfOpenState 个试探请求,成功率达到预期则转回 CLOSED,否则再次 OPEN。核心价值:OPEN 期间请求不占用线程等待超时,把故障隔离在下游,防止线程池耗尽引发的雪崩。

  • 熔断、降级、限流的区别? 限流在入口处拒绝超出容量的请求,保护自己;熔断在调用方检测下游持续故障后跳闸,停止调用、防止故障传染;降级是熔断/超时/异常发生时的兜底动作,返回默认值、缓存或提示。线上组合拳:入口限流(网关层)→ 调用超时(Feign/TimeLimiter)→ 熔断防传染(Resilience4j)→ 降级保体验(fallback),四层从外到内层层设防。

  • Hystrix 和 Resilience4j 怎么选?为什么? 新项目选 Resilience4j:Hystrix 已停止更新、只支持线程池/信号量隔离的老模型;Resilience4j 是函数式轻量库、原生支持 Spring Boot 3 / Jakarta、提供滑动窗口 + 慢调用检测 + Bulkhead/Retry/RateLimiter 全家桶,与 Spring Cloud CircuitBreaker 抽象无缝集成。面试能说出「Spring Cloud 2023 默认实现就是 Resilience4j」即可。

自测三问

  1. HALF_OPEN 状态放行的 3 个试探请求全部成功后,断路器进入什么状态?
  2. Feign 的 fallback 生效需要哪两个条件同时满足?
  3. 为什么 OPEN 状态能保护调用方的 Tomcat 线程池?

小结一句话

断路器用「滑动窗口统计失败率 → 状态机自动跳闸 → 半开试探恢复」保护调用方,fallback 提供降级兜底,与超时、限流组合成完整的雪崩防护体系。

延伸

  • Resilience4j 官方文档(CircuitBreaker 章节)
  • Spring Cloud CircuitBreaker 参考文档
  • 下一课:Spring Data 扩展——微服务里每个服务怎么选存储、怎么各用各的数据库。
加载本节练习…
← 上一课OpenFeign 服务调用下一课 →Spring Data 扩展