Spring Cloud Gateway实现限流
背景
切换为spring cloud gateway时,系统定制开发需要重新实现限流逻辑。系统定制开发本文主要整理了spring cloud gateway系统定制开发中如何实现限流。
zuul中的限流
之前zuul系统定制开发的限流是通过guava系统定制开发提供的令牌桶算法实现的,通过一个全局的过滤器,对所有经过的请求,以IP地址作区分进行限流。
引入guava依赖:
<dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId></dependency>
- 1
- 2
- 3
- 4
具体代码案例:
/** * 自定义过滤器 * * @author yuanzhihao * @since 2022/4/27 */@Component@Slf4jpublic class RequestRateLimitFilter implements Filter { private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder .newBuilder() .maximumSize(1000) .expireAfterAccess(1, TimeUnit.HOURS) .build(); private static final double DEFAULT_PERMITS_PER_SECOND = 1; // 令牌桶每秒填充速率 @SneakyThrows @Override public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException { String remoteAddr = servletRequest.getRemoteAddr(); RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(DEFAULT_PERMITS_PER_SECOND)); if (rateLimiter.tryAcquire()) { filterChain.doFilter(servletRequest, servletResponse); } else { ((HttpServletResponse) servletResponse).setStatus(HttpStatus.TOO_MANY_REQUESTS.value()); servletResponse.setContentType("application/json;charset=UTF-8"); servletResponse.getWriter().write("Too Many Request!!!"); } }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
Spring Cloud Gateway实现限流
编写自定义的限流过滤器
参考zuul中限流方法,可以很容易的编写一个全局过滤器来进行限流,具体代码:
/** * 自定义过滤器 * * @author yuanzhihao * @since 2022/4/27 */@Component@Slf4j@Order(-1)public class RequestRateLimitFilter implements GlobalFilter { private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder .newBuilder() .maximumSize(1000) .expireAfterAccess(1, TimeUnit.HOURS) .build(); private static final double DEFAULT_PERMITS_PER_SECOND = 1; // 令牌桶每秒填充速率 @SneakyThrows @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String remoteAddr = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress(); RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(DEFAULT_PERMITS_PER_SECOND)); if (rateLimiter.tryAcquire()) { return chain.filter(exchange); } ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); DataBuffer dataBuffer = response.bufferFactory().wrap("Too Many Request!!!".getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(dataBuffer)); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
不过这种限流的粒度非常大,对于所有的请求都进行了限流,不能进行定制化的限流。之前博客里面整理过gatewayFilters局部过滤器的用法,这边可以参考进行限流过滤器的编写。
贴一下案例代码:
/** * 自定义局部限流 * * @author yuanzhihao * @since 2022/4/27 */@Componentpublic class CustomRequestRateLimitGatewayFilterFactory extends AbstractGatewayFilterFactory<CustomRequestRateLimitGatewayFilterFactory.Config> { public CustomRequestRateLimitGatewayFilterFactory() { super(Config.class); } private static final Cache<String, RateLimiter> RATE_LIMITER_CACHE = CacheBuilder .newBuilder() .maximumSize(1000) .expireAfterAccess(1, TimeUnit.HOURS) .build(); @Override public GatewayFilter apply(Config config) { return new GatewayFilter() { @SneakyThrows @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String remoteAddr = Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress(); RateLimiter rateLimiter = RATE_LIMITER_CACHE.get(remoteAddr, () -> RateLimiter.create(Double.parseDouble(config.getPermitsPerSecond()))); if (rateLimiter.tryAcquire()) { return chain.filter(exchange); } ServerHttpResponse response = exchange.getResponse(); response.setStatusCode(HttpStatus.TOO_MANY_REQUESTS); response.getHeaders().add("Content-Type", "application/json;charset=UTF-8"); DataBuffer dataBuffer = response.bufferFactory().wrap("Too Many Request!!!".getBytes(StandardCharsets.UTF_8)); return response.writeWith(Mono.just(dataBuffer)); } }; } @Override public List<String> shortcutFieldOrder() { return Collections.singletonList("permitsPerSecond"); } @Data public static class Config { private String permitsPerSecond; // 令牌桶每秒填充速率 }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
对应请求路由生效过滤器:
- id: server1 uri: lb://eureka-server1 predicates: - Path=/server1/hello filters: - CustomRequestRateLimit=1
- 1
- 2
- 3
- 4
- 5
- 6
Spring Cloud Gateway自实现的限流过滤器
spring cloud gateway里面也提供了一个自实现的限流过滤器org.springframework.cloud.gateway.filter.factory.RequestRateLimiterGatewayFilterFactory,这个过滤器里面有两个参数,一个是KeyResolver,这个参数可以动态的指定限流的一些key(个人理解,这边还是详细参考下官方文档~~~),比如这个key可以是访问的IP。
还有一个是RateLimiter,这个参数是具体的限流策略,在spring cloud gateway里面,它的默认实现是RedisRateLimiter,它采用的也是。
首先我们实现KeyResolver接口,指定限流的key是访问的IP地址:
/** * 根据ip地址进行限流 * * @author yuanzhihao * @since 2022/4/27 */@Componentpublic class HostAddrKeyResolver implements KeyResolver { @Override public Mono<String> resolve(ServerWebExchange exchange) { return Mono.just(Objects.requireNonNull(exchange.getRequest().getRemoteAddress()).getAddress().getHostAddress()); }}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
添加spring-boot-starter-data-redis-reactive依赖,使用RedisRateLimiter限流:
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId></dependency>
- 1
- 2
- 3
- 4
配置文件中添加redis和限流的配置信息:
redis: host: 127.0.0.1 port: 6379
- 1
- 2
- 3
- id: server2 uri: lb://eureka-server1 predicates: - Path=/server1/twoDog filters: - name: RequestRateLimiter args: key-resolver: "#{@hostAddrKeyResolver}" redis-rate-limiter.replenishRate: 1 # 令牌桶填充的速率 秒为单位 redis-rate-limiter.burstCapacity: 1 # 令牌桶总容量 redis-rate-limiter.requestedTokens: 1 # 每次请求获取的令牌数
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
这边的参数表示填充的速率是1/s,桶的总容量也为1,每次请求获取一个令牌。也就是一秒只允许一次请求。测试生效:
结语
最后还是倾向于使用自定义的限流,他不需要引入redis组件,而且也可以自己重写响应到页面,更加灵活一点。
参考地址:
代码地址: