熔断与降级模式:熔断器模式原理、降级策略、熔断与降级的区别、Hystrix/Resilience4j实践
这一章我们来聊聊熔断与降级。这两个概念,很多同学容易搞混。我刚开始带团队的时候,也踩过这个坑——以为熔断就是降级,降级就是熔断。其实它们俩是两回事,但配合起来用,效果非常好。
熔断器模式原理
熔断器这个名字,是从电路里借来的。你想想看,家里电路短路了,保险丝会烧断,保护整个电路不被烧毁。微服务里的熔断器,干的是同样的事。
当一个服务调用另一个服务时,如果对方一直超时或报错,调用方不能无限等下去。否则,调用方自己的线程池会被占满,最终把自己也拖垮。这就是所谓的「雪崩效应」。
熔断器有三个状态:
- 关闭(Closed):正常状态,请求正常通过。但会统计失败次数。
- 打开(Open):失败率达到阈值,直接拒绝请求,快速返回失败。不再发起真实调用。
- 半开(Half-Open):过了一段时间,放少量请求过去试试。如果成功了,就关闭熔断器;如果还失败,继续保持打开。
核心要点:熔断器不是永久的。它会自动尝试恢复,这是它和手动降级最大的区别。
我在项目中遇到过这样一个场景:某个老系统依赖的第三方支付接口,每到月底就会卡顿。如果没有熔断器,整个订单服务都会被拖死。加了熔断之后,支付接口挂了,订单服务还能正常创建订单,只是支付状态暂时标记为「待处理」。
降级策略
降级,说白了就是「保底方案」。当服务不可用或熔断触发时,我们给用户一个备选响应,而不是直接抛异常。
常见的降级策略有这几种:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| 返回默认值 | 直接返回一个预设的默认结果 | 查询类接口,比如商品详情降级为缓存数据 |
| 返回兜底数据 | 从本地缓存或静态文件中读取数据 | 配置中心挂了,用本地配置文件 |
| 异步处理 | 把请求丢到消息队列里,等恢复后再处理 | 非实时性要求高的操作,比如发邮件 |
| 功能裁剪 | 关闭非核心功能,保证核心功能可用 | 大促期间,关闭评论、推荐等次要服务 |
我的习惯:降级策略一定要提前设计好,别等线上出问题了再临时想。我曾经吃过这个亏——半夜被叫起来,临时写降级代码,手忙脚乱还写了个bug。
熔断与降级的区别
这个问题面试经常问。我简单总结一下:
- 熔断是手段,降级是结果。熔断器打开了,触发降级逻辑。
- 熔断是自动的,基于指标(失败率、超时时间)自动触发。降级可以是自动的,也可以是手动的。
- 熔断保护的是调用方,防止自己被拖垮。降级保护的是用户体验,给用户一个「还能用」的感觉。
举个例子你就明白了:
你打电话给客服,打不通(熔断触发)。然后你听到语音提示「当前坐席繁忙,请稍后再拨」(降级响应)。熔断决定了「打不通」这个行为,降级决定了「给你什么反馈」。
注意:不要把所有降级都绑定在熔断上。有时候服务没熔断,但我们需要主动降级——比如大促时手动关闭非核心功能。这叫「主动降级」,和熔断无关。
Hystrix 实践
Hystrix 是 Netflix 开源的熔断降级框架。虽然现在官方已经进入维护模式,但很多老项目还在用。我简单说一下它的用法。
引入依赖:
<dependency>
<groupId>com.netflix.hystrix</groupId>
<artifactId>hystrix-core</artifactId>
<version>1.5.18</version>
</dependency>
写一个命令:
public class GetUserCommand extends HystrixCommand<String> {
private final Long userId;
public GetUserCommand(Long userId) {
super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("UserService"))
.andCommandPropertiesDefaults(
HystrixCommandProperties.Setter()
.withCircuitBreakerRequestVolumeThreshold(10) // 10秒内请求数
.withCircuitBreakerErrorThresholdPercentage(50) // 错误率50%触发熔断
.withCircuitBreakerSleepWindowInMilliseconds(5000) // 5秒后尝试半开
));
this.userId = userId;
}
@Override
protected String run() {
// 真实的远程调用
return userService.getUserById(userId);
}
@Override
protected String getFallback() {
// 降级逻辑
return "{\"id\": " + userId + ", \"name\": \"未知用户\"}";
}
}
调用方式:
String result = new GetUserCommand(1001L).execute();
避坑指南:Hystrix 的线程池隔离模式,每个命令组都有自己的线程池。我曾经遇到过一个坑——线程池配得太小,正常流量下没问题,一有突发流量就触发熔断。后来我把线程池大小和队列长度调大了一些,才稳定下来。
Resilience4j 实践
Resilience4j 是 Hystrix 的替代品,轻量级,设计更优雅。它不依赖 Netflix 的其它组件,适合 Spring Boot 2.x 和 Spring Cloud 的新项目。
引入依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot2</artifactId>
<version>1.7.1</version>
</dependency>
配置熔断器:
resilience4j.circuitbreaker:
instances:
userService:
registerHealthIndicator: true
slidingWindowSize: 10
minimumNumberOfCalls: 5
failureRateThreshold: 50
waitDurationInOpenState: 5s
permittedNumberOfCallsInHalfOpenState: 3
automaticTransitionFromOpenToHalfOpenEnabled: true
代码中使用:
@Service
public class UserService {
@CircuitBreaker(name = "userService", fallbackMethod = "getUserFallback")
public String getUser(Long userId) {
// 调用远程服务
return restTemplate.getForObject("http://user-service/users/" + userId, String.class);
}
public String getUserFallback(Long userId, Throwable t) {
// 降级逻辑
return "{\"id\": " + userId + ", \"name\": \"降级用户\"}";
}
}
Resilience4j 还支持重试、限流、舱壁隔离等模式。我个人比较喜欢它的链式调用风格:
// 组合使用熔断、重试、限流
Retry retry = Retry.ofDefaults("backendService");
CircuitBreaker circuitBreaker = CircuitBreaker.ofDefaults("backendService");
RateLimiter rateLimiter = RateLimiter.ofDefaults("backendService");
Supplier<String> decoratedSupplier = Decorators.ofSupplier(() -> backendService.doSomething())
.withCircuitBreaker(circuitBreaker)
.withRetry(retry)
.withRateLimiter(rateLimiter)
.decorate();
String result = Try.ofSupplier(decoratedSupplier).recover(throwable -> "fallback").get();
总结一下:熔断和降级是系统稳定性的两道防线。熔断负责「挡」,降级负责「兜」。Hystrix 虽然老了,但思想值得学习。新项目我建议直接用 Resilience4j,更轻、更灵活。
嗯,这一章就到这里。下一章我们聊聊「重试与退避模式」,到时候我会分享一个我踩过的重试风暴的坑,保证让你印象深刻。