Resilience4j熔断器:状态机转换与滑动窗口的实现细节
Resilience4j熔断器:状态机转换与滑动窗口的实现细节
适读人群:使用Resilience4j做服务容错、想理解熔断器底层原理的Java开发者 | 阅读时长:约18分钟
开篇故事
一次线上故障,支付服务因为数据库连接耗尽,响应时间从 100ms 暴增到 30 秒。没有熔断器的情况下,所有调用支付服务的请求都在等待,线程全部阻塞,最终导致整个订单系统雪崩。
恢复支付服务之后,我们在所有对外部依赖的调用上都加了 Resilience4j 的 CircuitBreaker。再次出现类似问题时,熔断器会在几秒内自动切断请求,让调用方立刻走 fallback 逻辑,不再等待。
这是我实际踩过的雪崩效应。今天把 Resilience4j 熔断器的状态机、滑动窗口的实现细节全部讲清楚。
一、熔断器状态机
1.1 三种状态
1.2 状态转换条件
| 转换 | 条件 |
|---|---|
| CLOSED → OPEN | 最近N次调用中,失败率 >= failureRateThreshold(默认50%)或慢调用率 >= slowCallRateThreshold(默认100%) |
| OPEN → HALF_OPEN | 等待 waitDurationInOpenState(默认60秒)后,自动或手动切换 |
| HALF_OPEN → CLOSED | 探测请求(permittedNumberOfCallsInHalfOpenState)中,失败率低于阈值 |
| HALF_OPEN → OPEN | 探测请求中,失败率高于阈值 |
二、滑动窗口:两种类型
Resilience4j 支持两种滑动窗口:
| 类型 | 说明 | 适用场景 |
|---|---|---|
COUNT_BASED | 基于请求次数,记录最近N次调用 | 请求量稳定,低延迟 |
TIME_BASED | 基于时间,统计最近N秒内的调用 | 请求量波动大,关注时间维度 |
COUNT_BASED 原理(环形数组):
三、完整代码实现
3.1 基础依赖
<dependencies>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- 监控端点 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
</dependencies>3.2 配置文件
resilience4j:
circuitbreaker:
configs:
default: # 默认配置,所有熔断器继承
sliding-window-type: COUNT_BASED
sliding-window-size: 20 # 统计最近20次调用
minimum-number-of-calls: 10 # 至少10次调用才开始计算
failure-rate-threshold: 50 # 失败率超过50%则熔断
slow-call-rate-threshold: 80 # 慢调用占比超过80%则熔断
slow-call-duration-threshold: 3s # 超过3秒算慢调用
wait-duration-in-open-state: 30s # OPEN状态持续30秒
permitted-number-of-calls-in-half-open-state: 5 # HALF_OPEN允许5次探测
automatic-transition-from-open-to-half-open-enabled: true # 自动转换
instances:
paymentService:
base-config: default
failure-rate-threshold: 30 # 支付服务更敏感,30%就熔断
wait-duration-in-open-state: 60s
inventoryService:
base-config: default
sliding-window-type: TIME_BASED
sliding-window-size: 10 # 最近10秒内的调用
retry:
configs:
default:
max-attempts: 3
wait-duration: 500ms
retry-exceptions:
- java.net.ConnectException
- java.net.SocketTimeoutException
timelimiter:
configs:
default:
timeout-duration: 5s # 超过5秒强制超时
management:
endpoints:
web:
exposure:
include: health, circuitbreakers, circuitbreakerevents3.3 服务层使用 CircuitBreaker
package com.laozhang.resilience.service;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import io.github.resilience4j.retry.annotation.Retry;
import io.github.resilience4j.timelimiter.annotation.TimeLimiter;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Service
@RequiredArgsConstructor
public class PaymentService {
private final PaymentServiceClient paymentClient;
/**
* 创建支付订单
* 熔断器 + 重试 + 超时 三重保护
*
* 注意顺序:TimeLimiter → CircuitBreaker → Retry
* 超时先判断,熔断器统计,最后重试
*/
@CircuitBreaker(name = "paymentService", fallbackMethod = "createPaymentFallback")
@Retry(name = "paymentService")
@TimeLimiter(name = "paymentService")
public CompletableFuture<PaymentResult> createPayment(PaymentRequest request) {
// 异步调用(TimeLimiter 要求返回 CompletableFuture 或 CompletionStage)
return CompletableFuture.supplyAsync(() -> {
log.info("[Payment] 发起支付请求 orderId={}", request.getOrderId());
return paymentClient.createPayment(request);
});
}
/**
* 熔断降级方法
* 参数列表:原方法参数 + Throwable(原因)
* 方法名:createPaymentFallback(固定格式:原方法名 + Fallback)
*/
public CompletableFuture<PaymentResult> createPaymentFallback(
PaymentRequest request, Throwable throwable) {
log.error("[Payment] 熔断降级,orderId={} 原因={}",
request.getOrderId(), throwable.getMessage());
// 根据业务选择:返回默认值、抛出友好异常、或者写入延迟队列
return CompletableFuture.completedFuture(
PaymentResult.pendingResult(request.getOrderId())
);
}
}3.4 手动使用 CircuitBreaker(编程式)
package com.laozhang.resilience.service;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerConfig;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.Duration;
import java.util.function.Supplier;
@Slf4j
@Service
public class ManualCircuitBreakerDemo {
/**
* 手动创建和使用 CircuitBreaker(不依赖 Spring 注解)
*/
public void demo() {
// 创建配置
CircuitBreakerConfig config = CircuitBreakerConfig.custom()
.slidingWindowType(CircuitBreakerConfig.SlidingWindowType.COUNT_BASED)
.slidingWindowSize(10)
.failureRateThreshold(50f)
.waitDurationInOpenState(Duration.ofSeconds(30))
.permittedNumberOfCallsInHalfOpenState(3)
.automaticTransitionFromOpenToHalfOpenEnabled(true)
// 自定义:哪些异常算失败
.recordExceptions(RuntimeException.class, Exception.class)
// 自定义:哪些异常不算失败(忽略)
.ignoreExceptions(BusinessException.class)
.build();
// 创建 CircuitBreaker 实例
CircuitBreaker cb = CircuitBreakerRegistry.of(config)
.circuitBreaker("myService");
// 监听状态变化
cb.getEventPublisher()
.onStateTransition(event ->
log.info("[CB] 状态变化 {} → {}",
event.getStateTransition().getFromState(),
event.getStateTransition().getToState())
)
.onFailureRateExceeded(event ->
log.warn("[CB] 失败率超限 {}%", event.getFailureRate())
);
// 包装调用
Supplier<String> decoratedSupplier = CircuitBreaker.decorateSupplier(cb, () -> {
// 实际的远程调用
return "result";
});
// 执行
try {
String result = decoratedSupplier.get();
} catch (Exception e) {
// 处理熔断或业务异常
log.error("调用失败", e);
}
}
}3.5 监听熔断器状态(生产监控)
package com.laozhang.resilience.monitor;
import io.github.resilience4j.circuitbreaker.CircuitBreaker;
import io.github.resilience4j.circuitbreaker.CircuitBreakerRegistry;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
/**
* 熔断器状态监控
* 在应用启动后为所有熔断器注册事件监听
*/
@Slf4j
@Component
@RequiredArgsConstructor
public class CircuitBreakerMonitor {
private final CircuitBreakerRegistry circuitBreakerRegistry;
@EventListener(ApplicationReadyEvent.class)
public void registerEventListeners() {
circuitBreakerRegistry.getAllCircuitBreakers().forEach(this::registerListeners);
}
private void registerListeners(CircuitBreaker cb) {
String name = cb.getName();
cb.getEventPublisher()
.onStateTransition(event -> {
String from = event.getStateTransition().getFromState().name();
String to = event.getStateTransition().getToState().name();
log.warn("[CircuitBreaker] {} 状态切换 {} → {}", name, from, to);
// 如果需要,可以在这里发送告警
if ("OPEN".equals(to)) {
sendAlert(name, "熔断器开启,服务降级!");
}
})
.onFailureRateExceeded(event ->
log.warn("[CircuitBreaker] {} 失败率超限 {:.1f}%",
name, event.getFailureRate())
)
.onSlowCallRateExceeded(event ->
log.warn("[CircuitBreaker] {} 慢调用率超限 {:.1f}%",
name, event.getSlowCallRate())
);
}
private void sendAlert(String serviceName, String message) {
// 实际项目里接入告警系统(钉钉、飞书等)
log.error("[ALERT] 服务 {} - {}", serviceName, message);
}
}四、踩坑实录
坑1:最小调用次数设置太低,刚启动就熔断
症状:服务启动后,第一次调用失败,立刻熔断,后续请求全部被拒绝。
根因:minimumNumberOfCalls=1,1次失败就达到了统计要求,失败率 100% 直接触发熔断。
解决:设置合理的 minimumNumberOfCalls(通常设为滑动窗口大小的一半):
minimum-number-of-calls: 10 # 至少10次才开始统计失败率坑2:fallback 方法签名不对
症状:熔断触发时,报错 No fallback method found for method: xxx。
根因:fallback 方法必须和原方法在同一个类里,且参数列表必须是原方法的所有参数 + 一个 Throwable(或其子类)。
// 原方法:public PaymentResult createPayment(PaymentRequest request)
// ❌ 错误:参数列表不对
public PaymentResult fallback(Throwable t) { ... }
// ✅ 正确:原方法参数 + Throwable
public PaymentResult createPaymentFallback(PaymentRequest request, Throwable t) { ... }坑3:@CircuitBreaker 和 @Transactional 同时使用时的顺序问题
症状:熔断降级时,事务没有回滚,产生了脏数据。
根因:当 @CircuitBreaker 捕获了异常执行 fallback,事务感知不到异常,就没有回滚。
解决:在 fallback 方法里手动标记回滚,或者把事务和熔断分层处理:
// 在 fallback 里手动标记事务回滚
public PaymentResult createPaymentFallback(PaymentRequest request, Throwable t) {
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
return PaymentResult.failed();
}坑4:TIME_BASED 窗口内请求数很少,统计不准
场景:时间窗口 10 秒,但流量很低,10 秒内只有 2 次调用,1 次失败 = 50% 失败率,触发熔断。
解决:配合 minimumNumberOfCalls 使用:
sliding-window-type: TIME_BASED
sliding-window-size: 10 # 统计最近10秒
minimum-number-of-calls: 20 # 至少20次调用才计算低流量服务可以考虑用 COUNT_BASED 而不是 TIME_BASED。
五、总结与延伸
Resilience4j 熔断器的设计精髓:
- 状态机驱动:CLOSED/OPEN/HALF_OPEN 三态转换,每个状态有清晰的进入条件和行为
- 滑动窗口统计:基于环形数组,高效统计最近N次(或N秒)的成功率和慢调用率
- 探测恢复:HALF_OPEN 状态用少量探测请求验证服务是否恢复,避免反复震荡
使用建议:
- 对所有外部依赖(HTTP调用、数据库、消息队列)都加 CircuitBreaker
- 熔断器 + 重试 + 超时三件套组合使用
- 生产环境必须配监控告警,熔断开启时立刻通知
