Resilience4j内部原理与生产治理
Resilience4j保护的是“未来是否继续进入某个故障域,以及最多允许多少工作占用资源”,不是把远程调用变成本地可靠调用。真正理解它,需要从函数装饰、许可获取、结果记录、滑动窗口聚合、状态并发转换、异步取消边界、AOP顺序、自动配置、指标和业务事实一路推演。
本章源码结论以以下可复核组合为样本:
| 层次 | 版本 | 说明 |
|---|---|---|
| Java | 17目标字节码 | Boot 3现代线,major version 61 |
| Spring Boot | 3.5.16 | Maven Central截至2026-07-19的Boot 3维护线样本 |
| Spring Cloud | 2025.0.3 | 与Boot 3.5线配套的发布列车样本 |
| Spring Cloud CircuitBreaker | 3.3.3 | Cloud BOM实际解析版本 |
| Resilience4j | 2.2.0 | Cloud BOM实际解析版本,不是独立最新2.4.0 |
| JDK 8边界 | Resilience4j 1.7.1 | major version 52,仅用于存量维护 |
版本变化可能调整默认值、属性和自动配置条件。本文凡是提到默认顺序、默认线程池或默认参数,均限定在上述源码样本;升级时应重新查看目标BOM、配置元数据、依赖树和源码。
一、学习目标
学完后应能:
- 从
decorateSupplier推演一次许可、调用、计时和结果记录全过程。 - 解释
CircuitBreakerStateMachine为何使用AtomicReference<State>和各状态自己的原子变量。 - 解释Count-based与Time-based滑动窗口的Subtract-on-Evict实现。
- 说明
minimumNumberOfCalls为什么比单独的失败率阈值更重要。 - 区分失败、慢成功、慢失败、忽略异常和拒绝调用的统计语义。
- 解释OPEN怎样惰性或定时进入HALF_OPEN,以及探测许可如何并发扣减。
- 说明SemaphoreBulkhead和FixedThreadPoolBulkhead的真实资源边界。
- 解释AtomicRateLimiter的Cycle、负许可、CAS和等待语义。
- 解释同步Retry为何阻塞等待,异步Retry怎样调度下一次Attempt。
- 解释TimeLimiter为什么只能限制Future等待,不能保证撤销远端副作用。
- 推演Spring Cloud CircuitBreaker Factory创建、配置查找、线程池、TimeLimiter、Bulkhead、CircuitBreaker和Fallback顺序。
- 推演原生注解的AOP顺序、自调用失效、Fallback签名与异常选择。
- 设计订单、库存、支付和接口聚合的超时、重试、隔离、熔断和降级。
- 用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和观测包装方式并不相同。
flowchart TD
A["业务方法或Supplier"] --> B["Spring AOP或Cloud Factory适配"]
B --> C["Resilience4j Registry查找实例"]
C --> D["原生模块装饰函数"]
D --> E["许可、执行、统计与事件"]
E --> F["Fallback或结果返回"]三、原生CircuitBreaker一次调用怎样走
核心伪代码可以理解为:
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是否允许,执行后按结果类型记录。熔断器不是在请求到来前“远程探测下游健康”,而是根据本应用实例过去的调用结果做本地决策。
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原子替换:
- 读取当前状态。
- 校验该转换是否合法。
- 调用旧状态的
preTransitionHook取消相关定时任务。 - 创建新状态对象。
- 原子替换引用。
- 发布外部状态转换事件。
每个状态内部还有自己的原子闸门:
| 状态 | 原子控制 | 目的 |
|---|---|---|
| CLOSED | AtomicBoolean isClosed | 多线程同时发现超阈值时只允许一个线程开路 |
| OPEN | AtomicBoolean isOpen | 等待到期时只允许一个线程转HALF_OPEN |
| HALF_OPEN | AtomicInteger permittedNumberOfCalls | 原子发放有限探测许可 |
| HALF_OPEN | AtomicBoolean isHalfOpen | 多个探测同时完成时只转换一次 |
所以状态转换不是简单的if (state == CLOSED) state = OPEN。没有CAS闸门时,多个并发完成事件可能重复建状态、重复调度任务、覆盖Metrics或发布多次转换事件。
五、CLOSED怎样判断开路
CLOSED总是允许新调用,调用完成后进入CircuitBreakerMetrics:
- 按耗时是否超过
slowCallDurationThreshold分类为普通或慢调用。 - 按调用结果分类为成功或失败。
- 记录到滑动窗口。
- 取得预聚合Snapshot。
- 若样本不足,返回
BELOW_MINIMUM_CALLS_THRESHOLD。 - 否则分别计算Failure Rate与Slow Call Rate。
- 任一达到阈值,CAS把
isClosed从true改为false。 - 成功CAS的线程发布阈值事件并转换到OPEN。
5.1 失败率公式
failureRate = failedCalls / totalCalls × 100%5.2 慢调用率公式
slowCallRate = slowCalls / totalCalls × 100%只有totalCalls >= minimumNumberOfCalls才计算有效比例,否则返回-1表达样本不足。以窗口20、最小调用10、失败阈值50%为例:前9次即使全部失败也不会按该规则自动开路;第10次完成后才有资格判断。
六、异常分类决定熔断器看见什么
不是所有异常都代表“下游不健康”:
| 结果 | 推荐语义 |
|---|---|
| 连接失败、读取超时、HTTP 5xx | 通常计入系统失败 |
| HTTP 429 | 表示过载,是否计失败及重试要结合Retry-After |
| 参数校验失败、权限拒绝 | 调用方或业务错误,通常不代表下游故障 |
| 库存不足 | 正常业务结果,不应当作系统异常率 |
| 订单不存在 | 可能是正常404,也可能是数据同步故障,需按契约分类 |
| Fallback抛错 | 是降级失败,应独立观测 |
Resilience4j提供:
recordExceptions或recordExceptionPredicate决定哪些异常算失败。ignoreExceptions或ignoreExceptionPredicate决定哪些异常不进入成功/失败统计。recordResultPredicate可以把某些正常返回对象视为失败。
先执行Ignore判断,再按Record规则分类。若把所有业务异常都计失败,熔断器会在业务高峰把“库存不足”误认为服务故障;若忽略范围过宽,又会掩盖真正的连接和数据问题。
七、次数滑动窗口源码原理
FixedSizeSlidingWindowMetrics维护:
- 长度为N的
Measurement[]循环数组。 - 当前
headIndex。 - 一个
TotalAggregation总聚合。
记录一次调用时方法使用synchronized保证数组和总量一致:
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:
- 当前秒内所有调用累加到同一Bucket。
- 时间跨秒时,计算当前秒与最新Bucket秒数的差。
- 最多前移N格,避免长时间空闲后做无限循环。
- 每前移一格,从总聚合减去被淘汰Bucket。
- 重置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源码原理
构造时创建:
new Semaphore(maxConcurrentCalls, fairCallHandlingEnabled)tryAcquirePermission按maxWaitDuration尝试获取许可,失败发布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后:
- 使用配置的ContextPropagator装饰任务。
CompletableFuture.supplyAsync提交到专属Executor。- 线程或队列有容量则发布PERMITTED。
- 执行完成发布FINISHED并完成外层Promise。
RejectedExecutionException转换为BulkheadFullException并发布REJECTED。
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路径
源码调用:
future.get(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS)超时时创建带组件名的TimeoutException,发布TIMEOUT事件;若cancelRunningFuture=true则执行future.cancel(true)。这通常向执行线程发送中断,但不保证:
- 阻塞Socket立即关闭。
- JDBC驱动取消服务器SQL。
- 远端HTTP请求停止。
- 已提交数据库事务回滚。
- 第三方接口撤销副作用。
14.2 CompletionStage路径
通过ScheduledExecutorService安排一个超时任务;到期且Future未完成时,把CompletableFuture异常完成。原始异步操作仍可能继续执行。因此超时处理必须区分:
调用方不再等待 ≠ 下游没有执行 ≠ 业务事务已失败写操作需要幂等键、状态查询、Outbox或补偿。HTTP客户端仍必须有真实网络超时,数据库仍必须有Query Timeout。
十五、Retry内部执行与流量放大
同步Retry为每个逻辑调用创建Context,维护Attempt计数和最后异常。每次失败后:
- 判断异常或结果是否满足Retry Predicate。
- 判断当前Attempt是否达到
maxAttempts。 - 通过IntervalFunction或IntervalBiFunction计算等待。
- 发布RETRY事件。
- 同步路径使用
Thread.sleep等待。 - 再执行下一次物理调用。
- 最终发布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。
获取许可时:
- 用单调纳秒时间计算当前Cycle。
- 若跨越多个Cycle,按周期累加许可,但上限不超过
limitForPeriod。 - 若许可不足,计算需要等待多少完整Cycle。
- 若等待不超过
timeoutDuration,先预留许可,activePermissions可以变成负数。 - 通过AtomicReference CAS替换整个State。
- CAS竞争失败时短暂backoff后重试。
- 根据
nanosToWait立即通过、park等待或拒绝。
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-query、payment-status,而不是具体业务ID。
Registry还负责:
- 默认配置与命名配置。
- 实例创建与移除事件。
- Tags。
- Metrics Publisher绑定。
- 按名称查找当前状态和指标。
十八、Resilience4j Boot 3自动配置启动链
resilience4j-spring-boot3通过AutoConfiguration.imports声明各模块自动配置:CircuitBreaker、Retry、Bulkhead、ThreadPoolBulkhead、RateLimiter、TimeLimiter、Metrics、Health Indicator、事件端点和调度器。
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创建实例的配置优先级
调用:
factory.create("inventory-query", "inventory-service")样本源码按以下方向查找CircuitBreaker配置:
- id对应配置:
inventory-query。 - group对应配置:
inventory-service。 - Factory默认配置。
TimeLimiter也先查id,再查group,最后取Registry默认配置。配置属性通常先构建Registry中的命名配置,因此属性配置可能覆盖仅通过Factory Customizer设置的默认值。不要同时在YAML、Java Customizer、动态配置中心和代码临时修改四处定义同名阈值。
二十一、Spring Cloud run()真实调用顺序
在样本默认设置下,Factory有ExecutorService,TimeLimiter启用;若类路径存在Bulkhead模块,默认还会创建BulkheadProvider。阻塞调用主链为:
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:
| Aspect | Order |
|---|---|
| Retry | LOWEST_PRECEDENCE - 5 |
| CircuitBreaker | LOWEST_PRECEDENCE - 4 |
| RateLimiter | LOWEST_PRECEDENCE - 3 |
| TimeLimiter | LOWEST_PRECEDENCE - 2 |
| Bulkhead | LOWEST_PRECEDENCE - 1 |
Spring AOP中数值更小的Advice通常更外层,因此默认可理解为:Retry包住CircuitBreaker,CircuitBreaker包住RateLimiter,随后TimeLimiter,Bulkhead靠近目标方法。实际异步返回类型和模块Aspect实现仍会影响执行,应通过测试统计每次逻辑请求产生多少Attempt、每个模块记录多少调用。
22.1 为什么顺序改变统计
Retry(CircuitBreaker(call))每个Retry Attempt可能分别经过CircuitBreaker并成为独立样本。
CircuitBreaker(Retry(call))CircuitBreaker可能只看到整组Retry最终成功或失败。前者更快反映物理下游失败,但也可能因重试快速开路;后者统计逻辑请求结果,却隐藏物理放大。没有全局唯一正确顺序,必须先定义想统计什么。
22.2 自调用失效
同一个Bean内this.protectedMethod()没有经过Spring代理,注解Aspect通常不执行。解决方向是拆分Bean、从代理入口调用或使用显式Decorator;不要通过注入自己等晦涩方式掩盖设计问题。
二十三、Fallback方法怎样匹配
注解Fallback通常要求:
- 参数与原方法一致,并可在末尾增加Throwable或其子类。
- 返回类型兼容。
- 异常类型越具体,匹配越优先。
- Fallback本身抛出的异常要继续向上暴露并独立观测。
错误做法:
public InventoryView fallback(String skuId, Throwable error) {
return new InventoryView(skuId, "AVAILABLE", false);
}这会把未知库存伪造成有货。正确做法是返回UNKNOWN与degraded=true,或让核心写操作失败并进入事实查询/补偿。
二十四、生产配置示例与逐项解释
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关键部分
<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 明确业务状态
package com.example.order;
public enum InventoryStatus {
AVAILABLE,
OUT_OF_STOCK,
UNKNOWN
}package com.example.order;
public record InventoryView(
String skuId,
InventoryStatus status,
boolean degraded,
String reason) {
}Record属于Java 17现代线,JDK 8项目应改成普通POJO。
25.3 模拟真实下游
package com.example.order;
public class InventoryConnectionException extends RuntimeException {
public InventoryConnectionException(String message) {
super(message);
}
}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门面调用
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 控制器
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 启动与验证
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。还要检查degraded、reason、真实Attempt、CircuitBreaker状态、not permitted、timeout、bulkhead rejected和下游日志。
25.7 原生Decorator显式组合Retry与CircuitBreaker
若业务确实需要Retry,应让顺序在代码中可见:
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、事件与可观测性
应至少观测:
| 模块 | 关键维度 |
|---|---|
| CircuitBreaker | state、successful、failed、slow、not_permitted |
| Retry | successful_without_retry、successful_with_retry、failed_with_retry、attempts |
| Bulkhead | available_concurrent_calls、max_allowed、rejected、queue_depth |
| ThreadPoolBulkhead | active、queue、core、max、rejected |
| RateLimiter | available_permissions、waiting_threads、failed_acquire |
| TimeLimiter | successful、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
- 先按实例和breaker name查看state变化时间。
- 查窗口内buffered、failed、slow与minimum calls。
- 核对哪些异常和结果被计为失败。
- 用Trace区分连接失败、HTTP 5xx、超时、Bulkhead拒绝和业务异常。
- 查下游P95/P99、线程、连接、数据库锁、GC和发布变更。
- 查Retry是否放大物理Attempt。
- 不要第一反应强制CLOSED;若下游仍故障会重新施压。
29.2 not permitted激增
- 区分OPEN拒绝、FORCED_OPEN还是运维配置。
- 检查OPEN等待时间和HALF_OPEN探测是否到达。
- 检查请求是否只落在部分本地OPEN实例。
- 确认Fallback容量和业务语义。
- 下游恢复后观察HALF_OPEN成功率,不要瞬时全量放开。
29.3 Bulkhead rejected激增
- 查Semaphore许可或ThreadPool active/queue。
- 查真实调用耗时是否增加,而不是只调大并发。
- 查HTTP连接池和数据库连接池是否更小,形成第二层瓶颈。
- 查队列最老任务年龄和剩余Deadline。
- 按依赖拆分Bulkhead,防止第三方接口挤占核心库存。
- 扩容前确认下游容量,否则只会把更多并发推给故障源。
29.4 Timeout激增但下游仍有成功日志
这是正常可能性:调用方停止等待后,远端仍完成。处理步骤:
- 用幂等键和业务请求号查下游事实状态。
- 查HTTP客户端是否真的取消连接或仅Future超时。
- 查SQL是否有Query Timeout。
- 不盲目重试写操作。
- 对UNKNOWN状态执行查询、对账或补偿。
29.5 Retry流量异常
- 对比逻辑请求数和物理Attempt数。
- 查Gateway、Mesh、Feign、业务和SDK是否多层重试。
- 按异常类型统计Retry原因。
- 检查退避、抖动、最大Attempt和总Deadline。
- 故障扩大时耗尽Retry Budget并快速失败。
29.6 Fallback成功率很高是否健康
不一定。HTTP 200和Fallback成功只说明降级代码返回了对象。必须监控degraded=true比例、关键字段UNKNOWN、用户影响和业务补偿积压。长期依赖Fallback等于功能长期不可用,不能被“接口成功率”掩盖。
三十、参数怎样从容量推导
30.1 Bulkhead并发
Little定律近似:
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又会让上游放弃后仍占资源。
三十一、故障演练验收
至少演练:
- 下游持续500,确认达到最小样本后OPEN。
- 下游全部成功但延迟升高,确认慢调用率触发。
- OPEN等待期内确认不再访问下游。
- HALF_OPEN只放配置数量的探测。
- 下游恢复后确认逐步CLOSED,不出现恢复洪峰。
- Bulkhead容量满时确认快速拒绝且核心依赖不受影响。
- TimeLimiter超时后确认下游可能仍执行,并通过幂等查询收敛。
- Retry只对允许异常生效,物理Attempt不超过预算。
- Fallback返回明确降级状态,不写入权威业务事实。
- Trace、日志、Metrics和Actuator能解释每次状态变化。
三十二、反模式
| 反模式 | 后果 |
|---|---|
| 一个全局breaker保护所有下游 | 不同故障域相互污染 |
| 每个请求创建新breaker | 状态永远不积累且对象/指标爆炸 |
| breaker名称包含业务ID | 高基数和内存泄漏风险 |
| Fallback吞掉所有Throwable | Bug、权限和数据错误被伪装 |
| Timeout等于远端事务失败 | 重复提交和数据不一致 |
| Retry所有异常 | 业务错误与过载被持续放大 |
| Bulkhead许可大于下游连接池很多 | 大量调用只是等待连接 |
| 只监控HTTP成功率 | 降级和UNKNOWN被算作成功 |
| 所有实例同时动态改阈值 | 集体抖动和全局开路 |
| Java 17使用虚拟线程话术 | 虚拟线程是Java 21正式能力 |
三十三、源码阅读路线
按Resilience4j 2.2.0与Spring Cloud CircuitBreaker 3.3.3:
CircuitBreaker:装饰Supplier、Callable、CompletionStage的入口。CircuitBreakerStateMachine:AtomicReference状态与各State实现。CircuitBreakerMetrics:失败率、慢调用率与最小样本。FixedSizeSlidingWindowMetrics:次数窗口循环数组。SlidingTimeWindowMetrics:秒Bucket与Subtract-on-Evict。SemaphoreBulkhead:许可获取、释放和动态配置。FixedThreadPoolBulkhead:ThreadPoolExecutor、有界队列与ContextPropagator。RetryImpl:同步/异步Context、Attempt和Interval。AtomicRateLimiter:Cycle、State、CAS和许可预留。TimeLimiterImpl:Future.get和CompletionStage定时异常完成。CircuitBreakerAspect、RetryAspect、BulkheadAspect等:注解代理。- 各
ConfigurationProperties:默认Aspect Order。 - Boot 3模块
AutoConfiguration.imports:自动配置入口。 Resilience4JAutoConfiguration:Spring Cloud Factory、Metrics与Observation。Resilience4JCircuitBreakerFactory:id/group配置优先级和Executor。Resilience4JCircuitBreaker:TimeLimiter、Bulkhead、CircuitBreaker、Fallback组合顺序。
三十四、关联知识点
- Resilience4j入门
- Resilience4j独立面试题
- Hystrix内部原理与Resilience4j迁移
- Sentinel内部原理与生产治理
- 微服务稳定性:Deadline、Retry Budget与过载保护
- OpenFeign内部调用链
- LoadBalancer实例选择与重试边界
- Boot 3可观测性内部原理
- 幂等与结果未知
- 分布式事务与补偿
本章小结
Resilience4j的完整主线是:“稳定名称从Registry取得共享实例 → 外层适配或Aspect按确定顺序装饰函数 → 各模块获取许可 → 真实调用执行 → 结果按业务语义分类 → 滑动窗口预聚合 → CAS状态转换 → 事件和Metric发布 → Fallback返回明确降级或进入事实查询与补偿”。掌握这条链后,才能解释为什么熔断仍会雪崩、为什么超时后下游还成功、为什么扩容后本地限流总量变化,以及配置错误会怎样把保护器变成事故放大器。
