Gateway限流

      Spring Cloud Gateway提供RequestRateLimiterGatewayFilterFactory这个类,适用Redis和lua脚本实现令牌桶方式。实现逻辑在RequestRateLimiterGatewayFilterFactory类中。

      pom文件中引入gateway的起步依赖和redis的reactive依赖

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-gateway</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>

  在配置文件的配置:

spring:
  application:
     name: gateway-client
  cloud:
     gateway:
        routes:
        - id: route_service_one
          uri: ${movie.uri}  # uri以lb://开头(lb代表从注册中心获取服务),后面就是需要转发到的服务名称
          filters:
          - name: RequestRateLimiter
            args:
              key-resolver: '#{@uriKeyResolver}'
              redis-rate-limiter.replenishRate: 1
              redis-rate-limiter.burstCapacity: 3
          - AddRequestHeader=X-Request-Foo, Bar
            RewritePath=/foo/(?<segment>.*), /$\{segment}
          predicates:
          - Path=/movie/**
  redis:
    host: localhost
    port: 6379
    database: 0

       过滤器配置三个参数:

  burstCapacity,令牌桶总容量
  replenishRate,令牌桶每秒填充平均速率
  key-resolver,用于限流的键的解析器的 Bean 对象的名字。使用 SpEL 表达式根据#{@beanName}从 Spring 容器中获取 Bean 对象。

       keyResolver 限流的维度。 默认实现类PrincipalNameKeyResolver,以用户名来作为维度,会从 ServerWebExchange检索 Principal并调用 Principal.getName()。因为Principal.getName()是空的,而denyEmptyKey =true,没有Principal的话,会返回forbidden403,就算设置denyEmptyKey =false,因为name是空,所以限流KeyResolver的键是空,限流会不生效 

        KeyResolver需要实现resolve方法,比如根据Hostname进行限流,则需要用hostAddress去判断

public class HostAddrKeyResolver implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getRemoteAddress().getAddress().getHostAddress());
    }

}

 @Bean
 public HostAddrKeyResolver hostAddrKeyResolver() {
    return new HostAddrKeyResolver();
 }

      根据uri去限流,这时KeyResolver代码如下:

public class UriKeyResolver  implements KeyResolver {

    @Override
    public Mono<String> resolve(ServerWebExchange exchange) {
        return Mono.just(exchange.getRequest().getURI().getPath());
    }

}

 @Bean
 public UriKeyResolver uriKeyResolver() {
    return new UriKeyResolver();
 }

      以用户的维度去限流:

   @Bean
   KeyResolver userKeyResolver() {
        return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
  }

RequestRateLimiterGatewayFilterFactory核心源码

public GatewayFilter apply(Config config) {
        KeyResolver resolver = getOrDefault(config.keyResolver, defaultKeyResolver);
        RateLimiter<Object> limiter = getOrDefault(config.rateLimiter,
                defaultRateLimiter);
        boolean denyEmpty = getOrDefault(config.denyEmptyKey, this.denyEmptyKey);
        HttpStatusHolder emptyKeyStatus = HttpStatusHolder
                .parse(getOrDefault(config.emptyKeyStatus, this.emptyKeyStatusCode));

        return (exchange, chain) -> {
            Route route = exchange
                    .getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);

            return resolver.resolve(exchange).defaultIfEmpty(EMPTY_KEY).flatMap(key -> {
                if (EMPTY_KEY.equals(key)) {
                    if (denyEmpty) {
                        setResponseStatus(exchange, emptyKeyStatus);
                        return exchange.getResponse().setComplete();
                    }
                    return chain.filter(exchange);
                }
                return limiter.isAllowed(route.getId(), key).flatMap(response -> {

                    for (Map.Entry<String, String> header : response.getHeaders()
                            .entrySet()) {
                        exchange.getResponse().getHeaders().add(header.getKey(),
                                header.getValue());
                    }

                    if (response.isAllowed()) {  // 允许访问
                        return chain.filter(exchange);
                    }
                    // 被限流了,直接返回
                    setResponseStatus(exchange, config.getStatusCode());
                    return exchange.getResponse().setComplete();
                });
            });
        };
    }

RedisRateLimiter核心源码

public Mono<Response> isAllowed(String routeId, String id) {
        if (!this.initialized.get()) {
            throw new IllegalStateException("RedisRateLimiter is not initialized");
        }

        Config routeConfig = loadConfiguration(routeId);

        // How many requests per second do you want a user to be allowed to do?
        int replenishRate = routeConfig.getReplenishRate();

        // How much bursting do you want to allow?
        int burstCapacity = routeConfig.getBurstCapacity();

        try {
            List<String> keys = getKeys(id);

            // The arguments to the LUA script. time() returns unixtime in seconds.
            List<String> scriptArgs = Arrays.asList(replenishRate + "",
                    burstCapacity + "", Instant.now().getEpochSecond() + "", "1");
            // allowed, tokens_left = redis.eval(SCRIPT, keys, args)
            Flux<List<Long>> flux = this.redisTemplate.execute(this.script, keys,
                    scriptArgs);
            // .log("redisratelimiter", Level.FINER);
            return flux.onErrorResume(throwable -> Flux.just(Arrays.asList(1L, -1L)))
                    .reduce(new ArrayList<Long>(), (longs, l) -> {
                        longs.addAll(l);
                        return longs;
                    }).map(results -> {
//是否获取令牌成功(1-成功,0-失败)
boolean allowed = results.get(0) == 1L; Long tokensLeft = results.get(1);//result返回四个参数:剩余令牌数、每秒补充的令牌数、令牌容量、每个请求申请的令牌数 Response response = new Response(allowed, getHeaders(routeConfig, tokensLeft)); if (log.isDebugEnabled()) { log.debug("response: " + response); } return response; }); } catch (Exception e) { /* * We don't want a hard dependency on Redis to allow traffic. Make sure to set * an alert so you know if this is happening too much. Stripe's observed * failure rate is 0.01%. */ log.error("Error determining if user allowed from redis", e); }
return Mono.just(new Response(true, getHeaders(routeConfig, -1L))); //兜底,默认是返回成功,剩余令牌数是-1 }

 


     

 

      

  

 

posted on 2022-04-17 17:15  溪水静幽  阅读(487)  评论(0)    收藏  举报