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