你的微服务网关是不是也遇到过这种场面:大促一开闸,几十万的请求像洪水一样涌进来,下游订单服务瞬间被打瘫,队列积压,用户刷屏投诉"系统繁忙"?限流不是"要不要做"的事,而是"早晚要出事故"的事。本文带你用 Spring Cloud Gateway 的限流过滤器,配合 Redis 做令牌桶限流,再用熔断把故障隔离在网关这一层,让下游服务永远站在安全线之后。纯 Java 实战,复制就能跑。
一、这个问题到底是什么
网关(Gateway)是微服务系统的"大门",所有外部请求先进这道门,才被路由到后端的各个服务。你可以把它想象成小区门口的保安亭,所有访客进门都得经过保安登记和分流。
问题来了:如果某天有大量请求同时涌进这扇门(比如秒杀、抢购、凌晨批量任务触发、甚至恶意刷接口),门后的服务扛不住,就会出现三种典型事故——
第一,服务崩溃。下单服务每秒只能处理 1000 个请求,结果来了 1 万个,线程池被打满,服务直接宕机,连正常用户都进不去。
第二,雪崩效应。A 服务挂了,调用 A 的 B 服务还在傻等 A 的响应,把 B 自己的线程也占满了,B 跟着挂,然后 C 也挂……一个服务出事,整条链路全瘫。这就是"雪崩",比单个服务挂掉可怕得多。
第三,超时堆积。请求响应越来越慢,用户的每个操作都在转圈,体验崩坏,最终用户流失。
限流要解决的核心问题就一句话:在请求进入后端之前,按"我定好的速度"把请求放行,超出的请求要么排队、要么拒绝,绝不能让后端一次性吃下超出能力的流量。它守护的是系统的"吞吐上限"。
网关做限流有个天然优势:它处于流量的最前沿,所有请求必经此地。在这里限流,等于在"洪水入口"就装好闸门,而不是等洪水淹到下游每个服务再各自救。
二、底层原理到底怎么回事
Spring Cloud Gateway 的限流是靠一个叫过滤器(Filter)的机制实现的。简单说,过滤器就是请求在网关里"过路"时要做的一连串检查动作:先看用户有没有权限,再查这个接口允许每秒放行多少,最后才把请求路由到后端。
限流过滤器里最关键的是桶这个概念,衍生出几种算法:
固定窗口计数:把时间切成一段段固定长度的小窗口(比如 1 秒一个窗口),每个窗口里维护一个计数器,请求进来就加 1,超过上限就拒绝,窗口一过计数器清零。它实现最省事,但有个明显漏洞:假设每秒限 100 个,某个窗口最后 0.5 秒来了 100 个,下一个窗口开头 0.5 秒又来了 100 个,那么在这挨着的 1 秒时间里实际放行了 200 个,远超限速。这就是"临界突刺"问题。
滑动窗口计数:把窗口切成很多更小的子窗口,取最近一段完整时间(比如最近 1 秒)内所有子窗口计数之和作为当前速率,超过就限流。它解决了临界突刺,但需要维护多个子窗口状态,内存和计算成本更高。
令牌桶算法(Token Bucket):这是最经典也最常用来做"允许突发流量"的算法。想象一个桶,桶里装的是"令牌",令牌按固定速率往桶里注入(比如每秒注入 10 个)。每个请求要进网关,必须先"拿"走一个令牌,拿到才能放行,拿不到就拒绝或排队。桶有容量上限,满了就不再注入,所以允许短时间内的突发流量(一口气把桶里攒的令牌用完),但又不会无限突发。
Gateway 默认用的RequestRateLimiter过滤器,底层就是令牌桶算法的实现,配合 Redis 存储令牌桶的状态。为什么用 Redis?因为网关在线上通常是多实例部署的(有好几个网关进程水平扩展),如果每个实例各用一个内存里的桶,那限流就失效了——用户绕着一个实例把桶打空,换到另一个实例又是满桶。用 Redis 存令牌桶,所有网关实例共享同一个桶,限流才是全局的、准确的。
RequestRateLimiter过滤器有两个核心配置项:
redis-rate-limiter.replenishRate:令牌桶每秒注入的令牌数,也就是正常情况下每秒允许通过的请求数(基准速率)。redis-rate-limiter.burstCapacity:桶的最大容量,也就是允许的突发流量上限。它必须大于 replenishRate,否则突发能力为 0。
它实现的是流控中"平滑突发"的效果:平时请求少时桶里令牌攒着,突然来一波流量,可以先用掉攒的令牌顶上;但长期平均速率始终被卡在 replenishRate 附近,不会让后端持续超载。
熔断则和限流是两码事,但经常搭配使用。限流是"主动防水",在流量进来前就控制;熔断是"被动止损",在监控到下游开始出错后主动切断调用。熔断有点类似家里电路的保险丝:电流过大时保险丝熔断,切断电路保护电器。在 Spring Cloud 里,熔断通常用一个叫 Resilience4j 的组件实现——当下游服务连续失败超过阈值,熔断器就"打开",一段时间内直接拒绝调用该服务(快速失败并返回降级结果),让下游有时间恢复;恢复后再"半开"试探,成功就把熔断器关闭。
用一张表说清限流和熔断的区别:
| 维度 | 限流(RequestRateLimiter) | 熔断(Resilience4j) |
|---|---|---|
| 目的 | 控制进入后端的流量速度 | 隔离故障、保护已出问题的下游 |
| 触发依据 | 请求速率是否超阈值 | 下游失败率/失败次数是否超标 |
| 作用位置 | 请求进来之前 | 检测到下游故障之后 |
| 比喻 | 进门闸门,按速度放行 | 保险丝,坏了就切断 |
限流在前,熔断在后,一前一后把网关打造成微服务的"安全阀"。
三、实战:手把手写代码
整个项目由三个完整文件组成:一个 pom.xml、一个 application.yml、一个网关启动类。复制下来放进同一个工程就能跑。
第一步:pom.xml(完整的 Maven 依赖)
下面是在纯 Spring Boot 项目里引入 Spring Cloud Gateway 和 Redis 的完整依赖。注意 Spring Boot 用 4.0.7、Spring Cloud 用 2025.1.3,两者是匹配的组合(Spring Cloud 2025.1.3 只兼容 Spring Boot 4.0.x,不能拉到 4.1.x),这是版本硬约束。
<?xml version="1.0" encoding="UTF-8"?><projectxmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"><modelVersion>4.0.0</modelVersion><parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>4.0.7</version><relativePath/></parent><groupId>com.dunfan</groupId><artifactId>gateway-demo</artifactId><version>1.0.0</version><name>gateway-demo</name><description>Spring Cloud Gateway 限流 + 熔断示例</description><properties><java.version>21</java.version><spring-cloud.version>2025.1.3</spring-cloud.version></properties><dependencyManagement><dependencies><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-dependencies</artifactId><version>${spring-cloud.version}</version><type>pom</type><scope>import</scope></dependency></dependencies></dependencyManagement><dependencies><!-- 网关核心:包含路由、过滤器、限流能力 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!-- 限流要用 Redis 存令牌桶,这里引入响应式 Redis 客户端 --><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis-reactive</artifactId></dependency><!-- 熔断降级使用 Resilience4j --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-circuitbreaker-resilience4j</artifactId></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build></project>这段配置在干什么:spring-boot-starter-parent是 Boot 的"家长",统一管理一大堆依赖版本,省得你一个个写版本号。spring-cloud-dependencies通过 dependencyManagement 导入,专门管理 Spring Cloud 各组件版本(Gateway 目前由它管理到 5.0.3)。spring-cloud-starter-gateway是网关本体,spring-cloud-starter-circuitbreaker-resilience4j提供熔断能力。版本 21 是因为 Java 已经是 21 的时代,别用旧的 17 了。
第二步:application.yml(路由 + 限流 + 熔断配置)
配置文件是限流真正生效的地方。下面这段给一个/api/order/**的路由加上了限流过滤器,并且配置了一个熔断 fallback。
server:port:8080spring:application:name:gateway-demoredis:host:localhostport:6379password:cloud:gateway:routes:-id:order-serviceuri:lb://order-servicepredicates:-Path=/api/order/**filters:-name:RequestRateLimiterargs:redis-rate-limiter.replenishRate:10redis-rate-limiter.burstCapacity:20key-resolver:"#{@userKeyResolver}"-name:CircuitBreakerargs:name:orderCircuitBreakerfallbackUri:forward:/fallback/orderBusy# 上面是常规写法,下面两行是等价的简化写法,二选一即可# filters:# - RequestRateLimiter=10, 20, #{@userKeyResolver}logging:level:org.springframework.cloud.gateway:INFO逐行解释:replenishRate: 10表示正常情况下每秒最多放行 10 个请求;burstCapacity: 20表示最多允许突发 20 个(相当于桶里最多攒 20 个令牌)。key-resolver: "#{@userKeyResolver}"指定"按什么来区分限流对象"——这里是按用户身份(下面定义),这样是每个用户各自限 10 个/秒,而不是所有人共用一个桶。
CircuitBreaker过滤器名字里配置了一个叫orderCircuitBreaker的熔断器,当它检测到调用 order-service 失败超过阈值时,会走fallbackUri指向的降级接口,返回"系统忙"的友好提示,而不是让用户看到一堆报错堆栈。
第三步:网关启动类 + 限流键解析器(两个完整 Java 文件)
先看主启动类。Gateway 项目不写常规的 Controller,它本身就是一个"转发器",所以启动类就是标准 Spring Boot 启动类:
packagecom.dunfan.gateway;importorg.springframework.boot.SpringApplication;importorg.springframework.boot.autoconfigure.SpringBootApplication;@SpringBootApplicationpublicclassGatewayApplication{publicstaticvoidmain(String[]args){SpringApplication.run(GatewayApplication.class,args);}}这段代码就是个入口,@SpringBootApplication告诉 Spring “这是应用的启动中心”,main方法里的run负责把整个网关拉起来。它和普通后端应用的启动类没有区别,因为网关本身也是一个 Spring Boot 应用。
再看限流键解析器。这个类的职责是告诉 Gateway"限流按谁算"——我们选择按请求里的用户 ID 来区分:
packagecom.dunfan.gateway.config;importorg.springframework.cloud.gateway.filter.ratelimit.KeyResolver;importorg.springframework.context.annotation.Bean;importorg.springframework.context.annotation.Configuration;importreactor.core.publisher.Mono;@ConfigurationpublicclassRateLimitConfig{@BeanpublicKeyResolveruserKeyResolver(){returnexchange->{// 从请求头里取出 userId,作为限流的分组键StringuserId=exchange.getRequest().getHeaders().getFirst("userId");if(userId==null||userId.trim().isEmpty()){userId="anonymous";}returnMono.just(userId);};}}这里的关键逻辑:KeyResolver返回一个Mono<String>,这个字符串就是"限流键"。比如取到userId=1001,那么所有带userId=1001的请求共用一个令牌桶,他们合起来每秒最多 10 个;userId=2002用的是另一个桶,互不影响。这样不同用户不会因为别人刷接口而被连带限流,比较公平。
第四步:降级 fallback 接口
熔断触发后要走fallbackUri,所以需要一个能响应这个降级路径的接口。Gateway 里可以用 WebFlux 的方式写一个简单的降级处理:
packagecom.dunfan.gateway.controller;importorg.springframework.web.bind.annotation.GetMapping;importorg.springframework.web.bind.annotation.RestController;importreactor.core.publisher.Mono;@RestControllerpublicclassFallbackController{@GetMapping("/fallback/orderBusy")publicMono<String>orderBusy(){returnMono.just("订单服务暂时繁忙,请稍后再试");}}当orderCircuitBreaker熔断打开,请求会被转到/fallback/orderBusy,返回这句友好提示,用户看到的是"请稍后再试",而不是 500 报错。
第五步:怎么验证跑起来
把上述文件放好,本地启动 Redis(默认端口 6379),然后运行GatewayApplication。再用下面命令模拟并发请求(比如同时发 30 个请求,超过了 burstCapacity 20):
foriin$(seq130);docurl-s-o/dev/null-w"%{http_code}\n"-H"userId: 1001"http://localhost:8080/api/order/1&done;wait你会看到:一部分请求返回正常(走路由转发,得到后端响应),超出 20 的请求被限流拦截。如果你没起真正的 order-service,返回的可能是网关的 503 或熔断降级响应——这正演示了"下游不可用被熔断兜住"的效果。把replenishRate调大调小,就能直观感受"每秒放行多少个"的差别。
四、踩坑经验和最佳实践
限流看起来配置几分钟就完事,但真实生产里到处是坑,下面几条都是我实际趟过的。
坑一:burstCapacity 必须大于 replenishRate,否则限流"卡死"。这两个值是很多新手栽跟头的地方。如果你把 burstCapacity 设得小于等于 replenishRate,桶里永远装不满能突发的量,限流行为会变得很怪,甚至出现该放行的请求也被拦。经验上 burstCapacity 设为 replenishRate 的 2 到 3 倍比较合理,太大会失去限流意义,太小又没突发能力。
坑二:key-resolver 不分键,等于全局一个桶,一个用户把全站堵死。如果限流键写死成一个固定字符串,那么所有用户共用一个令牌桶,某个用户恶意狂刷瞬间把桶打完,正常用户全被连带限流。生产必须按用户、按 IP、或按接口维度合理分键。分键粒度越细,越能精准限流,但也越考验你对业务的理解。
坑三:Redis 挂了,限流直接瘫痪(甚至更糟)。RequestRateLimiter 依赖 Redis 存令牌桶,如果 Redis 不可用,限流过滤器可能抛异常导致整个请求链崩掉。生产一定要给 Redis 做高可用(集群或哨兵),并且给限流过滤器配好容错——比如 Redis 异常时是"放行还是拒绝",这个策略必须在压测时定下来,别到事故现场才手忙脚乱。
坑四:GateWay 多实例必须共享同一个 Redis,否则限流失效。这是最容易被忽视的。很多团队本地一个实例调试好好的,一上生产开了 4 个网关实例才发现限流完全失效——因为每个实例自己的内存桶各算各的。必须让所有实例连同一个 Redis,令牌桶状态集中存储,限流才能全局生效。
坑五:熔断的 fallback 别只给一句话,要给"可用的降级数据"。很多项目熔断降级就反回一句"系统繁忙",对用户毫无价值。好的降级是:返回缓存里的旧数据、返回本地默认值、或返回更慢的可接受响应。把降级做"有内容",用户感知会好很多。
最佳实践小结:
- 限流阈值要基于压测数据定,不是拍脑袋。先测出后端服务的真实最大吞吐(比如 800 QPS),再留 20% 余量设限流值。
- 先限流、后熔断,顺序别反。限流挡在前面防超载,熔断跟在后面防故障扩散。
- 把限流、熔断、降级的触发次数和原因打日志、接监控告警,出事能第一时间定位是谁在拦、为什么拦。
- 配置用配置中心(比如 Nacos)管理,改限流阈值不用重启网关,线上调参才来得及。
五、性能对比和技术选型
网关层面的限流方案不止 Spring Cloud Gateway 一种,各有适用场景:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| Spring Cloud Gateway + Redis 限流 | 与微服务生态无缝集成,配置即用,支持动态调参 | 每个请求多一次 Redis 网络往返,多实例下是瓶颈点 | Spring Cloud 生态内的标准网关 |
| 网关层 + 本地内存令牌桶(如 Guava) | 无额外网络开销,性能最高 | 多实例各自为政,限流不全局 | 单实例、对实时性要求极高的场景 |
| 独立限流组件(如 Sentinel 独立部署) | 限流规则可视化、动态推送,功能全 | 引入额外组件,部署和运维成本高 | 大规模、限流规则复杂多变的中大型系统 |
| Nginx + Lua 限流 | 在流量最前端,性能极好 | 需要 Lua 功底,规则下发不如微服务组件灵活 | 偏基础设施层、统一入口的防护 |
性能层面:纯内存令牌桶(Guava)单机性能最高,因为不依赖外部存储;加 Redis 后每个限流请求多一次网络往返,在高并发下 Redis 就成了必须专项优化的点(比如用 Redis 集群分散压力)。Gateway 的 RequestRateLimiter 更适合"Spring Cloud 一体、方便动态配置"的场景,它的正确性带来的稳定性收益,通常大于那一点点额外网络开销。
选型建议:如果你的系统已经是 Spring Cloud 微服务架构,优先用 Gateway 自带限流 + Resilience4j 熔断,成本最低、集成最顺;如果限流规则极其复杂、需要可视化运维和流控大盘,再引入 Sentinel 这样的独立限流组件。
六、总结
限流和熔断是微服务网关保障稳定性的两件核心武器,它们解决的是两个不同的问题:限流在请求进来前"主动防水",控制进入后端的流量速率;熔断在检测到下游故障后"被动止损",隔离故障防止雪崩。两者一前一后配合,网关就成了微服务的"安全阀"。
Spring Cloud Gateway 用一个RequestRateLimiter过滤器就实现了令牌桶限流,配合 Redis 让多实例共享同一个桶、实现全局准确的限流;再用CircuitBreaker过滤器接上 Resilience4j 做熔断降级,把故障兜在网关这一层,用户只看到友好的"请稍后再试"。
实践中最容易栽的坑就几条:burstCapacity 与 replenishRate 的关系、限流键要合理分维度、Redis 要保证可用性、多实例必须共享同一套状态、降级要给有内容的数据。把这几点记牢,你就能在生产上放心地把安全阀交给网关。
记住:限流阈值靠压测数据说话,多实例限流靠共享存储兜底,降级要做得有内容。这三条守住,你的微服务再大促也不会轻易趴窝。