Spring Cloud Gateway RequestRateLimiter 实战:Redis 令牌桶限流从配置到本地压测验证

Spring Cloud Gateway RequestRateLimiter 实战:Redis 令牌桶限流从配置到本地压测验证

网关限流最容易踩的坑,不是"有没有加限流组件",而是配置看起来生效了,压测时却不知道到底按什么维度限、什么时候返回 429、Redis 里令牌桶参数该怎么解释。

本文用 Spring Cloud Gateway 的 RequestRateLimiter 讲一条最小实战链路:

复制代码
客户端请求
  -> Spring Cloud Gateway 路由匹配
  -> RequestRateLimiter 过滤器
  -> KeyResolver 解析限流 key
  -> RedisRateLimiter 按令牌桶算法判断是否放行
  -> 放行转发到后端服务 / 拒绝返回 429

目标读者是 Java 后端、微服务网关维护者和正在做接口限流治理的架构师。读完你应该能拿到三件东西:一份 Gateway + Redis 限流配置、一段 KeyResolver 示例、以及一套本地压测验证方法。

验证说明:本文核心参数和行为来自 Spring Cloud Gateway 官方文档 RequestRateLimiter GatewayFilter Factory。当前写作机器未安装 JDK/Maven,无法在本机完整启动 Java demo;文中的 Spring Boot 工程代码是工程骨架示例,压测命令和预期现象按官方行为给出,落地时请在你的项目版本下编译运行确认。

1. RequestRateLimiter 解决的是什么问题

在微服务里,限流一般不建议散落在每个业务服务里,否则会出现几个问题:

问题 放在业务服务里 放在 Gateway 层
统一治理 每个服务各配一套 路由维度统一配置
防护位置 请求已经打到业务服务 请求进入业务前先拦截
调整成本 多个服务逐个改 网关配置集中调整
观测口径 分散在业务日志 网关侧统一看 429、RT、命中率

Spring Cloud Gateway 的 RequestRateLimiter 是一个 GatewayFilter Factory。它会在请求进入某条路由后,调用一个 RateLimiter 判断请求是否允许通过。使用 Redis 时,常见实现是 RedisRateLimiter,底层思路是令牌桶算法。

如果请求不允许通过,默认返回:

复制代码
HTTP/1.1 429 Too Many Requests

这点很重要:压测时不要只看后端服务 QPS,还要看 Gateway 是否稳定返回 429。

2. 环境版本和依赖

一个最小 demo 可以按下面环境准备:

组件 建议版本 / 说明
JDK 17+
Spring Boot 3.x
Spring Cloud Gateway 与 Spring Boot 版本匹配的 Spring Cloud 版本线
Redis 6.x / 7.x 均可
压测工具 curlseqxargs,或 hey / wrk

Maven 依赖至少需要 Gateway 和 reactive Redis:

复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.cloud</groupId>
        <artifactId>spring-cloud-starter-gateway-server-webflux</artifactId>
    </dependency>

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

说明:不同 Spring Cloud 版本的 artifact 命名可能会调整。老项目里也常见 spring-cloud-starter-gateway。新项目请以当前 Spring Cloud Gateway 官方文档和你所用 Spring Cloud BOM 为准。

Redis 可以用 Docker 快速启动:

复制代码
docker run --name gateway-redis -p 6379:6379 -d redis:7-alpine

确认 Redis 端口:

复制代码
docker ps --filter name=gateway-redis

3. 最小路由配置:按路由加限流

假设后端服务地址是 http://localhost:8081,Gateway 监听 8080。下面是一份最小 application.yml

复制代码
server:
  port: 8080

spring:
  data:
    redis:
      host: localhost
      port: 6379

  cloud:
    gateway:
      server:
        webflux:
          routes:
            - id: order-api
              uri: http://localhost:8081
              predicates:
                - Path=/api/orders/**
              filters:
                - name: RequestRateLimiter
                  args:
                    key-resolver: '#{@ipKeyResolver}'
                    redis-rate-limiter.replenishRate: 2
                    redis-rate-limiter.burstCapacity: 4
                    redis-rate-limiter.requestedTokens: 1

几个参数先翻译成人话:

参数 含义 示例值说明
key-resolver 按什么维度限流 示例按客户端 IP 限流
replenishRate 每秒补充多少令牌 每秒恢复 2 个请求额度
burstCapacity 桶最多能装多少令牌 瞬时最多允许 4 个请求突发
requestedTokens 每个请求消耗几个令牌 默认 1,一次请求扣 1 个

如果 replenishRate=2burstCapacity=4,可以粗略理解为:稳定每秒允许 2 个请求,短时间最多可以攒到 4 个请求的突发能力。连续猛打超过令牌恢复速度后,就会开始返回 429。

4. KeyResolver:限流 key 一定要想清楚

RequestRateLimiter 不是天然知道"按谁限流"。它需要一个 KeyResolver

4.1 按 IP 限流

本地 demo 最容易按 IP 限流:

java 复制代码
`import org.springframework.cloud.gateway.filter.ratelimit.KeyResolver;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.core.publisher.Mono;

@Configuration
public class RateLimiterConfig {

    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> {
            var remoteAddress = exchange.getRequest().getRemoteAddress();
            if (remoteAddress == null || remoteAddress.getAddress() == null) {
                return Mono.just("unknown");
            }
            return Mono.just(remoteAddress.getAddress().getHostAddress());
        };
    }
}
`

按 IP 的优点是简单,缺点也明显:如果前面还有 Nginx、SLB、Ingress,remoteAddress 可能拿到的是代理 IP,而不是用户真实 IP。生产环境通常要结合可信代理头处理,例如 X-Forwarded-For,并且只信任来自内网代理层写入的头。

4.2 按用户 ID 限流

如果你的接口已经有登录态,用户维度更适合做业务限流:

java 复制代码
`@Bean
public KeyResolver userKeyResolver() {
    return exchange -> {
        String userId = exchange.getRequest()
                .getHeaders()
                .getFirst("X-User-Id");
        return Mono.justOrEmpty(userId);
    };
}
`

对应配置改成:

复制代码
filters:
  - name: RequestRateLimiter
    args:
      key-resolver: '#{@userKeyResolver}'
      redis-rate-limiter.replenishRate: 5
      redis-rate-limiter.burstCapacity: 10
      redis-rate-limiter.requestedTokens: 1

官方文档里提到:如果 KeyResolver 没有解析出 key,默认会拒绝请求。可以通过下面两个配置调整空 key 行为:

复制代码
spring:
  cloud:
    gateway:
      filter:
        request-rate-limiter:
          deny-empty-key: true
          empty-key-status-code: 403

生产上我更建议默认拒绝空 key。否则一旦用户 ID 获取失败,就可能绕过限流。

5. 本地后端服务:准备一个被转发接口

为了验证 Gateway 是否真的拦截,可以准备一个最小后端服务,监听 8081

java 复制代码
`@RestController
@RequestMapping("/api/orders")
public class OrderController {

    @GetMapping("/{id}")
    public Map<String, Object> getOrder(@PathVariable String id) {
        return Map.of(
                "id", id,
                "status", "CREATED",
                "timestamp", System.currentTimeMillis()
        );
    }
}
`

启动后验证直连后端:

复制代码
curl -i http://localhost:8081/api/orders/1001

再验证经过 Gateway:

复制代码
curl -i http://localhost:8080/api/orders/1001

正常情况下,经过 Gateway 的请求应该被转发到后端,返回业务 JSON。

6. 压测验证:重点看 200 和 429 的比例

现在开始打请求。先用一个简单循环:

复制代码
for i in $(seq 1 10); do
  curl -s -o /dev/null -w "%{http_code}\n" \
    http://localhost:8080/api/orders/1001
done

如果配置为:

复制代码
redis-rate-limiter.replenishRate: 2
redis-rate-limiter.burstCapacity: 4
redis-rate-limiter.requestedTokens: 1

你会看到前几个请求更可能是 200,后面连续请求开始出现 429。示例输出形态如下:

复制代码
200
200
200
200
429
429
429
429
429
429

注意:具体 200 数量会受请求间隔、机器速度和令牌恢复影响,不要把示例输出当成固定断言。真正要验证的是:

  1. 限流前请求能转发到后端;
  2. 连续请求超过令牌桶能力后返回 429;
  3. 等待一段时间后令牌恢复,请求又能通过。

可以加一个等待验证令牌恢复:

复制代码
sleep 2
curl -i http://localhost:8080/api/orders/1001

如果恢复正常,说明令牌桶在按 replenishRate 补充令牌。

并发压测命令

不用额外安装工具,也可以用 xargs 模拟并发:

复制代码
seq 1 30 | xargs -n1 -P10 -I{} \
  curl -s -o /dev/null -w "%{http_code}\n" \
  http://localhost:8080/api/orders/1001 | sort | uniq -c

可能看到类似:

复制代码
      4 200
     26 429

这和 burstCapacity=4 的直觉是一致的:同一时刻突发打进来时,最多先吃掉桶里已有令牌,后续请求被拒绝。

7. Redis 里发生了什么

RedisRateLimiter 会把限流状态放到 Redis。你不需要手写 Redis key,但排查时可以看 key 是否生成:

复制代码
docker exec -it gateway-redis redis-cli keys '*request_rate_limiter*'

常见排查重点:

现象 可能原因 检查方式
所有请求都是 200 路由没匹配 / filter 没生效 / key 每次都不同 看 Gateway 路由日志、打印 key
所有请求都是 429 burstCapacity=0 / key 为空且被拒绝 / Redis 时间或脚本异常 检查配置和 Gateway 日志
压测结果不稳定 请求间隔导致令牌恢复 增大并发、降低 replenishRate
多个用户互相影响 KeyResolver 维度太粗 改成 userId / tenantId
线上限流失效 代理 IP 被当成用户 IP 检查 X-Forwarded-For 处理链路

如果要更直观看到限流 key,可以在 KeyResolver 里临时打印:

java 复制代码
`@Bean
public KeyResolver ipKeyResolver() {
    return exchange -> {
        String key = exchange.getRequest().getRemoteAddress()
                .getAddress()
                .getHostAddress();
        log.info("gateway rate limit key={}", key);
        return Mono.just(key);
    };
}
`

生产环境不建议长期打印每个请求的 key,容易造成日志量过大。

8. 参数怎么取舍:replenishRate 和 burstCapacity 不要乱填

很多配置问题都出在这两个参数上。

8.1 稳定限流

如果你希望非常稳定,例如每秒最多 10 个请求,不允许明显突发:

复制代码
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 10

这种配置简单,适合后台接口、内部接口或不希望突发流量压垮下游的场景。

8.2 允许短突发

如果业务允许短时间突发,例如按钮点击、页面初始化、移动端重试:

复制代码
redis-rate-limiter.replenishRate: 10
redis-rate-limiter.burstCapacity: 30

含义是长期看每秒 10 个请求,但桶最多能攒到 30 个令牌。用户短时间内可以打出一波请求,但连续突发仍然会被限制。

8.3 降低单请求成本以支持低速率

官方文档还提到 requestedTokens。默认一个请求消耗 1 个 token。如果你要表达"每分钟 1 个请求"这种低速率,不能只把每秒速率写成小数。常见做法是把 requestedTokens 和补充速率组合起来设计。

例如需要更细粒度的时间窗口时,可以先用普通秒级参数满足大多数接口;真正复杂的分钟级、租户级、套餐级限流,建议封装独立限流策略,不要只靠一条 YAML 配置硬凑。

9. 生产落地建议

9.1 限流 key 要和业务风险一致

不同接口的 key 不一样:

接口类型 推荐 key 原因
未登录公开接口 IP / 设备指纹 防爬、防刷
登录用户接口 userId 防单用户刷接口
多租户 API tenantId + appId 防某个租户拖垮系统
开放平台接口 accessKey / clientId 和调用方额度绑定
后台管理接口 userId + path 防误操作和脚本重复提交

不要所有路由都用一个 IP key。公司内网、代理层、NAT 出口会让很多请求看起来来自同一个 IP,容易误伤。

9.2 限流只是第一道防线

Gateway 限流适合挡住入口流量,但它不是完整稳定性方案。生产上通常还要配合:

  • 下游服务线程池隔离;
  • 熔断和超时;
  • Redis 异常时的降级策略;
  • 429 指标监控和告警;
  • 按路由、按租户的限流配置中心化管理;
  • 对重要接口做灰度和压测基线。

9.3 429 要进入可观测性

上线前至少看三类指标:

复制代码
gateway_requests_total{routeId="order-api", outcome="SUCCESS"}
gateway_requests_total{routeId="order-api", status="TOO_MANY_REQUESTS"}
redis_command_latency / Redis 连接池指标

如果只看业务服务 QPS,你可能会误判"流量下降了";实际上可能是 Gateway 已经大量返回 429。

10. 常见问题

Q1:为什么我配置了限流但没有 429?

优先按这个顺序查:

  1. 请求路径是否真的匹配该 route;
  2. filter 名称是否写成 RequestRateLimiter
  3. key-resolver 是否引用到了正确 bean;
  4. Redis 是否连接成功;
  5. KeyResolver 每次返回的 key 是否一致;
  6. 压测强度是否足够,令牌可能已经恢复了。

Q2:为什么一启动就全部 429?

常见原因:

  1. burstCapacity 写成 0;
  2. KeyResolver 返回 empty,而默认空 key 会被拒绝;
  3. Redis 连接异常或 Lua 脚本执行失败;
  4. 配置层级写错,导致参数没有按预期生效。

可以先把 replenishRateburstCapacity 调大,确认主链路正常,再逐步收紧。

Q3:按 IP 限流上线后为什么误伤严重?

大概率是代理链路问题。很多生产环境里,Gateway 前面还有 SLB、Nginx、Ingress 或 API 网关。如果没有正确处理转发头,Gateway 看到的可能是代理层 IP。结果就是大量真实用户共享一个限流 key。

生产上要明确:

  • 哪一层写入 X-Forwarded-For
  • Gateway 是否信任这个头;
  • 是否只信任来自内网代理的头;
  • 是否需要按 userId / appId 替代 IP 限流。

Q4:Redis 挂了,限流应该放行还是拒绝?

这不是一个纯技术默认值问题,而是业务取舍:

策略 优点 风险
fail-open Redis 故障时尽量不影响用户 下游可能被流量打穿
fail-closed 优先保护下游 用户请求可能被误拒绝

核心交易、支付、库存类接口通常更偏保护下游;内容浏览、低风险读接口可能更偏可用性。不要上线时才讨论这个问题。

11. 小结

Spring Cloud Gateway 的 RequestRateLimiter 不难配置,真正难的是把它验证清楚:

  1. 路由是否匹配;
  2. key 是否稳定;
  3. Redis 令牌桶参数是否符合预期;
  4. 超限后是否返回 429;
  5. 等待后是否能恢复;
  6. 线上是否会被代理 IP、空 key、Redis 故障和指标缺失坑到。

如果只是复制一段 YAML,上线后很容易不知道限流到底有没有生效。我的建议是:每条关键路由都保留一组本地压测命令和预期 429 现象,先把限流行为跑清楚,再谈生产参数怎么调。

如果你关注 Java 后端、Spring Cloud、网关治理和线上稳定性建设,可以关注我的 CSDN 专栏。

相关推荐
ai产品老杨2 小时前
【架构深评】如何基于 GB28181 与 RTSP 协议栈,构建解耦、异构的百万级 AI 视频流媒体管理平台?(附开源源码)
人工智能·架构·媒体
java_cj2 小时前
K8s入门第一课:从零理解Kubernetes核心概念与架构设计
运维·云原生·容器·架构·kubernetes
段一凡-华北理工大学2 小时前
工业领域的Hadoop架构学习~系列文章22:Hadoop生态展望 - 面向未来的技术演进
大数据·人工智能·hadoop·分布式·学习·架构·高炉炼铁
AI人工智能+电脑小能手2 小时前
【大白话说Java面试题 第105题】【并发篇】第5题:说一下 synchronized 关键字的底层原理?
java·开发语言·面试
yueping22 小时前
【无标题】
java·开发语言
摇滚侠2 小时前
Spring 零基础入门到进阶 基于 XML 管理 Bean 29-37
xml·java·数据库·后端·spring·intellij-idea
TDengine (老段)2 小时前
TDengine 语义分析与 AST 重写 — Catalog 校验、列绑定与表达式规范化
java·大数据·数据库·物联网·时序数据库·tdengine·涛思数据
叫我:松哥2 小时前
基于deepseek大语言模型的项目架构图设计与绘制系统
人工智能·语言模型·自然语言处理·架构·flask·bootstrap
fengxin_rou2 小时前
Java垃圾回收机制深度解析:从原理到实战
java·jvm·性能优化·gc·垃圾回收