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); } } }