Gateway 网关限流

系统进行高并发处理时 ,往往需要进行限流处理,防止因流量过大导致服务不可用,也可防止网络攻击。

常见的限流算法:

1.计数器算法:

一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。存在弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms 请求全部拒绝掉。

2.漏桶算法:

类似漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。弊端:无法应对突发流量冲击。

3.令牌桶算法:

主要有生产令牌和消费令牌组成。

生产令牌:固定容量的令牌桶,按固定的速率(N/s)往桶中放入令牌,桶满时不再放入;
消费令牌:每个请求需要从桶中拿取令牌,当消费速率低于生产速率时,直至桶中令牌满而触发限流,此时请求可以放入缓冲队列或直接拒绝。
令牌桶算法有一个很关键的问题,就是桶容量的设置,这个参数可以让令牌桶算法具备处理突发流量的能力。假如将桶容量设置为 100,生成令牌的速度为每秒 10 个,那么在系统空闲一段时间之后(桶中令牌一直没有消费,慢慢的会被装满),突然来了 50 个请求,这时系统可以直接按每秒 50 个的速度处理,随着桶中的令牌很快用完,处理速度又会慢慢降下来,和生成令牌速度趋于一致。这是令牌桶算法和漏桶算法最大的区别,漏桶算法无论来了多少请求,只会按固定速度进行处理。

令牌桶具体代码实现方式:

1.引入jar包:

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

2.yml 配置:

spring:
  redis:
    host: 127.0.0.1
    port: 6379
  cloud:
    gateway:
      discovery:
        locator:
          enabled: false
          lowerCaseServiceId: true
      routes:
      - id: store-goods-service
        predicates:
        - Path=/store-goods-service/**
        uri: lb://STORE-GOODS-SERVICE
        filters:
        - StripPrefix=1
        - name: RedisRequestRateLimiter
          args:
            key-resolver: '#{@pathKeyResolver}'
            # 令牌桶每秒填充平均速率
            redis-rate-limiter.replenishRate: 1
            # 令牌桶的总容量
            redis-rate-limiter.burstCapacity: 3

3.配置 key-resolver 的bean 对象  和yaml 中定义的名称相同

@Configuration
public class KeyResolverConfiguration {

    /**
     * 基于请求路径的限流
     */
    @Bean
    public KeyResolver pathKeyResolver() {
        return exchange -> Mono.just(
                exchange.getRequest().getPath().toString()
        );
    }
}

这样基本就完成限流了,通过浏览器模拟访问发下 ,返回状态码429 

 

 为了更加有好的提示错误信息 ,可以自定义类继承RequestRateLimiterGatewayFilterFactory,自定义错误信息:

@Slf4j
@Component
public class RedisRequestRateLimiterGatewayFilterFactory extends RequestRateLimiterGatewayFilterFactory {

    private final RateLimiter defaultRateLimiter;

    private final KeyResolver defaultKeyResolver;

    public RedisRequestRateLimiterGatewayFilterFactory(RateLimiter defaultRateLimiter, KeyResolver defaultKeyResolver) {
        super(defaultRateLimiter, defaultKeyResolver);
        this.defaultRateLimiter = defaultRateLimiter;
        this.defaultKeyResolver = defaultKeyResolver;
    }

    @Override
    public GatewayFilter apply(Config config) {
        KeyResolver resolver = getOrDefault(config.getKeyResolver(), defaultKeyResolver);
        RateLimiter<Object> limiter = getOrDefault(config.getRateLimiter(), defaultRateLimiter);
        return (exchange, chain) -> resolver.resolve(exchange).flatMap(key -> {
            String routeId = config.getRouteId();
            if (routeId == null) {
                Route route = exchange.getAttribute(ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR);
                routeId = route.getId();
            }

            String finalRouteId = routeId;
            return limiter.isAllowed(routeId, 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);
                }

                log.info("已限流: {}", finalRouteId);
                ServerHttpResponse httpResponse = exchange.getResponse();
                httpResponse.setStatusCode(config.getStatusCode());
                if (!httpResponse.getHeaders().containsKey("Content-Type")) {
                    httpResponse.getHeaders().add("Content-Type", "application/json");
                }
                JSONObject json = new JSONObject();
                json.put("code", HttpStatus.TOO_MANY_REQUESTS.value());
                json.put("message","当前人数较多,请点击刷新试试");
                json.put("serverTimeMillis",System.currentTimeMillis());
                DataBuffer buffer = httpResponse.bufferFactory().wrap(json.toJSONString().getBytes(StandardCharsets.UTF_8));
                return httpResponse.writeWith(Mono.just(buffer));
            });
        });
    }

    private <T> T getOrDefault(T configValue, T defaultValue) {
        return (configValue != null) ? configValue : defaultValue;
    }
}

根据实际情况通过redis自定义限流:

         String deviceId = headers.getFirst(Constant.DEVICE_ID);
            if (!StringUtils.isEmpty(deviceId)) {
                redisTemplate.opsForZSet().add(Constant.REDIS_USER_DEVICE_ID_KEY, deviceId, timestamp);
                redisTemplate.expire(Constant.REDIS_USER_DEVICE_ID_KEY, 300, TimeUnit.SECONDS);
                Long size = redisTemplate.opsForZSet().size(Constant.REDIS_USER_DEVICE_ID_KEY);
                if (null != size && size > maxPerson) {
                    //删除5分钟之前的key
                    long score = System.currentTimeMillis() - (1000 * 60 * 5);
                    Long count = redisTemplate.opsForZSet().removeRangeByScore(Constant.REDIS_USER_DEVICE_ID_KEY, 0, score);
                    if ((size - count) >= maxPerson) {
                        redisTemplate.opsForZSet().remove(Constant.REDIS_USER_DEVICE_ID_KEY, deviceId);
                        return ApiAuthUtils.checkSign(exchange, WrapMapper.wrap(429, "Too Many Requests"), HttpStatus.TOO_MANY_REQUESTS);
                    }
                }
            }

 

posted @ 2022-05-11 14:33  山阴路的秋天  阅读(716)  评论(0编辑  收藏  举报