Skip to content

Resilience4j内部原理与生产治理

Resilience4j保护的是“未来是否继续进入某个故障域,以及最多允许多少工作占用资源”,不是把远程调用变成本地可靠调用。真正理解它,需要从函数装饰、许可获取、结果记录、滑动窗口聚合、状态并发转换、异步取消边界、AOP顺序、自动配置、指标和业务事实一路推演。

本章源码结论以以下可复核组合为样本:

层次版本说明
Java17目标字节码Boot 3现代线,major version 61
Spring Boot3.5.16Maven Central截至2026-07-19的Boot 3维护线样本
Spring Cloud2025.0.3与Boot 3.5线配套的发布列车样本
Spring Cloud CircuitBreaker3.3.3Cloud BOM实际解析版本
Resilience4j2.2.0Cloud BOM实际解析版本,不是独立最新2.4.0
JDK 8边界Resilience4j 1.7.1major version 52,仅用于存量维护

版本变化可能调整默认值、属性和自动配置条件。本文凡是提到默认顺序、默认线程池或默认参数,均限定在上述源码样本;升级时应重新查看目标BOM、配置元数据、依赖树和源码。

一、学习目标

学完后应能:

  1. decorateSupplier推演一次许可、调用、计时和结果记录全过程。
  2. 解释CircuitBreakerStateMachine为何使用AtomicReference<State>和各状态自己的原子变量。
  3. 解释Count-based与Time-based滑动窗口的Subtract-on-Evict实现。
  4. 说明minimumNumberOfCalls为什么比单独的失败率阈值更重要。
  5. 区分失败、慢成功、慢失败、忽略异常和拒绝调用的统计语义。
  6. 解释OPEN怎样惰性或定时进入HALF_OPEN,以及探测许可如何并发扣减。
  7. 说明SemaphoreBulkhead和FixedThreadPoolBulkhead的真实资源边界。
  8. 解释AtomicRateLimiter的Cycle、负许可、CAS和等待语义。
  9. 解释同步Retry为何阻塞等待,异步Retry怎样调度下一次Attempt。
  10. 解释TimeLimiter为什么只能限制Future等待,不能保证撤销远端副作用。
  11. 推演Spring Cloud CircuitBreaker Factory创建、配置查找、线程池、TimeLimiter、Bulkhead、CircuitBreaker和Fallback顺序。
  12. 推演原生注解的AOP顺序、自调用失效、Fallback签名与异常选择。
  13. 设计订单、库存、支付和接口聚合的超时、重试、隔离、熔断和降级。
  14. 用Metrics、事件、日志和Trace定位熔断、拒绝、超时和重试风暴。

二、先建立三个不同层次

层次代表对象回答的问题
原生内核CircuitBreaker、Retry、Bulkhead、RateLimiter、TimeLimiter许可、统计、状态和函数装饰怎样工作
Spring原生集成Registry、Aspect、AutoConfiguration配置如何变成Bean和注解代理
Spring Cloud门面CircuitBreakerFactory、Resilience4JCircuitBreaker如何按id/group统一创建并执行保护器

很多文档把三层混在一起,导致读者误以为@CircuitBreaker、Spring Cloud的factory.create()和原生CircuitBreaker.decorateSupplier()完全等价。它们最终会使用相同内核,但配置来源、线程切换、TimeLimiter、Bulkhead和观测包装方式并不相同。

mermaid
flowchart TD
    A["业务方法或Supplier"] --> B["Spring AOP或Cloud Factory适配"]
    B --> C["Resilience4j Registry查找实例"]
    C --> D["原生模块装饰函数"]
    D --> E["许可、执行、统计与事件"]
    E --> F["Fallback或结果返回"]

三、原生CircuitBreaker一次调用怎样走

核心伪代码可以理解为:

java
if (!circuitBreaker.tryAcquirePermission()) {
    throw CallNotPermittedException.createCallNotPermittedException(circuitBreaker);
}
long started = circuitBreaker.getCurrentTimestamp();
try {
    T result = supplier.get();
    circuitBreaker.onResult(elapsed(started), unit, result);
    return result;
} catch (Throwable error) {
    circuitBreaker.onError(elapsed(started), unit, error);
    throw error;
}

真实API还处理Checked异常、CompletionStage、Reactor、结果Predicate和许可释放,但主线不变:先问当前State是否允许,执行后按结果类型记录。熔断器不是在请求到来前“远程探测下游健康”,而是根据本应用实例过去的调用结果做本地决策。

mermaid
flowchart TD
    A["调用装饰后的Supplier"] --> B["当前State检查许可"]
    B --> C["拒绝则发布NOT_PERMITTED并快速失败"]
    C --> D["放行则计时并执行真实调用"]
    D --> E["返回值按ResultPredicate分类"]
    E --> F["异常按Ignore和Record Predicate分类"]
    F --> G["更新滑动窗口并检查阈值"]

图中“拒绝”和“放行”是互斥分支,为保证小屏纵向可读而按判断阶段顺序展示;拒绝后不会继续执行D到G。

四、StateMachine怎样保证并发状态转换

CircuitBreakerStateMachine把当前状态对象保存在AtomicReference<CircuitBreakerState>中。状态转换使用getAndUpdate原子替换:

  1. 读取当前状态。
  2. 校验该转换是否合法。
  3. 调用旧状态的preTransitionHook取消相关定时任务。
  4. 创建新状态对象。
  5. 原子替换引用。
  6. 发布外部状态转换事件。

每个状态内部还有自己的原子闸门:

状态原子控制目的
CLOSEDAtomicBoolean isClosed多线程同时发现超阈值时只允许一个线程开路
OPENAtomicBoolean isOpen等待到期时只允许一个线程转HALF_OPEN
HALF_OPENAtomicInteger permittedNumberOfCalls原子发放有限探测许可
HALF_OPENAtomicBoolean isHalfOpen多个探测同时完成时只转换一次

所以状态转换不是简单的if (state == CLOSED) state = OPEN。没有CAS闸门时,多个并发完成事件可能重复建状态、重复调度任务、覆盖Metrics或发布多次转换事件。

五、CLOSED怎样判断开路

CLOSED总是允许新调用,调用完成后进入CircuitBreakerMetrics

  1. 按耗时是否超过slowCallDurationThreshold分类为普通或慢调用。
  2. 按调用结果分类为成功或失败。
  3. 记录到滑动窗口。
  4. 取得预聚合Snapshot。
  5. 若样本不足,返回BELOW_MINIMUM_CALLS_THRESHOLD
  6. 否则分别计算Failure Rate与Slow Call Rate。
  7. 任一达到阈值,CAS把isClosed从true改为false。
  8. 成功CAS的线程发布阈值事件并转换到OPEN。

5.1 失败率公式

text
failureRate = failedCalls / totalCalls × 100%

5.2 慢调用率公式

text
slowCallRate = slowCalls / totalCalls × 100%

只有totalCalls >= minimumNumberOfCalls才计算有效比例,否则返回-1表达样本不足。以窗口20、最小调用10、失败阈值50%为例:前9次即使全部失败也不会按该规则自动开路;第10次完成后才有资格判断。

六、异常分类决定熔断器看见什么

不是所有异常都代表“下游不健康”:

结果推荐语义
连接失败、读取超时、HTTP 5xx通常计入系统失败
HTTP 429表示过载,是否计失败及重试要结合Retry-After
参数校验失败、权限拒绝调用方或业务错误,通常不代表下游故障
库存不足正常业务结果,不应当作系统异常率
订单不存在可能是正常404,也可能是数据同步故障,需按契约分类
Fallback抛错是降级失败,应独立观测

Resilience4j提供:

  • recordExceptionsrecordExceptionPredicate决定哪些异常算失败。
  • ignoreExceptionsignoreExceptionPredicate决定哪些异常不进入成功/失败统计。
  • recordResultPredicate可以把某些正常返回对象视为失败。

先执行Ignore判断,再按Record规则分类。若把所有业务异常都计失败,熔断器会在业务高峰把“库存不足”误认为服务故障;若忽略范围过宽,又会掩盖真正的连接和数据问题。

七、次数滑动窗口源码原理

FixedSizeSlidingWindowMetrics维护:

  • 长度为N的Measurement[]循环数组。
  • 当前headIndex
  • 一个TotalAggregation总聚合。

记录一次调用时方法使用synchronized保证数组和总量一致:

mermaid
flowchart TD
    A["记录新Outcome"] --> B["先累加TotalAggregation"]
    B --> C["headIndex循环前移一格"]
    C --> D["从Total减去将被覆盖的Measurement"]
    D --> E["重置旧Measurement"]
    E --> F["把新Outcome写入该格"]
    F --> G["基于Total创建Snapshot"]

这就是Subtract-on-Evict。窗口不是保存N个完整请求对象,而是保存N个测量聚合。获取Snapshot直接复制预聚合总量,时间复杂度O(1);内存随窗口N增长。

八、时间滑动窗口源码原理

SlidingTimeWindowMetrics维护N个PartialAggregation,每个Bucket对应一个epoch second:

  1. 当前秒内所有调用累加到同一Bucket。
  2. 时间跨秒时,计算当前秒与最新Bucket秒数的差。
  3. 最多前移N格,避免长时间空闲后做无限循环。
  4. 每前移一格,从总聚合减去被淘汰Bucket。
  5. 重置Bucket并写入对应epoch second。

时间窗口内存取决于秒数N,不取决于该秒有100次还是100万次调用。它统计的是最近N秒聚合,不是“最近N次”;低流量接口中可能很久达不到minimumNumberOfCalls,应根据真实QPS选择。

九、OPEN怎样进入HALF_OPEN

OPEN有两种进入HALF_OPEN的方式:

9.1 惰性转换

默认可在下一次tryAcquirePermission时检查当前时间是否超过retryAfterWaitDuration。到期后第一个成功CAS的线程执行转换,再从新HALF_OPEN状态申请探测许可。

9.2 自动定时转换

开启automaticTransitionFromOpenToHalfOpenEnabled后,OPEN构造时通过调度器安排定时任务,到期主动转换。优点是不需要等待下一次业务请求触发;代价是每个熔断器可能需要调度管理,实例数量很多时要评估调度资源。

OPEN期间拒绝的调用增加numberOfNotPermittedCalls,但不会访问下游。CallNotPermittedException表示本地保护器拒绝,不能误报成下游本次又失败了一次。

十、HALF_OPEN为什么需要有限许可

HALF_OPEN构造时创建一个次数窗口,大小等于允许探测数,并用AtomicInteger原子扣减许可。探测完成后:

  • 达到失败率或慢调用率阈值,CAS回OPEN。
  • 样本完成且低于阈值,CAS回CLOSED。
  • 未完成足够样本时继续等待剩余探测。
  • 配置maxWaitDurationInHalfOpenState后,探测迟迟不完成可定时回OPEN。

若下游恢复后立刻放开全部流量,缓存重建、连接预热和数据库恢复尚未完成,容易形成Recovery Storm。HALF_OPEN的有限探测本质是恢复阶段的Admission Control。

十一、FORCED_OPEN、DISABLED与METRICS_ONLY

11.1 FORCED_OPEN

始终拒绝并统计not permitted。适合明确切断故障依赖,但必须有权限、审计、TTL和恢复流程;手工开关忘记关闭会造成长期业务不可用。

11.2 DISABLED

始终放行,不记录正常熔断Metrics事件。适合彻底关闭保护器,但故障时没有自动保护。不要把DISABLED当“只关闭拒绝、仍保留完整统计”。

11.3 METRICS_ONLY

始终放行并统计,当阈值超限时发布相关事件,但不自动OPEN。适合新规则灰度观察:先验证异常分类和阈值会不会误判,再启用真正开路。

十二、SemaphoreBulkhead源码原理

构造时创建:

java
new Semaphore(maxConcurrentCalls, fairCallHandlingEnabled)

tryAcquirePermissionmaxWaitDuration尝试获取许可,失败发布REJECTED事件;成功发布PERMITTED事件。业务完成后必须onComplete()releasePermission()归还许可,否则许可泄漏会让Bulkhead永久变满。

公平信号量让等待较久的线程更有机会先获得许可,但公平调度可能降低吞吐。maxWaitDuration=0适合快速失败;允许等待时,等待时间必须小于剩余Deadline,否则线程只是从下游等待转成Bulkhead等待。

动态降低maxConcurrentCalls时,源码会通过acquireUninterruptibly收回多余许可;如果当前占用很多,配置变更线程可能等待已有调用释放。动态配置不是无成本的瞬时数字替换。

十三、ThreadPoolBulkhead源码原理

FixedThreadPoolBulkhead创建有界ThreadPoolExecutor

  • core size:核心线程。
  • max size:最大线程。
  • queue capacity为0时使用SynchronousQueue
  • queue capacity大于0时使用ArrayBlockingQueue
  • 默认拒绝处理器是AbortPolicy

提交Callable后:

  1. 使用配置的ContextPropagator装饰任务。
  2. CompletableFuture.supplyAsync提交到专属Executor。
  3. 线程或队列有容量则发布PERMITTED。
  4. 执行完成发布FINISHED并完成外层Promise。
  5. RejectedExecutionException转换为BulkheadFullException并发布REJECTED。
mermaid
flowchart TD
    A["提交Callable"] --> B["ContextPropagator捕获上下文"]
    B --> C["检查线程或队列容量"]
    C --> D["无容量则抛BulkheadFullException"]
    D --> E["有容量则由专属线程执行"]
    E --> F["发布完成事件"]
    F --> G["完成CompletableFuture"]

D和E是互斥结果,无容量时不会继续执行E;这里按纵向判断阶段展示,避免移动端出现宽分支图。

13.1 为什么队列100不一定安全

2个线程、队列100、每次5秒,队尾理论上可能等待约250秒。请求早已超时,任务仍会迟到执行。生产参数必须由下游容量、每次耗时、总Deadline和允许排队年龄计算,不能照抄默认值。

13.2 上下文传播

线程切换后MDC、Trace、SecurityContext和租户上下文不会天然存在。Resilience4j有ContextPropagator,但Spring Boot 3项目还要与Micrometer Context Propagation、Observation和目标Executor装饰策略统一,避免只复制MDC字符串而真实Trace断链。

十四、TimeLimiter源码与取消边界

14.1 Future路径

源码调用:

java
future.get(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS)

超时时创建带组件名的TimeoutException,发布TIMEOUT事件;若cancelRunningFuture=true则执行future.cancel(true)。这通常向执行线程发送中断,但不保证:

  • 阻塞Socket立即关闭。
  • JDBC驱动取消服务器SQL。
  • 远端HTTP请求停止。
  • 已提交数据库事务回滚。
  • 第三方接口撤销副作用。

14.2 CompletionStage路径

通过ScheduledExecutorService安排一个超时任务;到期且Future未完成时,把CompletableFuture异常完成。原始异步操作仍可能继续执行。因此超时处理必须区分:

text
调用方不再等待 ≠ 下游没有执行 ≠ 业务事务已失败

写操作需要幂等键、状态查询、Outbox或补偿。HTTP客户端仍必须有真实网络超时,数据库仍必须有Query Timeout。

十五、Retry内部执行与流量放大

同步Retry为每个逻辑调用创建Context,维护Attempt计数和最后异常。每次失败后:

  1. 判断异常或结果是否满足Retry Predicate。
  2. 判断当前Attempt是否达到maxAttempts
  3. 通过IntervalFunction或IntervalBiFunction计算等待。
  4. 发布RETRY事件。
  5. 同步路径使用Thread.sleep等待。
  6. 再执行下一次物理调用。
  7. 最终发布SUCCESS或ERROR事件并更新Metrics。

同步等待会占住调用线程;异步Retry使用调度器安排下一Attempt,但仍会增加下游物理请求。

15.1 指数退避与抖动

固定500ms会让大量实例同时失败后在同一时刻重试,形成同步脉冲。指数退避拉开尝试间隔,随机抖动打散实例;仍必须受总Deadline和Retry Budget限制。

15.2 maxAttempts语义

maxAttempts=3包含首次调用,即最多三个总Attempt。面试和监控要明确“逻辑请求数”与“物理Attempt数”,否则QPS看似100,实际下游可能收到300。

十六、AtomicRateLimiter源码原理

AtomicRateLimiter把时间按limitRefreshPeriod划分Cycle,State保存:

  • config。
  • activeCycle。
  • activePermissions。
  • nanosToWait。

获取许可时:

  1. 用单调纳秒时间计算当前Cycle。
  2. 若跨越多个Cycle,按周期累加许可,但上限不超过limitForPeriod
  3. 若许可不足,计算需要等待多少完整Cycle。
  4. 若等待不超过timeoutDuration,先预留许可,activePermissions可以变成负数。
  5. 通过AtomicReference CAS替换整个State。
  6. CAS竞争失败时短暂backoff后重试。
  7. 根据nanosToWait立即通过、park等待或拒绝。
mermaid
flowchart TD
    A["请求许可"] --> B["计算当前Cycle"]
    B --> C["补充跨周期许可"]
    C --> D["许可足够则CAS扣减并立即通过"]
    D --> E["许可不足则计算未来等待时间"]
    E --> F["超过timeout则拒绝"]
    F --> G["可按时获得则CAS预留并park等待"]

D和E代表互斥条件分支,纵向排列是为了避免宽图;许可足够的请求不会继续进入等待路径。

本地RateLimiter每个应用实例各自维护状态。10个实例各100 QPS,集群理论总许可约1000 QPS;它不能替代Redis、Gateway、Envoy或集中配额系统的全局限流。

十七、Registry为什么重要

每个模块都有Registry,用名称映射实例和配置:

  • CircuitBreakerRegistry
  • RetryRegistry
  • BulkheadRegistry
  • ThreadPoolBulkheadRegistry
  • RateLimiterRegistry
  • TimeLimiterRegistry

同名实例会复用状态。若把每个orderId拼进名称,会创建海量熔断器、Metrics时间序列和事件对象;名称应表达稳定故障域,例如inventory-querypayment-status,而不是具体业务ID。

Registry还负责:

  • 默认配置与命名配置。
  • 实例创建与移除事件。
  • Tags。
  • Metrics Publisher绑定。
  • 按名称查找当前状态和指标。

十八、Resilience4j Boot 3自动配置启动链

resilience4j-spring-boot3通过AutoConfiguration.imports声明各模块自动配置:CircuitBreaker、Retry、Bulkhead、ThreadPoolBulkhead、RateLimiter、TimeLimiter、Metrics、Health Indicator、事件端点和调度器。

mermaid
flowchart TD
    A["Boot读取AutoConfiguration.imports"] --> B["绑定resilience4j.*配置属性"]
    B --> C["按类路径创建各模块Registry"]
    C --> D["创建Aspect与Fallback解析器"]
    D --> E["绑定Micrometer Metrics Publisher"]
    E --> F["按条件暴露Actuator与Health"]

自动配置通常有类路径和属性条件。只写YAML但缺少模块JAR,不会凭空创建Retry或Bulkhead;只有模块JAR没有AOP,也不会让注解方法自动被代理。

十九、Spring Cloud CircuitBreaker自动配置

样本中的Resilience4JAutoConfiguration在属性允许且没有其他CircuitBreakerFactory时创建Resilience4JCircuitBreakerFactory,注入:

  • CircuitBreakerRegistry。
  • TimeLimiterRegistry。
  • 可选Resilience4jBulkheadProvider。
  • Spring Cloud配置属性。
  • Factory Customizer。

当ObservationRegistry存在时,Factory创建的CircuitBreaker会包装为ObservedCircuitBreaker,使执行进入Spring观测体系。Micrometer存在时还会绑定Tagged CircuitBreaker和Bulkhead Metrics,并补group标签。

二十、Spring Cloud Factory创建实例的配置优先级

调用:

java
factory.create("inventory-query", "inventory-service")

样本源码按以下方向查找CircuitBreaker配置:

  1. id对应配置:inventory-query
  2. group对应配置:inventory-service
  3. Factory默认配置。

TimeLimiter也先查id,再查group,最后取Registry默认配置。配置属性通常先构建Registry中的命名配置,因此属性配置可能覆盖仅通过Factory Customizer设置的默认值。不要同时在YAML、Java Customizer、动态配置中心和代码临时修改四处定义同名阈值。

二十一、Spring Cloud run()真实调用顺序

在样本默认设置下,Factory有ExecutorService,TimeLimiter启用;若类路径存在Bulkhead模块,默认还会创建BulkheadProvider。阻塞调用主链为:

mermaid
flowchart TD
    A["factory.create按id和group构建适配器"] --> B["Registry取得CircuitBreaker与TimeLimiter"]
    B --> C["Supplier提交到Factory Executor"]
    C --> D["TimeLimiter限制Future等待"]
    D --> E["BulkheadProvider装饰Callable"]
    E --> F["CircuitBreaker装饰最外层Callable"]
    F --> G["执行并按结果更新状态"]
    G --> H["任意Throwable进入Fallback函数"]

需要注意:

  • 样本Factory默认disableThreadPool=false,并持有CachedThreadPool;这不等于业务应该接受无界线程增长,生产应通过Customizer配置受控Executor或评估禁用线程切换。
  • enableSemaphoreDefaultBulkhead=false时,Cloud Bulkhead Provider默认方向是ThreadPoolBulkhead;配置选择必须通过实际Bean和Metrics验证。
  • CircuitBreaker装饰在外层时,Bulkhead拒绝和TimeLimiter超时可能作为调用结果进入熔断统计,具体异常分类仍由CircuitBreakerConfig决定。
  • Fallback捕获Throwable,必须区分Timeout、CallNotPermitted、BulkheadFull和真实业务异常,不能全部返回同一个伪成功对象。

二十二、原生注解与Spring AOP顺序

Resilience4j 2.2.0 Spring 6样本默认Aspect Order:

AspectOrder
RetryLOWEST_PRECEDENCE - 5
CircuitBreakerLOWEST_PRECEDENCE - 4
RateLimiterLOWEST_PRECEDENCE - 3
TimeLimiterLOWEST_PRECEDENCE - 2
BulkheadLOWEST_PRECEDENCE - 1

Spring AOP中数值更小的Advice通常更外层,因此默认可理解为:Retry包住CircuitBreaker,CircuitBreaker包住RateLimiter,随后TimeLimiter,Bulkhead靠近目标方法。实际异步返回类型和模块Aspect实现仍会影响执行,应通过测试统计每次逻辑请求产生多少Attempt、每个模块记录多少调用。

22.1 为什么顺序改变统计

text
Retry(CircuitBreaker(call))

每个Retry Attempt可能分别经过CircuitBreaker并成为独立样本。

text
CircuitBreaker(Retry(call))

CircuitBreaker可能只看到整组Retry最终成功或失败。前者更快反映物理下游失败,但也可能因重试快速开路;后者统计逻辑请求结果,却隐藏物理放大。没有全局唯一正确顺序,必须先定义想统计什么。

22.2 自调用失效

同一个Bean内this.protectedMethod()没有经过Spring代理,注解Aspect通常不执行。解决方向是拆分Bean、从代理入口调用或使用显式Decorator;不要通过注入自己等晦涩方式掩盖设计问题。

二十三、Fallback方法怎样匹配

注解Fallback通常要求:

  • 参数与原方法一致,并可在末尾增加Throwable或其子类。
  • 返回类型兼容。
  • 异常类型越具体,匹配越优先。
  • Fallback本身抛出的异常要继续向上暴露并独立观测。

错误做法:

java
public InventoryView fallback(String skuId, Throwable error) {
    return new InventoryView(skuId, "AVAILABLE", false);
}

这会把未知库存伪造成有货。正确做法是返回UNKNOWNdegraded=true,或让核心写操作失败并进入事实查询/补偿。

二十四、生产配置示例与逐项解释

yaml
resilience4j:
  circuitbreaker:
    configs:
      inventory-base:
        sliding-window-type: COUNT_BASED
        sliding-window-size: 20
        minimum-number-of-calls: 10
        failure-rate-threshold: 50
        slow-call-duration-threshold: 500ms
        slow-call-rate-threshold: 60
        wait-duration-in-open-state: 10s
        permitted-number-of-calls-in-half-open-state: 3
        max-wait-duration-in-half-open-state: 3s
        automatic-transition-from-open-to-half-open-enabled: false
    instances:
      inventory-query:
        base-config: inventory-base
  bulkhead:
    instances:
      inventory-query:
        max-concurrent-calls: 20
        max-wait-duration: 0
  timelimiter:
    instances:
      inventory-query:
        timeout-duration: 800ms
        cancel-running-future: true
  retry:
    instances:
      inventory-query:
        max-attempts: 2
        wait-duration: 100ms
        retry-exceptions:
          - com.example.order.InventoryConnectionException
        ignore-exceptions:
          - com.example.order.InventoryBusinessException

management:
  endpoints:
    web:
      exposure:
        include: health,metrics,prometheus,circuitbreakers,circuitbreakerevents

解释:

  • 窗口20、最小10避免低样本误开。
  • 失败率50%和慢调用率60%任一达到即可开路。
  • 慢调用阈值500ms来自库存查询SLO,不是随便选择。
  • OPEN 10秒后只允许3个探测。
  • HALF_OPEN最多等待3秒,避免探测永远不返回。
  • Bulkhead无等待,容量满立即降级,避免占住请求线程。
  • TimeLimiter 800ms必须小于接口总Deadline。
  • Retry最多2个总Attempt,只重试明确连接失败。

属性名必须以目标版本配置元数据为准。生产配置变更需要审批、灰度、监控和回滚;把阈值放配置中心不代表可以无风险实时修改。

需要特别注意:resilience4j.retry只配置原生Retry Registry。Spring Cloud CircuitBreakerFactory.run()不会因为类路径里出现Retry模块就自动把Retry加入调用链;只有业务显式使用Retry Decorator、Registry或@Retry时才会生效。RateLimiter同理。隐式猜测“Starter会组合全部模块”是生产事故的常见来源。

二十五、Java 17与Boot 3完整商业Demo

25.1 完整POM关键部分

xml
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>3.5.16</version>
    <relativePath/>
</parent>

<properties>
    <java.version>17</java.version>
    <spring-cloud.version>2025.0.3</spring-cloud.version>
</properties>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>${spring-cloud.version}</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-aop</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-bulkhead</artifactId>
    </dependency>
    <dependency>
        <groupId>io.github.resilience4j</groupId>
        <artifactId>resilience4j-retry</artifactId>
    </dependency>
</dependencies>

版本由Cloud BOM管理。样本解析到CircuitBreaker 3.3.3和Resilience4j 2.2.0,不单独强制写2.4.0。

25.2 明确业务状态

java
package com.example.order;

public enum InventoryStatus {
    AVAILABLE,
    OUT_OF_STOCK,
    UNKNOWN
}
java
package com.example.order;

public record InventoryView(
        String skuId,
        InventoryStatus status,
        boolean degraded,
        String reason) {
}

Record属于Java 17现代线,JDK 8项目应改成普通POJO。

25.3 模拟真实下游

java
package com.example.order;

public class InventoryConnectionException extends RuntimeException {
    public InventoryConnectionException(String message) {
        super(message);
    }
}
java
package com.example.order;

import java.util.concurrent.atomic.AtomicInteger;
import org.springframework.stereotype.Component;

@Component
public class InventoryHttpClient {
    private final AtomicInteger attempts = new AtomicInteger();

    public InventoryView query(String skuId, String mode) {
        attempts.incrementAndGet();
        if ("fail".equals(mode)) {
            throw new IllegalStateException("inventory 503");
        }
        if ("connect".equals(mode)) {
            throw new InventoryConnectionException("connection refused");
        }
        if ("slow".equals(mode)) {
            try {
                Thread.sleep(1200);
            } catch (InterruptedException interrupted) {
                Thread.currentThread().interrupt();
                throw new IllegalStateException("interrupted", interrupted);
            }
        }
        return new InventoryView(
                skuId, InventoryStatus.AVAILABLE, false, "CONFIRMED");
    }

    public int attempts() {
        return attempts.get();
    }

    public void reset() {
        attempts.set(0);
    }
}

25.4 Spring Cloud门面调用

java
package com.example.order;

import java.util.function.Function;
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
import org.springframework.stereotype.Service;

@Service
public class InventoryGateway {
    private final CircuitBreakerFactory<?, ?> factory;
    private final InventoryHttpClient client;

    public InventoryGateway(CircuitBreakerFactory<?, ?> factory,
                            InventoryHttpClient client) {
        this.factory = factory;
        this.client = client;
    }

    public InventoryView query(String skuId, String mode) {
        CircuitBreaker breaker =
                factory.create("inventory-query", "inventory-service");
        Function<Throwable, InventoryView> fallback = error ->
                new InventoryView(
                        skuId,
                        InventoryStatus.UNKNOWN,
                        true,
                        error.getClass().getSimpleName());
        return breaker.run(() -> client.query(skuId, mode), fallback);
    }
}

25.5 控制器

java
package com.example.order;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class InventoryController {
    private final InventoryGateway gateway;
    private final InventoryHttpClient client;

    public InventoryController(InventoryGateway gateway,
                               InventoryHttpClient client) {
        this.gateway = gateway;
        this.client = client;
    }

    @GetMapping("/inventory/{skuId}")
    public InventoryView query(
            @PathVariable String skuId,
            @RequestParam(defaultValue = "success") String mode) {
        return gateway.query(skuId, mode);
    }

    @GetMapping("/inventory/attempts")
    public int attempts() {
        return client.attempts();
    }
}

25.6 启动与验证

bash
mvn spring-boot:run
curl "http://localhost:8080/inventory/SKU-1001?mode=success"
curl "http://localhost:8080/inventory/SKU-1001?mode=fail"
curl "http://localhost:8080/inventory/SKU-1001?mode=slow"
curl "http://localhost:8080/actuator/metrics/resilience4j.circuitbreaker.calls"

验证时不能只看HTTP 200,因为Fallback也可能返回200。还要检查degradedreason、真实Attempt、CircuitBreaker状态、not permitted、timeout、bulkhead rejected和下游日志。

25.7 原生Decorator显式组合Retry与CircuitBreaker

若业务确实需要Retry,应让顺序在代码中可见:

java
package com.example.order;

import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import io.github.resilience4j.retry.Retry;
import io.github.resilience4j.retry.RetryRegistry;
import java.util.function.Supplier;
import org.springframework.stereotype.Service;

@Service
public class NativeInventoryGateway {
    private final CircuitBreakerRegistry circuitBreakers;
    private final RetryRegistry retries;
    private final InventoryHttpClient client;

    public NativeInventoryGateway(CircuitBreakerRegistry circuitBreakers,
                                  RetryRegistry retries,
                                  InventoryHttpClient client) {
        this.circuitBreakers = circuitBreakers;
        this.retries = retries;
        this.client = client;
    }

    public InventoryView query(String skuId, String mode) {
        CircuitBreaker breaker = circuitBreakers.circuitBreaker("inventory-query");
        Retry retry = retries.retry("inventory-query");

        Supplier<InventoryView> attempt =
                CircuitBreaker.decorateSupplier(
                        breaker,
                        () -> client.query(skuId, mode));
        Supplier<InventoryView> retryOutsideCircuit =
                Retry.decorateSupplier(retry, attempt);

        try {
            return retryOutsideCircuit.get();
        } catch (RuntimeException error) {
            return new InventoryView(
                    skuId,
                    InventoryStatus.UNKNOWN,
                    true,
                    error.getClass().getSimpleName());
        }
    }
}

这里Retry在外、CircuitBreaker在内,所以每个物理Attempt分别经过CircuitBreaker。若交换两行装饰顺序,CircuitBreaker可能只看到整组Retry最终结果。显式组合的价值是让语义可读、可测试,而不是认为该顺序永远最好。

二十六、JDK 8存量线边界

Resilience4j 1.7.1可编译为Java 8字节码major 52;2.2.0是Java 17字节码major 61。JDK 8存量系统不能只改版本号使用2.x。

JDK 8代码还必须避免Record、Map.of、文本块和String.isBlank。核心原理仍一致:函数装饰、许可、滑动窗口、状态机和事件;但Spring Boot自动配置包名、Spring版本、Jakarta/Javax以及Micrometer集成都可能不同,必须按1.7.1目标源码和依赖编译测试。

二十七、Metrics、事件与可观测性

应至少观测:

模块关键维度
CircuitBreakerstate、successful、failed、slow、not_permitted
Retrysuccessful_without_retry、successful_with_retry、failed_with_retry、attempts
Bulkheadavailable_concurrent_calls、max_allowed、rejected、queue_depth
ThreadPoolBulkheadactive、queue、core、max、rejected
RateLimiteravailable_permissions、waiting_threads、failed_acquire
TimeLimitersuccessful、timeout、error

组件名应为有限稳定集合。不要把orderId、skuId、userId拼进CircuitBreaker名称或Metric标签。高基数业务ID进入受控Trace与脱敏日志,通过traceId关联。

事件消费者运行在调用相关路径时应保持轻量。事件回调里同步写远程数据库或再次调用故障服务,会让保护器本身增加延迟甚至递归故障。生产事件可转换为有界异步审计,但遥测失败不能拖垮核心业务。

二十八、失败窗口

失败窗口表面现象真实风险恢复方式
异常分类错误业务失败触发开路大量正常请求被拒绝修Predicate,先METRICS_ONLY灰度
最小调用量过小熔断频繁抖动低流量误判按QPS与窗口重算
OPEN时间过短下游未恢复就探测Recovery Storm延长冷却并有限HALF_OPEN
HALF_OPEN许可过多恢复瞬间洪峰再次打垮下游小批探测与预热
TimeLimiter超时上游已降级底层I/O仍执行客户端超时、幂等、事实查询
Retry包在多层逻辑QPS不高物理Attempt爆炸唯一重试层与Retry Budget
Bulkhead队列过大拒绝率不高排队超时和幽灵流量有界队列与队列Deadline
CachedThreadPool失控TimeLimiter看似有效线程增长与上下文丢失自定义受控Executor
Fallback依赖远程服务主调用降级Fallback再次阻塞本地、快速、独立保护
FORCED_OPEN忘记恢复下游已健康业务持续降级开关TTL、审计和恢复检查
配置中心误改全实例同时变阈值集体开路或失去保护灰度、审批、版本与回滚

二十九、生产Runbook

29.1 熔断器突然OPEN

  1. 先按实例和breaker name查看state变化时间。
  2. 查窗口内buffered、failed、slow与minimum calls。
  3. 核对哪些异常和结果被计为失败。
  4. 用Trace区分连接失败、HTTP 5xx、超时、Bulkhead拒绝和业务异常。
  5. 查下游P95/P99、线程、连接、数据库锁、GC和发布变更。
  6. 查Retry是否放大物理Attempt。
  7. 不要第一反应强制CLOSED;若下游仍故障会重新施压。

29.2 not permitted激增

  1. 区分OPEN拒绝、FORCED_OPEN还是运维配置。
  2. 检查OPEN等待时间和HALF_OPEN探测是否到达。
  3. 检查请求是否只落在部分本地OPEN实例。
  4. 确认Fallback容量和业务语义。
  5. 下游恢复后观察HALF_OPEN成功率,不要瞬时全量放开。

29.3 Bulkhead rejected激增

  1. 查Semaphore许可或ThreadPool active/queue。
  2. 查真实调用耗时是否增加,而不是只调大并发。
  3. 查HTTP连接池和数据库连接池是否更小,形成第二层瓶颈。
  4. 查队列最老任务年龄和剩余Deadline。
  5. 按依赖拆分Bulkhead,防止第三方接口挤占核心库存。
  6. 扩容前确认下游容量,否则只会把更多并发推给故障源。

29.4 Timeout激增但下游仍有成功日志

这是正常可能性:调用方停止等待后,远端仍完成。处理步骤:

  1. 用幂等键和业务请求号查下游事实状态。
  2. 查HTTP客户端是否真的取消连接或仅Future超时。
  3. 查SQL是否有Query Timeout。
  4. 不盲目重试写操作。
  5. 对UNKNOWN状态执行查询、对账或补偿。

29.5 Retry流量异常

  1. 对比逻辑请求数和物理Attempt数。
  2. 查Gateway、Mesh、Feign、业务和SDK是否多层重试。
  3. 按异常类型统计Retry原因。
  4. 检查退避、抖动、最大Attempt和总Deadline。
  5. 故障扩大时耗尽Retry Budget并快速失败。

29.6 Fallback成功率很高是否健康

不一定。HTTP 200和Fallback成功只说明降级代码返回了对象。必须监控degraded=true比例、关键字段UNKNOWN、用户影响和业务补偿积压。长期依赖Fallback等于功能长期不可用,不能被“接口成功率”掩盖。

三十、参数怎样从容量推导

30.1 Bulkhead并发

Little定律近似:

text
inFlight ≈ arrivalRate × averageLatency

目标库存查询200 QPS、平均100ms,平均在途约20。考虑P99、抖动和实例下线余量后可从20附近压测,而不是直接设200。最终上限还受HTTP连接池、下游线程和数据库连接约束。

30.2 CircuitBreaker窗口

窗口要同时满足:

  • 有足够样本降低随机波动。
  • 能在可接受时间内发现故障。
  • 不把过久历史拖入当前判断。

100 QPS接口用10次窗口只覆盖约100ms,容易抖动;0.1 QPS接口用100次最小调用可能十几分钟都不判断。应按QPS分层配置。

30.3 Timeout

Timeout应来自端到端SLO预算、下游正常P99和失败成本。配置在正常P99以下会误杀健康请求;远高于用户Deadline又会让上游放弃后仍占资源。

三十一、故障演练验收

至少演练:

  1. 下游持续500,确认达到最小样本后OPEN。
  2. 下游全部成功但延迟升高,确认慢调用率触发。
  3. OPEN等待期内确认不再访问下游。
  4. HALF_OPEN只放配置数量的探测。
  5. 下游恢复后确认逐步CLOSED,不出现恢复洪峰。
  6. Bulkhead容量满时确认快速拒绝且核心依赖不受影响。
  7. TimeLimiter超时后确认下游可能仍执行,并通过幂等查询收敛。
  8. Retry只对允许异常生效,物理Attempt不超过预算。
  9. Fallback返回明确降级状态,不写入权威业务事实。
  10. Trace、日志、Metrics和Actuator能解释每次状态变化。

三十二、反模式

反模式后果
一个全局breaker保护所有下游不同故障域相互污染
每个请求创建新breaker状态永远不积累且对象/指标爆炸
breaker名称包含业务ID高基数和内存泄漏风险
Fallback吞掉所有ThrowableBug、权限和数据错误被伪装
Timeout等于远端事务失败重复提交和数据不一致
Retry所有异常业务错误与过载被持续放大
Bulkhead许可大于下游连接池很多大量调用只是等待连接
只监控HTTP成功率降级和UNKNOWN被算作成功
所有实例同时动态改阈值集体抖动和全局开路
Java 17使用虚拟线程话术虚拟线程是Java 21正式能力

三十三、源码阅读路线

按Resilience4j 2.2.0与Spring Cloud CircuitBreaker 3.3.3:

  1. CircuitBreaker:装饰Supplier、Callable、CompletionStage的入口。
  2. CircuitBreakerStateMachine:AtomicReference状态与各State实现。
  3. CircuitBreakerMetrics:失败率、慢调用率与最小样本。
  4. FixedSizeSlidingWindowMetrics:次数窗口循环数组。
  5. SlidingTimeWindowMetrics:秒Bucket与Subtract-on-Evict。
  6. SemaphoreBulkhead:许可获取、释放和动态配置。
  7. FixedThreadPoolBulkhead:ThreadPoolExecutor、有界队列与ContextPropagator。
  8. RetryImpl:同步/异步Context、Attempt和Interval。
  9. AtomicRateLimiter:Cycle、State、CAS和许可预留。
  10. TimeLimiterImpl:Future.get和CompletionStage定时异常完成。
  11. CircuitBreakerAspectRetryAspectBulkheadAspect等:注解代理。
  12. ConfigurationProperties:默认Aspect Order。
  13. Boot 3模块AutoConfiguration.imports:自动配置入口。
  14. Resilience4JAutoConfiguration:Spring Cloud Factory、Metrics与Observation。
  15. Resilience4JCircuitBreakerFactory:id/group配置优先级和Executor。
  16. Resilience4JCircuitBreaker:TimeLimiter、Bulkhead、CircuitBreaker、Fallback组合顺序。

三十四、关联知识点

本章小结

Resilience4j的完整主线是:“稳定名称从Registry取得共享实例 → 外层适配或Aspect按确定顺序装饰函数 → 各模块获取许可 → 真实调用执行 → 结果按业务语义分类 → 滑动窗口预聚合 → CAS状态转换 → 事件和Metric发布 → Fallback返回明确降级或进入事实查询与补偿”。掌握这条链后,才能解释为什么熔断仍会雪崩、为什么超时后下游还成功、为什么扩容后本地限流总量变化,以及配置错误会怎样把保护器变成事故放大器。