深入源码解析Spring Cloud Gateway限流原理最近,我接手了一个新项目,其中使用了Spring Cloud
一、背景
最近,我接手了一个新项目,其中使用了Spring Cloud Gateway
技术。这个项目有些接口的qps比较大,但是项目中的限流功能并没有采用公司统一封装的Sentinel
组件,而是直接使用了Spring Cloud Gateway的限流组件。之前对于Spring Cloud Gateway使用比较少,还是有点头疼哦,他的代码大部分用的响应式
编程,和平时写的代码相比,更难理解
,最近花了很多时间去理解业务代码。。。
为了尽快熟悉并掌握这部分功能的实现原理,我深入研究了Spring Cloud Gateway的限流机制,特别是其核心实现方式。本文将按照有输入必须要有输出
的理念,通过源码分析详细介绍Spring Cloud Gateway
的限流原理。
二、源码导读
在Spring Cloud Gateway
中,限流的关键组件包括RedisRateLimiter
类和RequestRateLimiterGatewayFilterFactory
过滤器工厂。RedisRateLimiter
通过Lua脚本
与Redis
交互,实现令牌桶算法
的限流逻辑,而RequestRateLimiterGatewayFilterFactory
则将该限流功能应用于具体的路由。
三、限流核心源码解析
1、RedisRateLimiter
类解析
RedisRateLimiter
是Spring Cloud Gateway
中实现限流逻辑的核心类。其主要通过令牌桶算法
实现限流,这里详细分析一下它的主要方法isAllowed
。
public Mono<RateLimiter.Response> isAllowed(String routeId, String id) {
// 检查是否已初始化
if (!this.initialized.get()) {
throw new IllegalStateException("RedisRateLimiter is not initialized");
} else {
// 加载配置
Config routeConfig = this.loadConfiguration(routeId);
int replenishRate = routeConfig.getReplenishRate(); // 每秒令牌恢复速率
int burstCapacity = routeConfig.getBurstCapacity(); // 桶的容量,即最多能存多少令牌
try {
// 构建Redis操作的Key
List<String> keys = getKeys(id);
// Lua脚本的参数
List<String> scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 执行Lua脚本,限流判断
Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys, scriptArgs);
return flux.onErrorResume((throwable) -> {
return Flux.just(Arrays.asList(1L, -1L)); // 出错时默认允许请求通过
}).reduce(new ArrayList(), (longs, l) -> {
longs.addAll(l);
return longs;
}).map((results) -> {
boolean allowed = (Long)results.get(0) == 1L; // 判断请求是否被允许
Long tokensLeft = (Long)results.get(1); // 剩余令牌数
RateLimiter.Response response = new RateLimiter.Response(allowed, this.getHeaders(routeConfig, tokensLeft));
if (this.log.isDebugEnabled()) {
this.log.debug("response: " + response);
}
return response;
});
} catch (Exception var9) {
Exception e = var9;
this.log.error("Error determining if user allowed from redis", e);
return Mono.just(new RateLimiter.Response(true, this.getHeaders(routeConfig, -1L))); // 出现异常时允许请求通过
}
}
}
这个方法是整个限流逻辑的核心,负责检查某个请求是否被允许通过。通过加载配置,我们可以获取到replenishRate
(令牌的生成速率)和burstCapacity
(桶的最大容量)。接着,通过构建Redis的Key并传入Lua脚本参数,执行限流判断。如果脚本返回的第一个值为1,则表示允许请求,否则不允许。
2、Lua脚本的关键逻辑
快醒醒喂,重点来了!
同志们,令牌桶的核心是什么呀?
2.1、生成令牌(增)
基于每秒生成令牌数,往桶里增加令牌
2.2、获取令牌(减)
请求打进来后,先获取令牌,看能不能获取到。能获取到,继续向下走。获取不到,拒绝请求。
所以,非常简单
,不管是哪个框架基于令牌桶去做限流,实现这两就行了,gateway老大哥包装在了一个方法里面。
用lua脚本提高性能与安全性
,解决上面两个问题的话,要怎么做了?
1 、生成令牌:核心是需要计算这次请求能生成多少令牌
1、首先计算时间差,把上次获取令牌的时间存储在redis.
key:timestamp_key value:上次获取令牌的时间
时间差:当前时间-上次获取令牌的时间
2、再计算当前时间差能生成多少令牌
根据配置每秒令牌恢复速率去计算redis-rate-limiter.replenishRate: 10 # 每秒生成10个令牌
2、获取令牌:核心是令牌能否分配,并更新令牌数
获取桶内剩余令牌数:key:tokens_key value:桶内剩余令牌数
local last_tokens = tonumber(redis.call("get", tokens_key))
如果请求令牌数>总令牌数,拒绝请求
如果********<*******,允许请求
3、最后,更新Redis中的令牌数和时间戳
总令牌数=原总令牌-申请令牌数
哈哈哈,还没睡着吧,到这可以下课了!看源码可以继续坚持!
Lua脚本在Redis中执行,确保限流操作的原子性。下面是Lua脚本的核心逻辑:
-- 获取上次的令牌数和刷新时间
local last_tokens = tonumber(redis.call("get", tokens_key))
if last_tokens == nil then
last_tokens = capacity
end
local last_refreshed = tonumber(redis.call("get", timestamp_key))
if last_refreshed == nil then
last_refreshed = 0
end
-- 计算从上次到现在的时间差,增加相应的令牌数
local delta = math.max(0, now - last_refreshed)
local filled_tokens = math.min(capacity, last_tokens + (delta * rate))
-- 判断令牌是否足够
local allowed = filled_tokens >= requested
local new_tokens = filled_tokens
local allowed_num = 0
if allowed then
new_tokens = filled_tokens - requested
allowed_num = 1
end
-- 更新Redis中的令牌数和时间戳
redis.call("setex", tokens_key, ttl, new_tokens)
redis.call("setex", timestamp_key, ttl, now)
return { allowed_num, new_tokens }
关键点在于这个Lua
脚本会根据当前时间与上次操作时间的差值,计算应该补充的令牌数,然后判断当前令牌是否足够请求使用。如果足够,则扣减相应的令牌,并返回允许的标志。下面是lua脚本限流逻辑的流程图。
3、RequestRateLimiterGatewayFilterFactory
过滤器工厂
RequestRateLimiterGatewayFilterFactory
是Spring Cloud Gateway的一个过滤器工厂类,用于将限流逻辑应用到指定的路由中。以下是该类的关键实现:
public GatewayFilter apply(Config config) {
// 获取限流的KeyResolver和RateLimiter
KeyResolver resolver = (KeyResolver)this.getOrDefault(config.keyResolver, this.defaultKeyResolver);
RateLimiter<Object> limiter = (RateLimiter)this.getOrDefault(config.rateLimiter, this.defaultRateLimiter);
boolean denyEmpty = (Boolean)this.getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
HttpStatusHolder emptyKeyStatus = HttpStatusHolder.parse((String)this.getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));
return (exchange, chain) -> {
Route route = (Route)exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
return resolver.resolve(exchange).defaultIfEmpty("____EMPTY_KEY__").flatMap((key) -> {
if ("____EMPTY_KEY__".equals(key)) {
if (denyEmpty) {
// 拒绝请求并返回特定的状态码
ServerWebExchangeUtils.setResponseStatus(exchange, emptyKeyStatus);
return exchange.getResponse().setComplete();
} else {
return chain.filter(exchange);
}
} else {
// 判断请求是否被允许
return limiter.isAllowed(route.getId(), key).flatMap((response) -> {
response.getHeaders().forEach((header, value) -> {
exchange.getResponse().getHeaders().add(header, value);
});
if (response.isAllowed()) {
return chain.filter(exchange);
} else {
// 超过限流限制,拒绝请求
ServerWebExchangeUtils.setResponseStatus(exchange, config.getStatusCode());
return exchange.getResponse().setComplete();
}
});
}
});
};
}
这个过滤器会在请求到达时解析出限流Key(如用户ID
或IP
),然后调用RateLimiter
进行限流判断。如果超过限流,则返回相应的状态码并终止请求链,否则允许请求继续执行。
四、使用示例
为了帮助大家理解,下面是一个使用RequestRateLimiterGatewayFilterFactory
的示例配置:
spring:
cloud:
gateway:
routes:
- id: demo_service
uri: http://localhost:8081
predicates:
- Path=/demo/api/**
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 10 # 每秒生成10个令牌
redis-rate-limiter.burstCapacity: 20 # 令牌桶最大容量为20
key-resolver: "#{@userKeyResolver}" # 自定义KeyResolver,根据用户ID进行限流
在这个配置中,我们为/demo/api/**路径的请求配置了限流规则:
replenishRate: 每秒生成10个令牌。
burstCapacity: 令牌桶的最大容量为20个。
key-resolver: 使用自定义的KeyResolver,根据用户ID来区分请求。
五、总结
源码看完后,Spring Cloud Gateway的限流功能还是比较好理解的,和公司统一封装的Sentinel
框架相比,它没有可视化页面
配置限流规则
的功能,这个需要到配置中心配置限流规则。
Spring Cloud Gateway的限流机制通过Redis
的Lua
脚本实现了一个轻量级
的令牌桶
算法,并通过配置灵活地控制
各个服务的请求速率。通过对源码的分析和实际的使用示例,我们能够更好地理解其工作原理,并在项目中合理配置限流策略。
转载自:https://juejin.cn/post/7408775736797315106