# Spring Cloud Gateway 限流适配多规则的解决方案

# Spring Cloud Gateway 限流适配多规则的解决方案

Author: Suremotoo

·

3 min read

websiteerror429-来源https://twitter.com/_mikystone

首先要说明,本文是使用的 Spring Cloud Gateway 自带的或者称原生的 Redis 限流!

背景

限流作用就不说了,往往都是防止一些恶意请求,无限制请求接口导致服务处理时间过长,继而导致响应延迟,服务阻塞等等,所以会对高频率的一些接口添加限流这样的功能。


通常,我们往往是针对 1 个路由或者说是对 1 个接口进行限流,限流的规则通常是:XXX 路由 XXX 在 XXX 时间内最多允许访问 XXX 次

比如:查询用户信息接口 [路由] 每个用户 [条件] 每秒 [频率时间] 最多支持访问 10 次 [频率最大限制]

举个明白点的例子🌰,我 1 秒内连续请求 11 次 [查询用户信息接口] ,那么第 11 次就应该被拦截,提示请求频繁,相信大家在一些双 11 这样的节日里会遇到过类似情况~

我们换个规则,再举个例子🌰:[查询用户信息接口] 每秒 最多支持访问 100 次~,也就是说不管谁请求,反正 [查询用户信息接口] 1 秒内最大支持访问 100 次请求,超过 100 的都会被拦截~


以上两个例子,都是单独使用,是对 1 个路由指定了 1 个限流的规则,实际业务需求中,1 个路由可能还需要 2 个或者多个规则同时使用。

比如:

[查询用户信息接口] 每个用户 每秒 最多支持访问 10 次 ,这是规则 1,用来限制单个用户的次数

同时,[查询用户信息接口] 每秒 最多支持访问 100 次~,这是规则 2,用来限制这个接口的次数

这个我再举个明白点的例子,假如有 10 个人在同 1 秒来请求 [查询用户信息接口]

前 8 个人都在 1 秒内请求 10 次,(8 个人每个人都不违反规则 1,接口请求总数也不超过 100 次,接口还可以请求 20 次,不违反规则 2)

第 9 个人请求 11 次,(达到规则 1 限流条件,这个人第 11 次请求肯定被拦截,接口请求总数不超过 100 次,接口还可以请求 9 次)

第 10 个人请求 10 次,(不违反规则 1,接口请求为 101 次,总数超过 100 次,达到规则 2 限流条件,所以这个人第 10 次请求肯定被拦截)

当然请求顺序这都是理想状态,实际场景中顺序会有差别~


直接看文字可能有点多,我这里梳理一个对比图 👋:

限流情况梳理.png

既然了解后,那么现在的问题就是:Spring Cloud Gateway 自带的限流默认 1 个路由(或者说是 1 个接口)只能配置 1 个限流规则!本文就是来解决这种问题,让 1 个路由适配多个规则!🤯

Spring Cloud Gateway 提供了一套限流方案的接口,并且也基于 Redis 实现了一套限流方案,这个也就是本文的要着重分析的点!

Spring Cloud Gateway 大致流程熟悉

大致流程图

SpringCloudGateway-01.png

具体流程这里就不说了,我直接说本文的涉及的要点。

当请求进入到网关,网关会根据请求路由来组装对应的过滤器,而我们的限流也是其中的过滤器,Spring Cloud Gateway 自己的实现就是:RequestRateLimiterGatewayFilterFactory ,所以我们要分析其源码,了解它大致干了什么事,我们才好知道有没有办法调整!

平常配置使用回顾

分析前,我们先回顾下平常我们配置限流是怎么配置的。

附: RateLimiterConfig,首先我们定义好限流规则 KeyResolver

RateLimiterConfig.png

然后在 application.yml 配置路由的限流, 示例:

spring:
  cloud:
    gateway:
      routes:
        - id: query_user_info_route
          uri: lb://user-center
          filters:
            - name: RequestRateLimiter
              args:
                # 令牌桶每秒填充平均速率
                redis-rate-limiter.replenishRate: 1
                # 令牌桶的上限
                redis-rate-limiter.burstCapacity: 10
                # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象
          # pathKeyResolver 是根据地址来限流
                key-resolver: "#{@remoteAddrKeyResolver}" # 详情见 RateLimiterConfig

可以看到,过滤器 (filters),我们配置的是 RequestRateLimiter ,这里的 RequestRateLimiter 其实指的就是 RequestRateLimiterGatewayFilterFactory ,只是省略了后面的 GatewayFilterFactory ~

该过滤器的参数有 redis-rate-limiter、key-resolver,说明这两个其实是很重要的属性!

1 个限流规则我们配置 1 个过滤器及属性,那么我想再加一个规则,预计我们会这样做,示例:

diy-filter-yaml-error.png

写的时候还洋洋洒洒~

skrjlg.jpeg

写完后看上没问题,程序也能跑起来,但你会发现实际就只有 1 个生效,下面属性的把上面的覆盖了! 欧了买了噶~

omg.jpeg

是配置了两个一样的过滤器,实际运行的时候,也确实都跑了两次这个过滤器,只是每次取的速率什么的,是相同的~,相当于同一个限流规则,校验了两遍~

好得很.jpeg

具体跑起来效果我就不展示了,接下来我们来正儿八经分析下源码,看看什么情况吧!

RequestRateLimiterGatewayFilterFactory 源码分析

这里仅列出核心代码分析😬

RequestRateLimiterGatewayFilterFactory.png

上述代码中,结合我们从 application.yml 配置中查看,该源码中其实最重要的就是:

RateLimiter :限流算法及实现(实际实现是令牌桶算法,这里先不做深入探究)

KeyResolver :限流关键字 key(这里 key 其实就是我们说的对用户限流、对接口限流,当我们要对 ip 限流时,这个 key 就是请求的 ip)

还有就是 limiter.isAllowed 这个函数,是校验是否达到限流条件的重要方法!

KeyResolver 看上去就不是影响多规则限流的重要因素~,那么我们就直接来看看 RateLimiter ~

RateLimiter 源码分析

打开源码一看,哦是 interface ,我们看看实现类( idea 中点击下图标记📌处即可查看)

RateLimiter.png

发现有两个实现类,一个是抽象类 AbstractRateLimiter,一个是基于 Redis 实现的 RedisRateLimiter,(o゜▽゜)o☆[BINGO!],肯定是 RedisRateLimiter,我们直接打开它~

RateLimiter-Impl.png

RedisRateLimiter 源码(别着急看代码先往下翻 😶)

RedisRateLimiter.png

别看代码多,不要慌!实际上就是基于 Redis 限流是怎么个算法实现的,但是和限流为什么只能有一个规则,好像一点关系都没有🤣,说明不在这里

吓傻了.jpeg

📢📢注意了,但是它 extends AbstractRateLimiter 了,继承了 AbstractRateLimiter 类,我们还是看看这个类吧~

AbstractRateLimiter 源码分析

AbstractRateLimiter 源码

AbstractRateLimiter.png

代码不多,就一个核心方法 onApplicationEvent,参数是个 FilterArgsEvent,看上去是把过滤器的参数 args 都获取出来,再做处理

小提示,看看人家的命名,一看就让人知道大概什么意思,以后大家也注意下命名!

贴一下限流的核心配置示例:

- name: RequestRateLimiter
         args:
           # 令牌桶每秒填充平均速率
           redis-rate-limiter.replenishRate: 1
           # 令牌桶的上限
           redis-rate-limiter.burstCapacity: 10
           # 使用 SpEL 表达式从 Spring 容器中获取 Bean 对象,pathKeyResolver 是根据地址来限流
           key-resolver: "#{@pathKeyResolver}"

捋一捋,这个过滤器的参数 args 就是限流参数,而

RedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config>

那么 onApplicationEvent,应该是把参数对应的 routeConfig 对象初始化出来~,也就是 RedisRateLimiter.Config 的这个 Config 对象,

Config 里就两个属性,也就是限流的重要参数,果然没错~

简单再贴一下 RedisRateLimiter.Config 代码

@Validated
public static class Config {
    @Min(1L)
    private int replenishRate;
    @Min(1L)
    private int burstCapacity = 1;

    public Config() {
    }
    // 省略...
}

最后最后有个 this.getConfig().put(routeId, routeConfig);

这不就是把路由和其对应的限流规则存到一个 Map 里嘛~,盲猜都知道这个 this.getConfig() 是个 Map,可以去 AbstractStatefulConfigurable 代码里看,这里就不展示了~

AbstractRateLimiter<C> extends AbstractStatefulConfigurable<C>

其实看到 this.getConfig().put(routeId, routeConfig); 这里我大概已经知道是什么问题了:我们针对一个 1 路由配置多个限流规则,最终名为 Config 的 Map 里存储的只有 1 个!

说白了就是: Map 里,Key 是 routeId,Value 是限流规则 routeConfig,你的路由 id 是固定的, 即使你有多个 routeConfig,存入 Map 里,后面的 routeConfig 规则把前面的覆盖了~~~

这不就找到问题了嘛🎉🎉🎉🎉

叉会腰.jpeg

改造方案

既然找到问题了,我们就想办法改造它!经过前面的分析,我们应该要改造的就是 onApplicationEvent 方法里的 this.getConfig().put(routeId, routeConfig);

我们应该改造成,放入 Config 里的 Key 不用 RouteId !

用什么呢,我这里 使用 routeId 和 KeyResolver 的 hashcode 组合

改造前再捋清楚,Spring Cloud Gateway 自带的 Redis 限流实现类是 RedisRateLimiter,它继承的抽象类 AbstractRateLimiter,而我们要改造的方法在 AbstractRateLimiter

所以我们重写一个 RedisRateLimiter,重写 onApplicationEvent 方法 !

ok!Just Do It~

自定义 DiyRedisRateLimiter

首先,我们新建 1 个类,叫 DiyRedisRateLimiter,剩下的代码就从 RedisRateLimiter 全部拷贝过来!

然后重写 onApplicationEvent 方法!

DiyRedisRateLimiter 代码:

DiyRedisRateLimiter.png

创建完后,我们再将这个类初始化到 Spring 里

/**
 * Author: Suremotoo
 */
@Configuration
public class RateLimiterConfig {

    /**
     * 使用自定义的限流类
     */
    @Bean
    @Primary
    public DiyRedisRateLimiter diyRedisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
        @Qualifier(DiyRedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript
    , Validator validator) {
        return new DiyRedisRateLimiter(redisTemplate, redisScript, validator);
    }

  // .... 其他 KeyResolver 省略,详情见上文描述中的 RateLimiterConfig
}

这就弄好了,但是注意,我们还没有改造完!

这仅仅是放入 Map 中已经不是 1 个了!但是用的时候呢?还记得前面提到的 isAllowed 方法吗?这个方法是在 RequestRateLimiterGatewayFilterFactory 里的 apply 方法中 ,所以我们还要重写 这里!

复制粘贴.jpeg

自定义 DiyRequestRateLimiterGatewayFilterFactory

首先,我们新建 1 个类,叫 DiyRequestRateLimiterGatewayFilterFactory,继承 RequestRateLimiterGatewayFilterFactory

然后重写 apply 方法!

DiyRequestRateLimiterGatewayFilterFactory 代码示例:

DiyRequestRateLimiterGatewayFilterFactory.png

然后在 application.yml 中使用的时候用自己定义的 DiyRequestRateLimiterGatewayFilterFactory

示例:

diy-filter-yaml.png

终于大功告成~

a.jpeg