Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护

举报
江南清风起 发表于 2025/07/24 18:54:23 2025/07/24
【摘要】 Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护 一、背景:为什么网关需要令牌桶限流网关作为流量的第一入口,在秒杀、大促或爬虫突袭等场景下,瞬时 QPS 可能飙升至日常十倍甚至百倍。固定窗口/滑动窗口计数器无法应对突发尖峰,而 令牌桶(Token Bucket) 可以:平滑日常流量:匀速向桶中补充令牌,保障后端负载稳定;容忍可控突发:桶内可累积令牌,突发请求只要...

Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护


一、背景:为什么网关需要令牌桶限流

网关作为流量的第一入口,在秒杀、大促或爬虫突袭等场景下,瞬时 QPS 可能飙升至日常十倍甚至百倍。
固定窗口/滑动窗口计数器无法应对突发尖峰,而 令牌桶(Token Bucket) 可以:

  1. 平滑日常流量:匀速向桶中补充令牌,保障后端负载稳定;
  2. 容忍可控突发:桶内可累积令牌,突发请求只要桶里还有令牌即可快速通过;
  3. 分布式安全:借助 Redis + Lua 脚本实现集群级精准限流,避免单机 Guava 带来的不一致。

二、令牌桶原理与 Spring Cloud Gateway 实现总览

2.1 算法核心

capacity  : 桶最大令牌数
rate      : 每秒补充令牌数
requested : 每次请求消耗的令牌数

tokens ≥ requested 时请求通过,否则返回 HTTP 429
补充公式(Lua 伪代码):

local added_tokens = rate * (now - last_timestamp)
tokens = math.min(capacity, tokens + added_tokens)

2.2 Spring Cloud Gateway 的落地方案

组件 作用
RequestRateLimiter Filter 官方过滤器,封装 Redis + Lua
RedisRateLimiter 真正执行 Lua 脚本
KeyResolver 决定限流 Key(IP、用户、URL)
Lua 脚本 保证 get & set 原子操作

三、生产级配置:快速起步

3.1 Maven 依赖

<!-- spring-boot 3.x -->
<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>

3.2 application.yml(最小可运行)

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 32
  cloud:
    gateway:
      routes:
        - id: order-service
          uri: lb://order
          predicates:
            - Path=/order/**
          filters:
            - name: RequestRateLimiter
              args:
                # 核心参数
                redis-rate-limiter.replenishRate: 100   # 每秒补充 100 个
                redis-rate-limiter.burstCapacity: 300   # 桶容量 300,允许 3 倍突发
                redis-rate-limiter.requestedTokens: 1   # 每个请求消耗 1 个
                key-resolver: "#{@ipKeyResolver}"       # SpEL 取 Bean

3.3 KeyResolver Bean(按 IP 限流)

@Bean
public KeyResolver ipKeyResolver() {
    return exchange -> Mono.just(
        Objects.requireNonNull(
            exchange.getRequest()
                    .getRemoteAddress())
               .getAddress()
               .getHostAddress()
    );
}

启动后,连续刷 /order/1 300 次立即成功,第 301 次返回 429 Too Many Requests等待 1 秒后继续恢复 100 QPS。


四、高阶实战:自定义 RedisRateLimiter 支持动态热点

官方 RedisRateLimiter 参数写死,生产需要 实时调整多维度阶梯限流(VIP 用户桶更大)。

4.1 设计目标

维度 普通用户 VIP 用户
replenish 100 200
burst 300 600

4.2 继承并实现 ReactiveRateLimiter

@Component("vipRateLimiter")
public class VipRateLimiter implements ReactiveRateLimiter<VipRateLimiter.Config> {

    private final RedisScript<List<Long>> script;
    private final ReactiveStringRedisTemplate redis;

    public VipRateLimiter(ReactiveStringRedisTemplate redis) {
        this.redis = redis;
        this.script = new DefaultRedisScript<>(
            new ClassPathResource("META-INF/scripts/vip_rate_limiter.lua"),
            List.class
        );
    }

    @Override
    public Mono<Response> isAllowed(String routeId, String id) {
        // id = userId#ip 拼接
        String[] split = id.split("#");
        String userId = split[0];
        String key = "rate:vip:" + userId;

        // 根据用户标签动态取配置
        Config cfg = getUserConfig(userId);

        List<String> keys = List.of(key);
        List<String> args = List.of(
            String.valueOf(cfg.getReplenishRate()),
            String.valueOf(cfg.getBurstCapacity()),
            String.valueOf(Instant.now().getEpochSecond()),
            "1"
        );

        return redis.execute(script, keys, args)
                    .map(result -> new Response(
                        result.get(0) == 1L,
                        Map.of(
                            "X-RateLimit-Remaining", result.get(1).toString(),
                            "X-RateLimit-Replenish", String.valueOf(cfg.getReplenishRate())
                        )
                    ));
    }

    private Config getUserConfig(String userId) {
        // 可接入配置中心 Apollo/Nacos
        return "vip".equals(userTagService.getTag(userId))
                ? new Config(200, 600)
                : new Config(100, 300);
    }

    @Data
    @AllArgsConstructor
    public static class Config {
        private int replenishRate;
        private int burstCapacity;
    }
}

4.3 自定义 Lua(vip_rate_limiter.lua)

-- KEYS[1] = key
-- ARGV[1] = replenishRate
-- ARGV[2] = burstCapacity
-- ARGV[3] = now
-- ARGV[4] = requested

local key       = KEYS[1]
local rate      = tonumber(ARGV[1])
local capacity  = tonumber(ARGV[2])
local now       = tonumber(ARGV[3])
local requested = tonumber(ARGV[4])

local tokens_key     = key .. ":tokens"
local timestamp_key  = key .. ":ts"

local last_tokens = redis.call("GET", tokens_key)
if last_tokens == false then
    last_tokens = capacity
end

local last_refreshed = redis.call("GET", timestamp_key)
if last_refreshed == false then
    last_refreshed = 0
end

local delta = math.max(0, now - tonumber(last_refreshed))
local filled_tokens = math.min(capacity, tonumber(last_tokens) + delta * rate)
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
if allowed then
    new_tokens = filled_tokens - requested
end

redis.call("SET", tokens_key, new_tokens, "EX", math.floor(capacity / rate * 2))
redis.call("SET", timestamp_key, now, "EX", math.floor(capacity / rate * 2))

return { allowed and 1 or 0, new_tokens }

4.4 路由配置

filters:
  - name: RequestRateLimiter
    args:
      rate-limiter: "#{@vipRateLimiter}"
      key-resolver: "#{@userIpKeyResolver}"

五、可观测与运维:指标、报警、热刷新

5.1 暴露 actuator 端点

management:
  endpoints.web.exposure.include: gateway,metrics,prometheus

Prometheus 抓取指标:

# HELP spring_cloud_gateway_requests_seconds  
spring_cloud_gateway_requests_seconds_count{route_id="order-service",status="429"} 3245

5.2 Grafana 面板关键图

Panel PromQL 示例
被限流 QPS rate(spring_cloud_gateway_requests_seconds_count{status="429"}[1m])
令牌桶剩余数 avg_over_time(gateway_tokens_left[1m])
热点用户 TOP10 topk(10, increase(gateway_requests_total[5m]))

5.3 动态参数热刷新(Nacos)

  1. 监听 rate-limit.yaml 配置变化;
  2. 调用 VipRateLimiter#refreshConfig() 更新内存速率;
  3. 无需重启网关,秒级生效。

六、最佳实践与踩坑总结

场景 建议值 说明
日常峰值 burst = 2~3 × replenish 兼顾平滑与突发
大促秒杀 burst = 5~10 × replenish 短时间全部放完,后端做好队列/缓存
Lua 脚本超时 设置 spring.redis.timeout=500ms Redis 抖动时快速失败,避免网关夯死
集群漂移 开启 lettuce.cluster.refresh Redis Cluster slot 迁移导致 MOVED
本地测试 使用 embedded-redis 单元测试零依赖

七、小结

Spring Cloud Gateway 通过 Redis + Lua 令牌桶 提供了生产级、低延迟、分布式限流能力。
本文从快速配置到动态热点、再到可观测与运维,给出了一条完整落地路径。
当你下一次面对“0 点抢购”或“恶意爬虫”时,只需在配置中心改两行数字,即可让网关优雅地 挡住流量洪峰,守护后端服务稳定。

image.png

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。