侧边栏壁纸
  • 累计撰写 116 篇文章
  • 累计收到 2 条评论

接口被刷到扛不住?我用限流+熔断护住了服务,几个关键配置说清楚

2026-5-19 / 0 评论 / 8 阅读
🤖AI摘要
本文介绍了在Spring Boot项目中实施限流和熔断的策略,以防止接口被刷到扛不住导致的服务雪崩。文章首先区分了限流和熔断的区别,强调了两者在保护系统不同层面的重要性。接着,作者详细介绍了使用Bucket4j和Redis实现分布式限流的方法,并展示了相关配置和拦截器代码。最后,作者推荐了Resilience4j作为熔断解决方案,以应对下游服务故障。

之前有一次线上事故,一个接口被第三方脚本疯狂调用,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 面板,看看有没有哪个接口的限流拒绝量异常高,或者熔断有没有频繁触发。这比出了事再看日志强太多。

限流和熔断不是什么高深技术,但真正出了事再补就晚了。我的建议是:核心接口上线前就把这两个加上,哪怕配置保守一点也比裸奔强。

评论一下?

OωO
取消