网关是微服务架构的流量入口,限流和熔断是保护下游服务的两道防线。这篇文章记录我在生产环境实际配置这两个功能的过程,不是文档翻译,只记录真正用到的部分和踩的坑。

环境说明

  • Spring Cloud Gateway 4.x
  • Spring Boot 3.x
  • Resilience4j(熔断)
  • Redis(限流存储)

限流:Redis 令牌桶

Gateway 内置了基于 Redis 的令牌桶限流,原理是每个路由维护一个令牌桶,请求消耗令牌,令牌按固定速率补充。

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

配置

spring:
  cloud:
    gateway:
      routes:
        - id: user-service
          uri: lb://user-service
          predicates:
            - Path=/api/user/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100    # 每秒补充 100 个令牌
                redis-rate-limiter.burstCapacity: 200    # 桶容量(允许突发)
                redis-rate-limiter.requestedTokens: 1    # 每个请求消耗 1 个令牌
                key-resolver: "#{@ipKeyResolver}"        # 按 IP 限流

Key Resolver:按 IP 还是按用户

内置的是按 IP 限流,实际业务通常需要按用户 ID:

@Configuration
public class RateLimiterConfig {

    // 按 IP 限流(适合未登录接口)
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> Mono.just(
            exchange.getRequest().getRemoteAddress().getAddress().getHostAddress()
        );
    }

    // 按用户 ID 限流(适合已登录接口,从 Header 或 JWT 取)
    @Bean
    @Primary
    public KeyResolver userKeyResolver() {
        return exchange -> {
            String userId = exchange.getRequest().getHeaders().getFirst("X-User-Id");
            return Mono.just(userId != null ? userId : "anonymous");
        };
    }
}

限流触发后的响应

默认是 429 空响应,体验很差。自定义一下:

@Component
public class CustomRateLimitErrorHandler implements ErrorWebExceptionHandler {

    @Override
    public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
        if (ex instanceof ResponseStatusException rse
                && rse.getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {

            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            exchange.getResponse().getHeaders()
                    .add("Content-Type", "application/json;charset=UTF-8");

            String body = """
                {"code": 429, "message": "请求过于频繁,请稍后重试"}
                """;
            DataBuffer buffer = exchange.getResponse().bufferFactory()
                    .wrap(body.getBytes(StandardCharsets.UTF_8));
            return exchange.getResponse().writeWith(Mono.just(buffer));
        }
        return Mono.error(ex);
    }
}

熔断:Resilience4j

限流是防止流量过大把下游压垮,熔断是在下游已经出问题时快速失败,避免请求堆积拖垮整个链路。

依赖

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId>
</dependency>

配置熔断器

spring:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - name: CircuitBreaker
              args:
                name: orderServiceCB
                fallbackUri: forward:/fallback/order

resilience4j:
  circuitbreaker:
    instances:
      orderServiceCB:
        slidingWindowType: COUNT_BASED
        slidingWindowSize: 20          # 统计最近 20 次请求
        failureRateThreshold: 50       # 失败率超过 50% 开启熔断
        waitDurationInOpenState: 30s   # 熔断后等待 30s 再半开
        permittedNumberOfCallsInHalfOpenState: 5  # 半开状态允许 5 次探测
        minimumNumberOfCalls: 10       # 至少 10 次请求后才统计

Fallback 接口

@RestController
@RequestMapping("/fallback")
public class FallbackController {

    @RequestMapping("/order")
    public Mono<Map<String, Object>> orderFallback(ServerWebExchange exchange) {
        // 记录熔断日志,方便排查
        log.warn("Circuit breaker triggered for order-service, path: {}",
            exchange.getRequest().getPath());

        return Mono.just(Map.of(
            "code", 503,
            "message", "服务暂时不可用,请稍后重试",
            "fallback", true
        ));
    }
}

踩坑记录

坑 1:Redis 限流 Key 重复,所有接口共享一个桶

原因:key-resolver Bean 名字写错,没有正确注入,Gateway 使用了默认的 resolver,所有路由共享同一个 Key。

排查方法:Redis 里查看 request_rate_limiter.* 的 Key,正常情况下应该按 IP 或用户 ID 分开。

坑 2:熔断后 Fallback 接口返回 200,监控系统看不到异常

业务上 fallback 应该返回 503,而不是 200。改 fallback 接口的响应码:

@RequestMapping("/order")
public Mono<ResponseEntity<Map<String, Object>>> orderFallback() {
    return Mono.just(ResponseEntity
        .status(HttpStatus.SERVICE_UNAVAILABLE)
        .body(Map.of("code", 503, "message", "服务暂时不可用")));
}

坑 3:minimumNumberOfCalls 设置太小,新上线的服务频繁误熔断

流量低谷期,几次偶发超时就触发熔断。调大 minimumNumberOfCalls(建议生产环境设 20-50),同时调整 slidingWindowSize


监控:看熔断状态

Resilience4j 暴露 Actuator 端点,可以看到熔断器当前状态:

# 查看所有熔断器状态
curl http://gateway:8080/actuator/circuitbreakers

# 输出示例
{
  "circuitBreakers": {
    "orderServiceCB": {
      "state": "CLOSED",  # CLOSED=正常, OPEN=熔断, HALF_OPEN=探测中
      "failureRate": "5.0%",
      "slowCallRate": "0.0%"
    }
  }
}

接入 Grafana 后可以做成实时看板,熔断事件触发时报警。


总结

功能组件核心配置
限流Redis RequestRateLimiterreplenishRate(速率)+ burstCapacity(突发)
按用户限流自定义 KeyResolver从 Header / JWT 取用户 ID
熔断Resilience4j CircuitBreakerfailureRateThreshold + slidingWindowSize
降级响应Fallback Controller返回 503,记录日志

这两个功能配合使用,基本能覆盖网关层面的稳定性保护。实际调参需要根据业务流量模式来,不存在通用的”最佳值”,建议先在压测环境跑通再上生产。