Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护
【摘要】 Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护 一、背景:为什么网关需要令牌桶限流网关作为流量的第一入口,在秒杀、大促或爬虫突袭等场景下,瞬时 QPS 可能飙升至日常十倍甚至百倍。固定窗口/滑动窗口计数器无法应对突发尖峰,而 令牌桶(Token Bucket) 可以:平滑日常流量:匀速向桶中补充令牌,保障后端负载稳定;容忍可控突发:桶内可累积令牌,突发请求只要...
Spring Cloud Gateway生产级限流:基于令牌桶的突发流量防护
一、背景:为什么网关需要令牌桶限流
网关作为流量的第一入口,在秒杀、大促或爬虫突袭等场景下,瞬时 QPS 可能飙升至日常十倍甚至百倍。
固定窗口/滑动窗口计数器无法应对突发尖峰,而 令牌桶(Token Bucket) 可以:
- 平滑日常流量:匀速向桶中补充令牌,保障后端负载稳定;
- 容忍可控突发:桶内可累积令牌,突发请求只要桶里还有令牌即可快速通过;
- 分布式安全:借助 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)
- 监听
rate-limit.yaml
配置变化; - 调用
VipRateLimiter#refreshConfig()
更新内存速率; - 无需重启网关,秒级生效。
六、最佳实践与踩坑总结
场景 | 建议值 | 说明 |
---|---|---|
日常峰值 | 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 点抢购”或“恶意爬虫”时,只需在配置中心改两行数字,即可让网关优雅地 挡住流量洪峰,守护后端服务稳定。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱:
cloudbbs@huaweicloud.com
- 点赞
- 收藏
- 关注作者
评论(0)