1. 为什么需要请求限流?
在分布式系统中,API网关作为所有请求的入口,承担着重要的流量管控职责。想象一下,当你的电商系统突然遭遇恶意刷单,或者某个热点商品引发抢购狂潮时,如果没有限流措施,后端服务很可能会被瞬间涌入的请求压垮。我在去年双十一期间就遇到过这样的情况——某个促销接口由于没有配置限流,导致整个订单服务雪崩,教训深刻。
Spring Cloud Gateway作为Spring Cloud生态中的API网关,提供了强大的限流能力。而Redis凭借其高性能和原子性操作,成为实现限流算法的理想选择。特别是令牌桶算法,它能够应对突发流量,比简单的计数器限流更加灵活实用。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 令牌桶算法原理解析
2.1 算法核心思想
令牌桶算法的运作方式很像游乐园的门票系统:假设有一个桶,里面存放着固定数量的令牌(token)。管理员以恒定速率向桶中添加令牌(比如每秒5个),当桶满时新令牌会被丢弃。每当有请求到来时,必须从桶中获取一个令牌才能继续处理,如果桶中没有令牌,请求就会被拒绝。
这种设计有两个关键特性:
- 允许突发流量:当桶中有足够令牌时,可以一次性处理多个请求
- 长期平均速率受限:添加令牌的速率决定了系统的最大吞吐量
2.2 Redis实现优势
使用Redis实现令牌桶有几个不可替代的优势:
- 原子性操作:Redis的INCR和EXPIRE命令可以保证在高并发下正确计算令牌数量
- 高性能:内存操作比数据库方案快几个数量级
- 分布式一致性:所有网关实例共享同一个Redis,保证集群范围的限流准确
- 丰富的过期策略:可以方便地设置令牌的自动清理
我在实际项目中对比过几种方案,发现基于Redis的实现TPS(每秒事务数)可以达到本地内存方案的80%,但跨节点一致性要强得多。
3. Spring Cloud Gateway集成Redis限流
3.1 基础环境准备
首先确保你的项目包含以下依赖(以Maven为例):
xml复制<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
<version>2025.0.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
然后在application.yml中配置Redis连接:
yaml复制spring:
redis:
host: localhost
port: 6379
password: yourpassword
注意:生产环境一定要配置密码和适当的超时参数,我见过太多因为Redis未授权访问导致的安全事故。
3.2 自定义限流过滤器
创建自定义的GatewayFilter:
java复制public class RedisRateLimiter implements GatewayFilter {
private final RedisTemplate<String, String> redisTemplate;
private final int replenishRate; // 令牌补充速率(每秒)
private final int burstCapacity; // 桶容量
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String routeId = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_PREDICATE_ROUTE_ATTR);
String key = "rate_limiter:" + routeId + ":" + exchange.getRequest().getRemoteAddress();
return redisTemplate.execute(script,
Collections.singletonList(key),
String.valueOf(replenishRate),
String.valueOf(burstCapacity),
String.valueOf(System.currentTimeMillis() / 1000)
).flatMap(allowed -> {
if (Boolean.TRUE.equals(allowed)) {
return chain.filter(exchange);
}
exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS);
return exchange.getResponse().setComplete();
});
}
}
配套的Lua脚本(存放在resources/limiter.lua):
lua复制local tokens_key = KEYS[1]
local timestamp = tonumber(ARGV[3])
local rate = tonumber(ARGV[1])
local capacity = tonumber(ARGV[2])
local now = math.floor(timestamp)
local ttl = math.floor(capacity / rate) * 2
local last_tokens = tonumber(redis.call("get", tokens_key) or capacity)
local last_refreshed = tonumber(redis.call("get", tokens_key..":ts") or now)
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
local allowed = filled_tokens >= 1
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - 1
allowed_num = 1
end
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", tokens_key..":ts", ttl, now)
return allowed_num
这个脚本实现了完整的令牌桶逻辑,包括:
- 计算自上次请求以来应该补充的令牌数
- 处理令牌获取的原子性操作
- 设置合理的key过期时间
3.3 全局配置与路由绑定
在配置类中注册过滤器:
java复制@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service", r -> r.path("/api/users/**")
.filters(f -> f.filter(new RedisRateLimiter(redisTemplate, 10, 20)))
.uri("lb://user-service"))
.build();
}
这里我们为用户服务配置了每秒10个请求的速率,突发容量为20。这意味着:
- 正常情况下每秒最多处理10个请求
- 如果有突发流量,最多可以一次性处理20个请求
- 超过这些限制的请求会收到429状态码
4. 高级配置与优化技巧
4.1 动态限流规则
硬编码的限流值不够灵活,我们可以结合配置中心实现动态调整:
java复制@RefreshScope
@Bean
public RedisRateLimiter redisRateLimiter(RedisTemplate<String, String> redisTemplate,
@Value("${rate.limit.default}") int defaultRate) {
return new RedisRateLimiter(redisTemplate, defaultRate, defaultRate * 2);
}
然后在配置中心(如Nacos)中设置:
properties复制rate.limit.default=5
rate.limit.special.route=20
4.2 多维度限流策略
实际业务中,我们可能需要根据不同维度进行限流:
java复制String key = "limiter:" + routeId
+ ":" + exchange.getRequest().getPath().value()
+ ":" + exchange.getRequest().getHeaders().getFirst("User-Agent")
+ ":" + exchange.getRequest().getQueryParams().getFirst("userId");
常见的维度包括:
- IP地址:防止单一IP攻击
- 用户ID:限制单个用户的请求频率
- 接口路径:不同接口设置不同限流值
- 设备类型:移动端和PC端区别对待
4.3 监控与告警
限流系统需要配套的监控措施:
- 记录被拒绝的请求:
java复制if (!allowed) {
metricsCounter.increment("rate_limit.rejected");
log.warn("Rate limit exceeded for {}", key);
}
-
通过Redis的INFO命令监控内存使用情况
-
配置Prometheus监控指标:
java复制@Bean
MeterRegistryCustomizer<MeterRegistry> metricsCommonTags() {
return registry -> registry.config().commonTags("application", "api-gateway");
}
5. 生产环境踩坑实录
5.1 Redis连接池配置不当
曾经遇到过一个线上问题:在流量高峰期间,网关节点开始大量报超时错误。排查后发现是Redis连接池配置不合理:
yaml复制spring:
redis:
lettuce:
pool:
max-active: 50 # 默认8,根据节点数和QPS调整
max-idle: 20
min-idle: 5
经验公式:每个网关节点的最大连接数 = 预计QPS × 平均响应时间(秒) × 安全系数(1.5)
5.2 时钟不同步问题
在多节点部署时,如果服务器时钟不同步,会导致限流计算不准确。解决方案:
- 部署NTP服务保持时间同步
- 使用Redis的TIME命令获取统一时间戳(修改Lua脚本)
5.3 热key问题
当某个接口突然变得热门时,对应的Redis key会成为热点。我们通过以下方式缓解:
- 添加随机过期时间:ttl = baseTtl + random(0, 5)
- 使用Redis集群分散压力
- 本地二级缓存(牺牲一定一致性)
5.4 误杀正常流量
过于激进的限流会误伤正常用户。我们采用的渐进式策略:
- 先记录超限请求但不拦截(日志模式)
- 分析日志确定合理阈值
- 逐步实施限流并观察效果
- 对API进行分级,核心接口放宽限制
6. 性能测试与调优
6.1 基准测试结果
使用JMeter对网关进行压测(单节点4核8G,Redis单实例):
| 场景 | TPS | 平均延迟 | 错误率 |
|---|---|---|---|
| 无限流 | 12500 | 12ms | 0% |
| Redis限流 | 9800 | 15ms | 0.2% |
| 本地限流 | 11800 | 13ms | 0.1% |
可以看到Redis方案带来了约20%的性能损耗,但在分布式一致性上是必须的。
6.2 关键优化点
- 使用Redis Pipeline批量执行命令
- 将Lua脚本预加载到Redis:
script load - 调整Redis内存分配策略:
vm.overcommit_memory=1 - 为Redis配置持久化,防止重启后限流状态丢失
6.3 容量规划建议
根据我们的经验,建议如下配置比例:
- 每个网关节点:4核8G,处理约8000 TPS
- Redis实例:每10000 TPS需要1个CPU核心
- 网络带宽:每1000 TPS需要约5Mbps
对于百万级QPS的系统,应该:
- 部署多个网关集群按业务划分
- 使用Redis Cluster分片
- 实施多级限流(网关层+服务层)
