Spring Cloud Gateway限流实战 Redis令牌桶算法+熔断降级

你的微服务网关是不是也遇到过这种场面:大促一开闸,几十万的请求像洪水一样涌进来,下游订单服务瞬间被打瘫,队列积压,用户刷屏投诉"系统繁忙"?限流不是"要不要做"的事,而是"早晚要出事故"的事。本文带你用 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 复制代码
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="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。

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: gateway-demo
  redis:
    host: localhost
    port: 6379
    password:
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order-service
          predicates:
            - Path=/api/order/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@userKeyResolver}"
            - name: CircuitBreaker
              args:
                name: orderCircuitBreaker
                fallbackUri: 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 启动类:

java 复制代码
package com.dunfan.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

    public static void main(String[] args) {
        SpringApplication.run(GatewayApplication.class, args);
    }
}

这段代码就是个入口,@SpringBootApplication 告诉 Spring "这是应用的启动中心",main 方法里的 run 负责把整个网关拉起来。它和普通后端应用的启动类没有区别,因为网关本身也是一个 Spring Boot 应用。

再看限流键解析器。这个类的职责是告诉 Gateway"限流按谁算"------我们选择按请求里的用户 ID 来区分:

java 复制代码
package com.dunfan.gateway.config;

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 RateLimitConfig {

    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            // 从请求头里取出 userId,作为限流的分组键
            String userId = exchange.getRequest().getHeaders().getFirst("userId");
            if (userId == null || userId.trim().isEmpty()) {
                userId = "anonymous";
            }
            return Mono.just(userId);
        };
    }
}

这里的关键逻辑:KeyResolver 返回一个 Mono<String>,这个字符串就是"限流键"。比如取到 userId=1001,那么所有带 userId=1001 的请求共用一个令牌桶,他们合起来每秒最多 10 个;userId=2002 用的是另一个桶,互不影响。这样不同用户不会因为别人刷接口而被连带限流,比较公平。

第四步:降级 fallback 接口

熔断触发后要走 fallbackUri,所以需要一个能响应这个降级路径的接口。Gateway 里可以用 WebFlux 的方式写一个简单的降级处理:

java 复制代码
package com.dunfan.gateway.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;

@RestController
public class FallbackController {

    @GetMapping("/fallback/orderBusy")
    public Mono<String> orderBusy() {
        return Mono.just("订单服务暂时繁忙,请稍后再试");
    }
}

当 orderCircuitBreaker 熔断打开,请求会被转到 /fallback/orderBusy,返回这句友好提示,用户看到的是"请稍后再试",而不是 500 报错。

第五步:怎么验证跑起来

把上述文件放好,本地启动 Redis(默认端口 6379),然后运行 GatewayApplication。再用下面命令模拟并发请求(比如同时发 30 个请求,超过了 burstCapacity 20):

bash 复制代码
for i in $(seq 1 30); do curl -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 要保证可用性、多实例必须共享同一套状态、降级要给有内容的数据。把这几点记牢,你就能在生产上放心地把安全阀交给网关。

记住:限流阈值靠压测数据说话,多实例限流靠共享存储兜底,降级要做得有内容。 这三条守住,你的微服务再大促也不会轻易趴窝。

相关推荐
Wang's Blog1 小时前
Java 项目部署之 Docker工具快速入门: DockerCompose 部署微服务集群
java·docker·微服务
用户072367066601 小时前
日志脱敏:不改业务代码,单遍扫描完成替换
后端
lightning_bug1 小时前
docker部署黑马笔记
后端·架构
BullSmall1 小时前
SonarQube 9.9.8-community 适配 Jenkins 版本说明
java·前端·jenkins
威哥爱编程1 小时前
【AI全栈12-10】Spring Boot 用多模态做商品图审核 / 发票识别
java·spring boot·后端
bruce541101 小时前
给 UniRAG RAG 配一个前端,为什么最后选了 Lobe UI
后端
imDwAaY1 小时前
用户态与内核态到底怎么切换?从系统调用到上下文切换
后端·操作系统
imDwAaY1 小时前
进程、线程、协程有什么区别?从资源分配到调度,一篇理清
后端·操作系统
轮子大叔1 小时前
Java8和Java17常见区别
java