美文网首页
Spring Cloud Gateway 限流适配多规则的解决方

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

作者: Suremotoo | 来源:发表于2021-07-12 13:53 被阅读0次
    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 次请求肯定被拦截)

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


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

    限流梳理图

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

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

    Spring Cloud Gateway 大致流程熟悉

    大致流程图

    SpringCloudGateway-01.png

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

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

    平常配置使用回顾

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

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


    RateLimiterConfig

    然后在 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

    写的时候还洋洋洒洒~

    skrjlg.jpeg

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

    omg.jpeg

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

    好得很

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

    RequestRateLimiterGatewayFilterFactory 源码分析

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

    RequestRateLimiterGatewayFilterFactory

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

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

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

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

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

    RateLimiter 源码分析

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

    RateLimiter

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

    RateLimiter-Impl.png

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

    RedisRateLimiter 源码

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

    吓傻了

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

    AbstractRateLimiter 源码分析

    AbstractRateLimiter 源码

    AbstractRateLimiter

    代码不多,就一个核心方法 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

    创建完后,我们再将这个类初始化到 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 个了!但是用的时候呢?还记得前面提到的 isAllow 方法吗?这个方法是在 RequestRateLimiterGatewayFilterFactory 里的 apply 方法中 ,所以我们还要重写 这里!

    复制粘贴

    自定义 DiyRequestRateLimiterGatewayFilterFactory

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

    然后重写 apply 方法!

    DiyRequestRateLimiterGatewayFilterFactory 代码示例:

    DiyRequestRateLimiterGatewayFilterFactory

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

    示例:

    diy-filter-yaml

    终于大功告成~

    🎉 附: 精美 PDF 版本

    PDF 宣传图

    由于排版和文件管理的原因,🤩🤩🤩 关注我的公众号: 浆果捕鼠草
    发送关键字:限流多规则方案

    即可获得 本文高清精美 PDF 版本 🎉🎉🎉🎉!

    相关文章

      网友评论

          本文标题:Spring Cloud Gateway 限流适配多规则的解决方

          本文链接:https://www.haomeiwen.com/subject/vbnhpltx.html