之前有一次线上事故,一个接口被第三方脚本疯狂调用,QPS 从平时的 200 直接飙到 8000,数据库连接池被打满,整个服务雪崩。从那以后我就下定决心,核心接口必须上限流和熔断,不能等出了事再补。
今天把我在 Spring Boot 项目里实际用的限流+熔断方案写下来,踩过的几个坑也一并说清楚。
先分清楚限流和熔断是两回事
很多人把限流和熔断混为一谈,其实它们解决的是不同层面的问题。
限流是入口端的事:控制进入系统的请求速率,超了就直接拒绝或者排队。目的是保护系统不被打满。
熔断是出口端的事:当下游服务(数据库、第三方 API、微服务)响应变慢或者报错率飙升时,主动切断调用,快速失败返回,避免一个下游拖垮整个链路。
实际项目里两个都要上,缺一个都不行。
限流:我用的是 Bucket4j + Redis
Java 里限流方案不少,Guava RateLimiter、Sentinel、Bucket4j 都可以用。我的场景是多实例部署,单机限流没意义,所以选了 Bucket4j + Redis 做分布式限流。
Maven 依赖:
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-core</artifactId>
<version>8.10.1</version>
</dependency>
<dependency>
<groupId>com.bucket4j</groupId>
<artifactId>bucket4j-redis</artifactId>
<version>8.10.1</version>
</dependency>
配置限流桶:
@Configuration
public class RateLimitConfig {
@Bean
public ProxyManager<String> proxyManager(RedisConnectionFactory connectionFactory) {
RedisBasedProxyManager.Builder<String> builder = RedisBasedProxyManager.builderFor(connectionFactory);
return builder.build();
}
@Bean
public BucketConfiguration bucketConfiguration() {
return BucketConfiguration.builder()
.addLimit(Bandwidth.classic(100, Refill.greedy(100, Duration.ofMinutes(1))))
.build();
}
}
这段配置的意思是:每个 key 每分钟最多 100 次请求,超过就拒绝。
在拦截器里使用:
@Component
public class RateLimitInterceptor implements HandlerInterceptor {
@Autowired
private ProxyManager<String> proxyManager;
@Autowired
private BucketConfiguration bucketConfiguration;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String key = resolveKey(request); // 按 IP 或用户 ID 做 key
BucketProxy bucket = proxyManager.builder().build(key, bucketConfiguration);
if (bucket.tryConsume(1)) {
return true;
}
response.setStatus(429);
response.setContentType("application/json;charset=UTF-8");
response.getWriter().write("{\"code\":429,\"msg\":\"请求太频繁,请稍后再试\"}");
return false;
}
private String resolveKey(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip.split(",")[0].trim();
}
}
熔断:我用的是 Resilience4j
Spring Cloud 早期用 Hystrix,但 Netflix 已经不维护了。现在主流是 Resilience4j,轻量、函数式风格、和 Spring Boot 集成得很好。
Maven 依赖:
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-spring-boot3</artifactId>
<version>2.2.0</version>
</dependency>
<dependency>
<groupId>io.github.resilience4j</groupId>
<artifactId>resilience4j-circuitbreaker</artifactId>
<version>2.2.0</version>
</dependency>
application.yml 配置:
resilience4j:
circuitbreaker:
instances:
paymentApi:
register-health-indicator: true
sliding-window-size: 10
minimum-number-of-calls: 5
failure-rate-threshold: 50
wait-duration-in-open-state: 30s
permitted-number-of-calls-in-half-open-state: 3
automatic-transition-from-open-to-half-open-enabled: true
这段配置的含义:
- 滑动窗口 10 次调用
- 至少 5 次调用才开始计算失败率
- 失败率超过 50% 就熔断
- 熔断后等 30 秒进入半开状态
- 半开状态放 3 个请求试探
- 试探通过就恢复,否则继续熔断
在代码里使用:
@Service
public class PaymentService {
@CircuitBreaker(name = "paymentApi", fallbackMethod = "paymentFallback")
public String callPaymentApi(String orderId) {
// 调用下游支付接口
return restTemplate.getForObject("https://payment.example.com/status/" + orderId, String.class);
}
public String paymentFallback(String orderId, Throwable t) {
log.warn("支付接口熔断,orderId: {}, 原因: {}", orderId, t.getMessage());
return "{\"status\":\"pending\",\"msg\":\"支付查询暂时不可用,请稍后重试\"}";
}
}
踩过的坑
坑一:X-Forwarded-For 被伪造
用 IP 做限流 key 的时候,X-Forwarded-For 头是可以伪造的。攻击者可以随机变换这个头来绕过限流。
解决办法:不要信任客户端传的头,让 Nginx 在转发时覆盖它:
proxy_set_header X-Forwarded-For $remote_addr;
或者只取 Nginx 设置的第一段,忽略客户端传入的值。
坑二:Redis 连接超时导致限流失效
限流依赖 Redis,如果 Redis 偶尔抖动或者超时,Bucket4j 会抛异常。如果不处理,这个异常会直接返回 500 给用户,限流反而变成了事故源。
解决办法:在拦截器里 catch 住 Redis 异常,降级为放行(宁可放过也不能因为限流组件故障把正常用户拦住):
try {
if (!bucket.tryConsume(1)) {
// 限流拒绝
}
} catch (Exception e) {
log.error("限流组件异常,降级放行", e);
// 放行
}
坑三:熔断的 fallback 方法签名必须一致
Resilience4j 的 fallback 方法必须和原方法参数列表一致,最后多加一个 Throwable 参数。如果签名不匹配,fallback 不会生效,异常会直接抛出去。这个坑我调了半天才看出来。
坑四:滑动窗口太小导致误熔断
一开始我把 sliding-window-size 设成了 5,minimum-number-of-calls 设成了 3。结果接口偶发一两次超时就被熔断了,误伤很严重。后来调成 10 和 5,稳定多了。核心接口可以适当放大窗口。
坑五:限流和熔断的顺序
限流应该在熔断前面。如果先走熔断,大量请求已经打到下游了,下游挂了才触发熔断。正确的顺序是:请求进来 → 限流拦截(挡住多余流量)→ 正常流量走业务逻辑 → 调下游时走熔断保护。
在 Spring Boot 里,限流用拦截器(Interceptor)在 Controller 之前执行,熔断用注解在 Service 层,天然就是这个顺序。但如果你用网关(比如 Spring Cloud Gateway)做限流,要注意 Gateway 的限流 Filter 和下游服务的熔断是两层,不要搞混了。
监控
限流和熔断上了之后不看监控等于白上。Resilience4j 自带 Actuator 端点,接入 Prometheus + Grafana 就能看到熔断状态变化和失败率曲线。Bucket4j 没有内置监控,但可以自己埋点:每次限流拒绝的时候打一条日志或者发一个 Counter 指标到 Prometheus。
我现在每天早上扫一眼 Grafana 面板,看看有没有哪个接口的限流拒绝量异常高,或者熔断有没有频繁触发。这比出了事再看日志强太多。
限流和熔断不是什么高深技术,但真正出了事再补就晚了。我的建议是:核心接口上线前就把这两个加上,哪怕配置保守一点也比裸奔强。
评论一下?