ITPub博客

首页 > 数据库 > 数据库开发技术 > 深入源码解析Spring Cloud Gateway限流原理

深入源码解析Spring Cloud Gateway限流原理

原创 数据库开发技术 作者:服务端技术栈 时间:2024-08-31 23:20:06 0 删除 编辑

一、背景

最近,我接手了一个新项目,其中使用了Spring Cloud Gateway技术。项目中的限流功能并没有采用公司统一封装的Sentinel组件,而是直接使用了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脚本的关键逻辑

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来区分请求。

五、总结

源码看完后,限流功能还是比较好理解的,和公司统一封装的Sentinel框架相比,它没有可视化页面配置限流规则的功能,这个需要到配置中心配置限流规则。Spring Cloud Gateway的限流机制通过Redis的Lua脚本实现了一个轻量级的令牌桶算法,并通过配置灵活地控制各个服务的请求速率。通过对源码的分析和实际的使用示例,我们能够更好地理解其工作原理,并在项目中合理配置限流策略。

坚持相信有输入一定要有输出,关注我们一起学习沉淀技术,希望我们的技术能力越来越强。


原文作者:服务端技术栈;如有侵权,请联系管理员删除。

请登录后发表评论 登录
全部评论

注册时间:2019-03-08

  • 博文量
    97
  • 访问量
    41432