前言
在SpringCloudGateway中官方默认提供了基于Redis的分布式限流方案,对于大部分的场景开箱即用。但实际应用场景下,针对不同的业务场景可能需要进行定制化扩展,此时很有必要了解其工作原理,从而更加快速有效的实现自定义扩展。
正文
此部分将通过3个层面逐步展开:
- Redis分布式限流的核心组件;
- 如何配置路由;
- 如何处理请求;
- 如何刷新路由配置;
Redis分布式限流的核心组件
既然是Gateway模块的源码分析,根据springboot源码分析的套路,从GatewayAutoConfiguration类着手逐步展开,在GatewayAutoConfiguration类中能够找到如下bean实例的注册
@Bean(name = PrincipalNameKeyResolver.BEAN_NAME)
@ConditionalOnBean(RateLimiter.class)
public PrincipalNameKeyResolver principalNameKeyResolver() {
return new PrincipalNameKeyResolver();
}
@Bean
@ConditionalOnBean({
RateLimiter.class, KeyResolver.class})
public RequestRateLimiterGatewayFilterFactory requestRateLimiterGatewayFilterFactory(RateLimiter rateLimiter, PrincipalNameKeyResolver resolver) {
return new RequestRateLimiterGatewayFilterFactory(rateLimiter, resolver);
}
其中
- PrincipalNameKeyResolver 将作为默认的 KeyResolver 实现,其作用于redis存储的限流键key定义;
- RequestRateLimiterGatewayFilterFactory 请求限流网关过滤器工厂类,其会默认注入已经定义的 RateLimiter 实例和 PrincipalNameKeyResolver 实例,此处说明 PrincipalNameKeyResolver 作为了默认的 KeyResolver 实现。
不难发现两个bean实例的注册均依赖于 RateLimiter 实例,该接口定义了判断是否能够放行的isAllowed方法,如下:
public interface RateLimiter<C> extends StatefulConfigurable<C> {
Mono<Response> isAllowed(String routeId, String id);
.....
}
在默认配置中,可以在 GatewayRedisAutoConfiguration类中找到如下其Bean实例的默认装配,目前SpringCloudGateway分布式限流官方提供的正是基于redis的实现,如下
@Bean
@ConditionalOnMissingBean
public RedisRateLimiter redisRateLimiter(ReactiveRedisTemplate<String, String> redisTemplate,
@Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
Validator validator) {
return new RedisRateLimiter(redisTemplate, redisScript, validator);
}
RedisRateLimiter 实例通过 @ConditionalOnMissingBean实现了条件注入,并不会被强制注入,其提供了自定义扩展的可能性。当前Bean实例依赖注入的 RedisScript实例,其指定了具体执行的lua脚本路径,
@Bean
@SuppressWarnings("unchecked")
public RedisScript redisRequestRateLimiterScript() {
DefaultRedisScript redisScript = new DefaultRedisScript<>();
redisScript.setScriptSource(new ResourceScriptSource(new ClassPathResource("META-INF/scripts/request_rate_limiter.lua")));
redisScript.setResultType(List.class);
return redisScript;
}
该脚本已经在对应的jar包中可以直接查看,其默认采用的是令牌桶算法。需要注意的是该bean实例并不是条件注册的,而是默认强制注册。此时如果我们需要对脚本进行简单的调整,可以添加一个新的 RedisScript 实例,同时重新注册 RedisRateLimiter 实例,并重新指定其依赖注入的RedisScript实例为定义的新实例即可。
小节:
到这里基本已经清楚SpringCloudGateway基于Redis实现的分布式限流的核心组件以及对应的实现:
- RequestRateLimiterGatewayFilterFactory;
- KeyResolver:PrincipalNameKeyResolver;
- RateLimiter:RedisRateLimiter;
- RedisScript :META-INF/scripts/request_rate_limiter.lua。
如何配置路由
Gateway中的限流目前是针对每个路由单独定义的,在了解如何针对每个路由定制化限流参数之前,需要先了解Gateway中是如何配置路由定位器的,从一个简单的application.yaml
配置角度入手,其定义如下:
spring:
cloud:
gateway:
routes:
- id: consumer-service
uri: http://127.0.0.1:8081
predicates:
- Path=/consumer-service/**
filters:
- name: RequestRateLimiter
args:
key-resolver: "#{@userKeyResolver}"
redis-rate-limiter.replenishRate: 5
redis-rate-limiter.burstCapacity: 10
- RewritePath=/consumer-service/(?<segment>.*), /$\{
segment}
其中明确指定将采用限流过滤器 RequestRateLimiter并配置了3个主要参数。
此时再次把焦点放在 GatewayAutoConfiguration类,根据spring.cloud.gateway
前缀设定,上述 application.yaml中的配置项将绑定到 GatewayProperties实例中,
@Bean
public GatewayProperties gatewayProperties() {
return new GatewayProperties();
}
根据 GatewayProperties中的路由配置信息,将生成基于properties的路由定义定位器 PropertiesRouteDefinitionLocator
@Bean
@ConditionalOnMissingBean
public PropertiesRouteDefinitionLocator propertiesRouteDefinitionLocator(GatewayProperties properties) {
return new PropertiesRouteDefinitionLocator(properties);
}
默认情况下,系统还会注入一个基于内存的路由定义实例,如下 InMemoryRouteDefinitionRepository
@Bean
@ConditionalOnMissingBean(RouteDefinitionRepository.class)
public InMemoryRouteDefinitionRepository inMemoryRouteDefinitionRepository() {
return new InMemoryRouteDefinitionRepository();
}
在实际开发中可以定义多个路由定义定位器(此部分也是一个常规的扩展点,比如通过DB获取路由定义等),并通过 CompositeRouteDefinitionLocator将所有的路由定义定位器信息进行组合合并,
@Bean
@Primary
public RouteDefinitionLocator routeDefinitionLocator(List<RouteDefinitionLocator> routeDefinitionLocators) {
return new CompositeRouteDefinitionLocator(Flux.fromIterable(routeDefinitionLocators));
}
在Debug模式下可以看到 routeDefinitionLocators包含了上述两个路由定义实例,如下