• 作者:老汪软件技巧
  • 发表时间:2024-09-01 07:01
  • 浏览量:

一、背景

最近,我接手了一个新项目,其中使用了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 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 keys = getKeys(id);
// Lua脚本的参数
List scriptArgs = Arrays.asList(replenishRate + "", burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
// 执行Lua脚本,限流判断
Flux> 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脚本的关键逻辑

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 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脚本实现了一个轻量级的令牌桶算法,并通过配置灵活地控制各个服务的请求速率。通过对源码的分析和实际的使用示例,我们能够更好地理解其工作原理,并在项目中合理配置限流策略。