Spring Cloud Gateway限流:从Redis到Sentinel的三种方案

一、为什么网关层需要限流?

在微服务架构中,网关是所有流量的入口。如果没有限流保护,一个突发流量高峰就可能导致下游服务雪崩。

先看一个真实案例:

某电商平台大促期间,由于未在网关层做限流,秒杀接口的流量在1秒内激增了100倍,直接打垮了商品服务和订单服务,最终导致30分钟的服务不可用。

网关限流的核心价值:

价值 说明
保护下游服务 防止突发流量冲垮业务服务
资源隔离 保障核心API的可用性,牺牲非核心API
成本控制 云环境下,限流能有效控制资源消耗成本
防刷治理 拦截恶意刷接口的请求

二、限流算法选型:为什么选择令牌桶?

在实现限流之前,先了解主流的限流算法及其优缺点:

2.1 计数器算法(固定窗口)

text 复制代码
时间窗口(1s)    时间窗口(1s)    时间窗口(1s)
│ 请求100 │      │ 请求100 │      │ 请求100 │
└─────────┘      └─────────┘      └─────────┘

最简单的实现,统计每个时间窗口内的请求数。

问题:存在"突刺现象"。窗口切换瞬间,可能涌入2倍阈值的请求(窗口末尾100个 + 窗口开头100个 = 200个)。

2.2 滑动窗口算法

text 复制代码
时间轴 →  ┌──┬──┬──┬──┬──┐
          │10│10│10│10│10│  ← 每个小格代表一个子窗口
          └──┴──┴──┴──┴──┘
          ←──── 滑动窗口 ────→

将固定窗口切分为多个子窗口,随着时间滑动,统计窗口内的总请求数。

✅ 解决了固定窗口的突刺问题,但实现复杂度稍高,且需要存储每个子窗口的计数。

2.3 令牌桶算法(重点)

text 复制代码
          ┌──────────────┐
 请求 ──▶ │  桶容量 = 20  │ ──▶ 放行(有令牌时)
          │  速率 = 10/s  │ ──▶ 拒绝(无令牌时)
          └──────────────┘
                ▲
                │ 以固定速率(10/s)生成令牌

这是Spring Cloud Gateway RequestRateLimiter 采用的算法,也是生产环境最推荐的方式。

核心参数:

  • replenishRate:令牌生成速率,即允许的每秒请求数(QPS)
  • burstCapacity:桶容量,允许的突发请求数

为什么选择令牌桶?

✅ 允许合理突发(桶内积累的令牌可应对短时流量洪峰)

✅ 平滑限流,不会出现"一刀切"的拒绝

✅ 适合互联网业务的流量特征(有波峰波谷)


三、方案一:Redis + RequestRateLimiter(最常用)

这是Spring Cloud Gateway官方提供的限流方案,基于Redis实现分布式限流,适合网关集群部署的场景。

3.1 整体架构

text 复制代码
┌─────────────────────────────────────────────────────┐
│                   Spring Cloud Gateway               │
│  ┌───────────┐    ┌──────────────┐    ┌──────────┐ │
│  │ 请求进入  │───▶│ KeyResolver  │───▶│ 限流判断 │ │
│  └───────────┘    │ (确定限流维度)│    │ 令牌桶   │ │
│                    └──────────────┘    └────┬─────┘ │
└─────────────────────────────────────────────┼───────┘
                                              │
                                              ▼
                                    ┌─────────────────┐
                                    │   Redis集群     │
                                    │ (存储令牌计数)  │
                                    └─────────────────┘

3.2 完整实现步骤

Step 1:添加依赖

xml 复制代码
<!-- gateway 核心依赖 -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!-- Redis 响应式驱动(必需) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>

Step 2:配置Redis连接

yml 复制代码
spring:
  redis:
    cluster:
      nodes:
        - 192.168.1.101:6379
        - 192.168.1.102:6379
        - 192.168.1.103:6379
    password: your_password
    timeout: 3000ms
    lettuce:
      pool:
        max-active: 100
        max-idle: 20
        min-idle: 5

Step 3:定义限流维度(KeyResolver)

这是限流策略的核心,决定了"按什么来限流"。

java 复制代码
@Configuration
public class KeyResolverConfig {

    /**
     * 方案一:按客户端IP限流
     * 适合:开放API、防刷场景
     */
    @Bean
    public KeyResolver ipKeyResolver() {
        return exchange -> {
            String ip = exchange.getRequest().getRemoteAddress()
                .orElseThrow(() -> new RuntimeException("无法获取IP"))
                .getAddress().getHostAddress();
            return Mono.just("ip:" + ip);
        };
    }

    /**
     * 方案二:按用户ID限流
     * 适合:需要区分用户等级的付费API
     */
    @Bean
    public KeyResolver userKeyResolver() {
        return exchange -> {
            // 从请求Header中获取用户Token
            String token = exchange.getRequest().getHeaders()
                .getFirst("Authorization");
            String userId = extractUserIdFromToken(token);
            return Mono.just("user:" + userId);
        };
    }

    /**
     * 方案三:按API路径限流
     * 适合:不同接口不同限流阈值
     */
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> {
            String path = exchange.getRequest().getURI().getPath();
            return Mono.just("path:" + path);
        };
    }

    /**
     * 方案四:组合维度(推荐)
     * 格式:IP + API路径,实现更精细的限流
     */
    @Bean
    public KeyResolver compositeKeyResolver() {
        return exchange -> {
            String ip = exchange.getRequest().getRemoteAddress()
                .orElseThrow(() -> new RuntimeException("无法获取IP"))
                .getAddress().getHostAddress();
            String path = exchange.getRequest().getURI().getPath();
            // 组合Key,例如:192.168.1.100:/api/order
            return Mono.just("composite:" + ip + ":" + path);
        };
    }
}

Step 4:配置路由限流规则

yml 复制代码
spring:
  cloud:
    gateway:
      routes:
        # 核心业务接口:限流宽松
        - id: core-service
          uri: lb://core-service
          predicates:
            - Path=/api/order/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 100   # 每秒100个请求
                redis-rate-limiter.burstCapacity: 200   # 突发容量200
                key-resolver: "#{@compositeKeyResolver}"

        # 非核心接口:限流严格
        - id: non-core-service
          uri: lb://non-core-service
          predicates:
            - Path=/api/log/**
          filters:
            - name: RequestRateLimiter
              args:
                redis-rate-limiter.replenishRate: 10    # 每秒10个请求
                redis-rate-limiter.burstCapacity: 20
                key-resolver: "#{@ipKeyResolver}"

3.3 源码深度解析

RequestRateLimiter过滤器的工作原理:

java 复制代码
// Gateway内部核心逻辑(简化版)
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // 1. 通过KeyResolver获取限流Key
    Mono<String> keyMono = keyResolver.resolve(exchange);
    
    // 2. 调用Redis Lua脚本进行令牌桶判断
    return keyMono.flatMap(key -> {
        // 执行的Lua脚本逻辑:
        // - 获取当前时间窗口
        // - 计算已使用的令牌数
        // - 判断是否还有令牌
        // - 返回是否放行
        return redisTemplate.execute(script, keys, args);
    }).flatMap(response -> {
        if (response.isAllowed()) {
            return chain.filter(exchange);  // 放行
        } else {
            return onError(exchange);       // 限流拒绝
        }
    });
}

核心Lua脚本逻辑(Redis中执行的原子操作):

lua 复制代码
-- 令牌桶算法在Redis中的实现(简化)
local tokens_key = KEYS[1]    -- 剩余令牌数
local timestamp_key = KEYS[2] -- 上次更新时间

local rate = tonumber(ARGV[1])  -- 令牌生成速率
local capacity = tonumber(ARGV[2]) -- 桶容量
local now = tonumber(ARGV[3])  -- 当前时间

-- 计算应该新增的令牌数
local last_tokens = tonumber(redis.call('get', tokens_key) or capacity)
local last_refreshed = tonumber(redis.call('get', timestamp_key) or now)
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate / 1000))

-- 判断是否有足够令牌
local allowed = filled_tokens >= 1
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - 1
end

-- 更新Redis
redis.call('setex', tokens_key, 10, new_tokens)
redis.call('setex', timestamp_key, 10, now)

return { allowed, new_tokens }

💡 设计要点 :整个判断过程在Redis中通过Lua脚本完成,保证了原子性,避免了分布式环境下的并发问题。


四、方案二:集成 Sentinel(动态限流)

当限流规则需要频繁调整 、需要可视化监控 、或需要熔断降级时,Sentinel是更好的选择。

4.1 Sentinel vs Redis限流对比

对比维度 Redis RequestRateLimiter Sentinel
规则变更 修改配置重启网关 控制台动态推送,实时生效
限流算法 令牌桶 多种(令牌桶/漏桶/滑动窗口)
熔断降级 ❌ 不支持 ✅ 支持(慢调用/异常比例)
监控面板 ❌ 无 ✅ 丰富的Dashboard
系统自适应保护 ❌ 不支持 ✅ 支持(根据CPU/内存自适应)
学习成本

4.2 集成步骤

Step 1:添加依赖

xml 复制代码
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-sentinel-gateway</artifactId>
</dependency>

Step 2:配置Sentinel控制台

yml 复制代码
spring:
  cloud:
    sentinel:
      transport:
        dashboard: 192.168.1.100:8080  # Sentinel控制台地址
        port: 8719                      # 与控制台通信的端口
      eager: true                       # 提前初始化,懒加载可能导致首次请求异常

Step 3:在Sentinel控制台配置限流规则

访问 http://192.168.1.100:8080,在"网关流控"页面配置:

text 复制代码
资源名: /api/order/**
阈值类型: QPS
单机阈值: 100
流控效果: 快速失败

Step 4:定制限流响应

java 复制代码
@Component
public class SentinelGatewayConfig {

    @PostConstruct
    public void init() {
        // 自定义限流返回结果
        GatewayCallbackManager.setBlockHandler((serverWebExchange, throwable) -> {
            ServerHttpResponse response = serverWebExchange.getResponse();
            response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            
            // 返回JSON格式错误信息
            Map<String, Object> result = new HashMap<>();
            result.put("code", 429);
            result.put("msg", "请求过于频繁,请稍后重试");
            result.put("timestamp", System.currentTimeMillis());
            
            DataBuffer buffer = response.bufferFactory()
                .wrap(JSON.toJSONString(result).getBytes(StandardCharsets.UTF_8));
            return response.writeWith(Mono.just(buffer));
        });
    }
}

4.3 高级特性:熔断降级

Sentinel不仅能限流,还能做熔断降级。当某个API错误率过高时,自动熔断保护下游。

在控制台配置熔断规则:

text 复制代码
资源名: /api/order/**
熔断策略: 慢调用比例
最大RT: 200ms
比例阈值: 0.5
熔断时长: 10s
最小请求数: 5

含义:当5个请求中有50%的响应时间超过200ms时,触发熔断,后续请求直接走降级逻辑,10秒后恢复探测。


五、方案三:自定义限流过滤器(灵活定制)

如果官方方案无法满足业务需求(比如需要自定义限流算法、需要从数据库读取规则等),可以自己实现限流过滤器。

5.1 实现一个自定义限流过滤器

java 复制代码
@Component
@Slf4j
public class CustomRateLimiterFilter implements GlobalFilter, Ordered {

    @Autowired
    private StringRedisTemplate redisTemplate;
    
    // 使用滑动窗口实现限流
    private static final String LUA_SCRIPT = 
        "local key = KEYS[1] " +
        "local window = tonumber(ARGV[1]) " +
        "local limit = tonumber(ARGV[2]) " +
        "local now = tonumber(ARGV[3]) " +
        "local window_start = now - window * 1000 " +
        "redis.call('ZREMRANGEBYSCORE', key, 0, window_start) " +
        "local current = redis.call('ZCARD', key) " +
        "if current < limit then " +
        "    redis.call('ZADD', key, now, now .. '_' .. math.random()) " +
        "    redis.call('EXPIRE', key, window + 1) " +
        "    return 1 " +
        "else " +
        "    return 0 " +
        "end";

    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1. 获取限流Key
        String ip = exchange.getRequest().getRemoteAddress()
            .orElseThrow(() -> new RuntimeException("无法获取IP"))
            .getAddress().getHostAddress();
        String key = "ratelimit:custom:" + ip;
        
        // 2. 执行限流判断(滑动窗口)
        Long windowSeconds = 1L;   // 1秒窗口
        Long maxRequests = 10L;    // 最多10个请求
        
        Long result = redisTemplate.execute(
            new DefaultRedisScript<>(LUA_SCRIPT, Long.class),
            Collections.singletonList(key),
            windowSeconds.toString(),
            maxRequests.toString(),
            String.valueOf(System.currentTimeMillis())
        );
        
        // 3. 判断是否放行
        if (result == null || result == 0) {
            // 限流拒绝
            log.warn("请求被限流,IP: {}", ip);
            exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
            return exchange.getResponse().setComplete();
        }
        
        // 4. 放行请求
        return chain.filter(exchange);
    }

    @Override
    public int getOrder() {
        // 确保在业务路由之前执行
        return -1;
    }
}

5.2 动态加载限流配置(从配置中心)

java 复制代码
@Component
@RefreshScope  // 支持Spring Cloud Config动态刷新
public class DynamicRateLimiterConfig {

    @Value("${rate-limiter.default.qps:100}")
    private Integer defaultQps;
    
    // 也可以从数据库或Nacos读取更细致的配置
    public Integer getQpsForPath(String path) {
        // 从缓存或数据库获取该路径的限流阈值
        return defaultQps;
    }
}

六、生产环境最佳实践

6.1 限流配置建议

场景 replenishRate burstCapacity 说明
核心写接口(下单/支付) 50 100 严格限流,保护数据一致性
核心读接口(商品详情) 500 800 适当放宽,允许高并发读
非核心接口(日志上报) 20 30 严格限流,避免浪费资源
开放API(无需鉴权) 10 20 最严格,防止恶意刷接口

6.2 限流响应处理

被限流的请求,应该返回明确的错误信息,而不是让用户感到困惑。

java 复制代码
@Component
public class RateLimiterErrorHandler {

    @Bean
    public GlobalFilter rateLimiterErrorFilter() {
        return (exchange, chain) -> {
            // 检查是否被限流标记
            if (exchange.getResponse().getStatusCode() == HttpStatus.TOO_MANY_REQUESTS) {
                exchange.getResponse().getHeaders().setContentType(MediaType.APPLICATION_JSON);
                Map<String, Object> error = new HashMap<>();
                error.put("code", 429);
                error.put("message", "请求太频繁,请休息一下再试");
                error.put("retryAfter", 1); // 建议1秒后重试
                
                String body = JSON.toJSONString(error);
                DataBuffer buffer = exchange.getResponse().bufferFactory()
                    .wrap(body.getBytes(StandardCharsets.UTF_8));
                return exchange.getResponse().writeWith(Mono.just(buffer));
            }
            return chain.filter(exchange);
        };
    }
}

6.3 监控告警

限流不是为了"拒绝用户",而是为了让系统更稳定。因此需要建立监控:

java 复制代码
@Component
public class RateLimiterMetrics {

    @Autowired
    private MeterRegistry meterRegistry;
    private Counter rateLimitCounter;

    @PostConstruct
    public void init() {
        rateLimitCounter = Counter.builder("gateway.ratelimit.rejected")
            .description("被限流拒绝的请求数")
            .register(meterRegistry);
    }

    public void recordReject(String key, String path) {
        rateLimitCounter.increment();
        // 按路径维度记录
        Counter.builder("gateway.ratelimit.rejected.bypath")
            .tag("path", path)
            .register(meterRegistry)
            .increment();
    }
}

告警规则建议:

  • 限流拒绝率 > 5% → 告警,可能业务突增
  • 限流拒绝率 > 20% → 严重告警,可能系统容量不足或遭受攻击

6.4 灰度发布时的注意事项

灰度期间,新旧版本流量分配不均,可能导致限流阈值不准确。

解决方案:

yml 复制代码
# 对灰度流量使用独立的限流Key
spring:
  cloud:
    gateway:
      routes:
        - id: gray-service
          uri: lb://gray-service
          predicates:
            - Path=/api/**
            - Header=X-Gray-Tag, gray  # 灰度流量标记
          filters:
            - name: RequestRateLimiter
              args:
                # 灰度流量单独限流,互不影响
                key-resolver: "#{@grayKeyResolver}"
                redis-rate-limiter.replenishRate: 20
                redis-rate-limiter.burstCapacity: 40

七、三种方案对比总结

维度 Redis RequestRateLimiter Sentinel 自定义过滤器
实现难度 ⭐⭐ 简单 ⭐⭐⭐ 中等 ⭐⭐⭐⭐ 复杂
分布式支持 ✅ 原生支持 ✅ 原生支持 ❌ 需要自己实现
动态规则 ❌ 需要重启 ✅ 实时生效 ✅ 需要自己实现
熔断降级 ❌ 不支持 ✅ 完善支持 ❌ 需要自己实现
监控能力 ❌ 较弱 ✅ 强大的Dashboard ❌ 需要自己实现
生态集成 Spring Cloud原生 阿里系生态
推荐场景 大部分通用场景 需要动态规则/熔断能力 极端定制需求

个人推荐:

  1. 新项目快速上线 → 直接用 Redis RequestRateLimiter,够用且简单
  2. 已有Sentinel基础设施 → 使用Sentinel,充分利用其治理能力
  3. 特殊业务需求(如按时间段调整阈值)→ 自定义过滤器 + 配置中心

八、总结

网关限流是微服务架构的第一道安全防线。本文从算法原理出发,到三种落地方案,再到生产实践细节,希望能帮助你构建一个高可用的限流系统。

核心要点回顾:

  1. 算法选型:令牌桶算法能同时兼顾限流精度和突发流量处理
  2. 存储选型:Redis是实现分布式限流的首选,Lua脚本保证原子性
  3. 维度选择:按IP、用户、API路径、组合维度,根据业务场景灵活选择
  4. 监控告警:限流不是终点,建立完善的监控体系才能持续优化
  5. 渐进式演进:从简单的Redis限流起步,复杂后逐步演进到Sentinel

限流不是目的,而是手段。最终目标是在保证系统稳定的前提下,尽可能多地服务合法用户。

相关推荐
IT_陈寒1 小时前
Python的GIL让我以为CPU跑满了,结果...
前端·人工智能·后端
Jiude1 小时前
AI 产品的几种付费方式
后端·架构·openai
Java面试题总结2 小时前
Go 语言大白话入门 6 - 判断与循环
开发语言·后端·golang
hsg773 小时前
简述:Rust、GeoRust、自主研发GIS平台
开发语言·后端·rust
谭光志10 小时前
Vibe Coding 时代,程序员该何去何从
前端·后端·ai编程
GoGeekBaird11 小时前
我用 BeeWeave 跑完了一次完整写作闭环
后端·github
仿生狮子12 小时前
别再说“全栈”了,AI 时代团队只认这 5 种人
前端·人工智能·后端
Reart15 小时前
Leetcode 213.打家劫舍2(内含闲谈,打劫真是技术活,好题,716)
后端·算法
触底反弹15 小时前
🔥 RAG 到底是怎么工作的?掰开揉碎了给你讲明白!
javascript·人工智能·后端