likes
comments
collection
share

深入源码解析Spring Cloud Gateway限流原理最近,我接手了一个新项目,其中使用了Spring Cloud

作者站长头像
站长
· 阅读数 21

一、背景

最近,我接手了一个新项目,其中使用了Spring Cloud Gateway技术。这个项目有些接口的qps比较大,但是项目中的限流功能并没有采用公司统一封装的Sentinel组件,而是直接使用了Spring Cloud Gateway的限流组件。之前对于Spring Cloud Gateway使用比较少,还是有点头疼哦,他的代码大部分用的响应式编程,和平时写的代码相比,更难理解,最近花了很多时间去理解业务代码。。。

深入源码解析Spring Cloud Gateway限流原理最近,我接手了一个新项目,其中使用了Spring Cloud

为了尽快熟悉并掌握这部分功能的实现原理,我深入研究了Spring Cloud Gateway的限流机制,特别是其核心实现方式。本文将按照有输入必须要有输出的理念,通过源码分析详细介绍Spring Cloud Gateway的限流原理。

二、源码导读

Spring Cloud Gateway中,限流的关键组件包括RedisRateLimiter类和RequestRateLimiterGatewayFilterFactory过滤器工厂。RedisRateLimiter通过Lua脚本Redis交互,实现令牌桶算法的限流逻辑,而RequestRateLimiterGatewayFilterFactory则将该限流功能应用于具体的路由。

三、限流核心源码解析

1、RedisRateLimiter类解析

RedisRateLimiterSpring 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脚本的关键逻辑

快醒醒喂,重点来了!

深入源码解析Spring Cloud Gateway限流原理最近,我接手了一个新项目,其中使用了Spring Cloud

同志们,令牌桶的核心是什么呀?

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脚本限流逻辑的流程图。

深入源码解析Spring Cloud Gateway限流原理最近,我接手了一个新项目,其中使用了Spring Cloud

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

转载自:https://juejin.cn/post/7408775736797315106
评论
请登录