大家也可以关注我的公众号:浆果捕鼠草,文章也会同步更新,当然,公众号还会有一些资源可以分享给大家~
首先要说明,本文是使用的 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 大致流程熟悉
大致流程图
具体流程这里就不说了,我直接说本文的涉及的要点。
当请求进入到网关,网关会根据请求路由来组装对应的过滤器,而我们的限流也是其中的过滤器,Spring Cloud Gateway 自己的实现就是:RequestRateLimiterGatewayFilterFactory ,所以我们要分析其源码,了解它大致干了什么事,我们才好知道有没有办法调整!
平常配置使用回顾
分析前,我们先回顾下平常我们配置限流是怎么配置的。
附: RateLimiterConfig,首先我们定义好限流规则 KeyResolver
然后在 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 个过滤器及属性,那么我想再加一个规则,预计我们会这样做,示例:
写的时候还洋洋洒洒~
写完后看上没问题,程序也能跑起来,但你会发现实际就只有 1 个生效,下面属性的把上面的覆盖了! 欧了买了噶~
是配置了两个一样的过滤器,实际运行的时候,也确实都跑了两次这个过滤器,只是每次取的速率什么的,是相同的!相当于同一个限流规则,校验了两遍~
具体跑起来效果我就不展示了,接下来我们来正儿八经分析下源码,看看什么情况吧!
RequestRateLimiterGatewayFilterFactory 源码分析
这里仅列出核心代码分析
上述代码中,结合我们从 application.yml 配置中查看,该源码中其实最重要的就是:
RateLimiter :限流算法及实现(实际实现是令牌桶算法,这里先不做深入探究)
KeyResolver :限流关键字 key(这里 key 其实就是我们说的对用户限流、对接口限流,当我们要对 ip 限流时,这个 key 就是请求的 ip)
还有就是 limiter.isAllowed
这个函数,是校验是否达到限流条件的重要方法!
KeyResolver 看上去就不是影响多规则限流的重要因素~,那么我们就直接来看看 RateLimiter ~
RateLimiter 源码分析
打开源码一看,哦是 interface ,我们看看实现类( idea 中点击下图标记处即可查看)
发现有两个实现类,一个是抽象类 AbstractRateLimiter
,一个是基于 Redis 实现的 RedisRateLimiter
,(o゜▽゜)o☆[BINGO!],肯定是 RedisRateLimiter
,我们直接打开它~
RedisRateLimiter
源码(别着急看代码,先往下翻 )
别看代码多,不要慌!实际上就是基于 Redis 限流是怎么个算法实现的,但是和限流为什么只能有一个规则,好像一点关系都没有�,说明不在这里
注意了,但是它 extends 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
那么 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 extends AbstractStatefulConfigurable
其实看到 this.getConfig().put(routeId, routeConfig);
这里我大概已经知道是什么问题了:我们针对一个 1 路由配置多个限流规则,最终名为 Config 的 Map 里存储的只有 1 个!
说白了就是: Map 里,Key 是 routeId,Value 是限流规则 routeConfig,你的路由 id 是固定的, 即使你有多个 routeConfig,存入 Map 里,后面的 routeConfig 规则把前面的覆盖了~~~
这不就找到问题了嘛
改造方案
既然找到问题了,我们就想办法改造它!经过前面的分析,我们应该要改造的就是 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
代码:
创建完后,我们再将这个类初始化到 Spring 里
/**
* Author: Suremotoo
*/
@Configuration
public class RateLimiterConfig {
/**
* 使用自定义的限流类
*/
@Bean
@Primary
public DiyRedisRateLimiter diyRedisRateLimiter(ReactiveRedisTemplate redisTemplate,
@Qualifier(DiyRedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript> redisScript
, Validator validator) {
return new DiyRedisRateLimiter(redisTemplate, redisScript, validator);
}
// .... 其他 KeyResolver 省略,详情见上文描述中的 RateLimiterConfig
}
这就弄好了,但是注意,我们还没有改造完!
这仅仅是放入 Map 中已经不是 1 个了!但是用的时候呢?还记得前面提到的 isAllow 方法吗?这个方法是在 RequestRateLimiterGatewayFilterFactory
里的 apply
方法中 ,所以我们还要重写 这里!
自定义 DiyRequestRateLimiterGatewayFilterFactory
首先,我们新建 1 个类,叫 DiyRequestRateLimiterGatewayFilterFactory
,继承 RequestRateLimiterGatewayFilterFactory
然后重写 apply
方法!
DiyRequestRateLimiterGatewayFilterFactory
代码示例:
然后在 application.yml 中使用的时候用自己定义的 DiyRequestRateLimiterGatewayFilterFactory
示例:
终于大功告成~
附: 精美 PDF 版本
由于排版和文件管理的原因, 关注我的公众号: 浆果捕鼠草
发送关键字:限流多规则方案即可获得 本文高清精美 PDF 版本 !