一个真实的故事
上周五晚上11点,我们线上某个核心链路突然超时告警。排查发现,下游一个用户服务响应变慢(P99 从 50ms 飙到 8s),导致上游所有调用它的服务线程池被耗尽,最终整条链路雪崩。
最终根因:用户服务依赖的 Redis 实例内存打满了。
但真正的问题不是 Redis 挂了,而是系统没有任何熔断机制。
今天聊的就是如何用 Circuit Breaker 模式从根源上防止这种雪崩。
Circuit Breaker 到底是什么?
Circuit Breaker 的核心思想来自电路中的保险丝——当下游服务异常过多时,自动"断开"调用,快速失败,避免问题扩散。
它有三个状态:
1
2
3
4
5
6
7
8
9
10
11
12
13
|
超过阈值
┌──────────┐ ───────────► ┌──────────┐
│ CLOSED │ │ OPEN │
│ (正常放行) │ │ (拒绝请求) │
└──────────┘ └──────────┘
▲ │
│ 探测成功,恢复 │ 超过等待时间
└────────────────────────────┘
▼
┌──────────┐
│HALF-OPEN │
│(试探性放行) │
└──────────┘
|
- CLOSED:正常状态,所有请求正常通过,同时统计失败率
- OPEN:熔断状态,所有请求直接快速失败(不再调用下游)
- HALF-OPEN:探测状态,放少量请求试探下游是否恢复,成功则回到 CLOSED,失败则回到 OPEN
方案一:Resilience4j(推荐,轻量级)
Spring Boot 2 时代的 Hystrix 已经停止维护,Resilience4j 是目前 Java 生态最主流的选择。
依赖配置
1
2
3
4
5
|
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.1.0</version>
</dependency>
|
1
2
3
4
5
6
7
8
9
10
11
|
# application.yml
resilience4j:
circuitbreaker:
instances:
user-service:
slidingWindowSize: 10 # 统计窗口大小:最近10次调用
minimumNumberOfCalls: 5 # 至少调用5次才开始计算失败率
failureRateThreshold: 50 # 失败率超过50%触发熔断
waitDurationInOpenState: 30s # OPEN状态持续30秒后进入HALF-OPEN
permittedNumberOfCallsInHalfOpenState: 3 # HALF-OPEN状态允许3次探测调用
automaticTransitionFromOpenToHalfOpenEnabled: true
|
代码使用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
@Service
@Slf4j
public class UserServiceClient {
private final RestTemplate restTemplate;
public UserServiceClient(RestTemplate restTemplate) {
this.restTemplate = restTemplate;
}
@CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
public UserDTO getUser(Long userId) {
return restTemplate.getForObject(
"http://user-service/api/users/" + userId,
UserDTO.class
);
}
// 熔断时的降级逻辑
public UserDTO getUserFallback(Long userId, Throwable t) {
log.warn("User service 熔断降级, userId={}, reason={}",
userId, t.getMessage());
// 返回缓存数据或默认值
return UserDTO.builder()
.id(userId)
.name("服务暂不可用")
.fromCache(true)
.build();
}
}
|
组合使用:熔断 + 限流 + 重试
Resilience4j 的优势在于可以自由组合:
1
2
3
4
5
6
7
8
9
|
@CircuitBreaker(name = "user-service", fallbackMethod = "getUserFallback")
@RateLimiter(name = "user-service") // 额外加限流
@Retry(name = "user-service") // 额外加重试
public UserDTO getUserWithAllProtection(Long userId) {
return restTemplate.getForObject(
"http://user-service/api/users/" + userId,
UserDTO.class
);
}
|
方案二:阿里 Sentinel(适合复杂流控场景)
如果你已经在用 Spring Cloud Alibaba 生态,Sentinel 是更自然的选择。它除了熔断,还内置了流量控制、热点参数限流、系统自适应保护等功能。
核心配置
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// Sentinel 通过定义规则来控制
@PostConstruct
public void initDegradeRule() {
DegradeRule rule = new DegradeRule("user-service")
.setGrade(CircuitBreakerStrategy.SLOW_REQUEST_RATIO.getType())
.setCount(0.5) // 慢调用比例阈值
.setSlowRatioThreshold(1000) // 慢调用 RT 阈值(ms)
.setTimeWindow(30) // 熔断时长(秒)
.setMinRequestAmount(5) // 最小请求数
.setStatIntervalMs(10000); // 统计窗口(ms)
DegradeRuleManager.loadRules(List.of(rule));
}
|
Sentinel Dashboard
Sentinel 提供了一个可视化的 Dashboard,可以在运行时动态调整规则,不需要重启服务:
1
2
3
4
|
# 启动 Sentinel Dashboard
java -Dserver.port=8080 \
-Dcsp.sentinel.dashboard.server=localhost:8080 \
-jar sentinel-dashboard-1.8.8.jar
|
方案三:手写一个简易熔断器
理解原理比依赖框架更重要。下面是一个不到 80 行的完整实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
|
public class CircuitBreaker {
public enum State { CLOSED, OPEN, HALF_OPEN }
private final int failureThreshold;
private final long openDurationMs;
private final int halfOpenMaxCalls;
private volatile State state = State.CLOSED;
private AtomicInteger failureCount = new AtomicInteger(0);
private volatile long lastFailureTime = 0;
private AtomicInteger halfOpenCalls = new AtomicInteger(0);
public CircuitBreaker(int failureThreshold,
long openDurationMs,
int halfOpenMaxCalls) {
this.failureThreshold = failureThreshold;
this.openDurationMs = openDurationMs;
this.halfOpenMaxCalls = halfOpenMaxCalls;
}
public <T> T execute(Supplier<T> action, Supplier<T> fallback) {
switch (state) {
case OPEN:
if (System.currentTimeMillis() - lastFailureTime > openDurationMs) {
state = State.HALF_OPEN;
halfOpenCalls.set(0);
} else {
return fallback.get();
}
break;
case HALF_OPEN:
if (halfOpenCalls.get() >= halfOpenMaxCalls) {
return fallback.get();
}
break;
}
try {
T result = action.get();
onSuccess();
return result;
} catch (Exception e) {
onFailure();
return fallback.get();
}
}
private void onSuccess() {
if (state == State.HALF_OPEN) {
if (halfOpenCalls.incrementAndGet() >= halfOpenMaxCalls) {
state = State.CLOSED;
failureCount.set(0);
}
} else {
failureCount.set(0);
}
}
private void onFailure() {
lastFailureTime = System.currentTimeMillis();
if (state == State.HALF_OPEN) {
state = State.OPEN;
} else if (failureCount.incrementAndGet() >= failureThreshold) {
state = State.OPEN;
}
}
public State getState() { return state; }
}
|
使用示例:
1
2
3
4
5
6
|
CircuitBreaker cb = new CircuitBreaker(5, 30_000, 3);
String result = cb.execute(
() -> restTemplate.getForObject("/api/users/1", String.class),
() -> "{\"name\": \"fallback\"}"
);
|
生产环境的几个关键参数
| 参数 |
推荐值 |
说明 |
| slidingWindowSize |
10~20 |
太小波动大,太大反应慢 |
| failureRateThreshold |
40%~60% |
过低会误熔断,过高保护不及时 |
| waitDurationInOpenState |
15~60s |
取决于下游恢复速度 |
| minimumNumberOfCalls |
5~10 |
避免少量调用触发误判 |
最佳实践
- 每个下游服务独立一个 CircuitBreaker 实例,不要共用——否则一个服务挂了会连累所有服务被熔断
- Fallback 必须有实际意义——返回缓存数据、默认值或降级逻辑,不要直接返回 null
- 配合监控使用——接入 Prometheus + Grafana,实时观察熔断状态和失败率
- 渐进式上线——先在非核心链路灰度,确认参数合理后再推广到核心链路
小结
微服务架构下,没有熔断的调用链就是定时炸弹。Resilience4j 适合大多数 Java 项目的轻量级方案,Sentinel 适合需要复杂流控和动态规则的场景,而手写熔断器则能帮你真正理解原理。
回到开头的故事:我们在用户服务调用链路上加了 Resilience4j 熔断后,即使下游再出问题,上游最多影响 30 秒,之后自动降级到缓存数据,用户几乎无感知。
防御性编程不是悲观主义,而是对用户负责。