前言:在互聯網應用中,特別是電商,高並發的場景非常多,比如:秒殺、搶購、雙11等,在開始時間點會使流量爆發式地涌入,如果對網絡流量不加控制很有可能造成后台實例資源耗盡。限流是指通過指定的策略削減流量,使到達后台實例的請求在合理范圍內。本章將介紹spring cloud gateway如何實現限流。
前情回顧請參考:
Spring Cloud 微服務二:API網關spring cloud zuul
Spring Cloud 微服務三: API網關Spring cloud gateway
Spring Cloud 微服務四:熔斷器Spring cloud hystrix
- 限流算法
-
主流的限流算法有兩種:漏桶(leaky bucket)和令牌桶(token bucket)。漏桶算法 有一個固定容量的桶,對於流入的水無法預計速率,流出的水以固定速率,當水滿之后會溢出。
令牌桶算法,有一個固定容量的桶,桶里存放着令牌(token)。桶最開始是空的,token以一個固定速率向桶中填充,直到達到桶的容量,多余的token會被丟棄。每當一個請求過來時,都先去桶里取一個token,如果沒有token的話請求無法通過。
兩種算法的最主要區別是令牌桶算法允許一定流量的突發,因為令牌桶算法中取走token是不需要時間的,即桶內有多少個token都可以瞬時拿走。基於這個特點令牌桶算法在互聯網企業中應用比較廣泛,我們在實現限流的時候也會基於這個算法。
- gateway如何實現限流 方法1:Spring cloud gateway實現限流的方式主要是通過添加自定義filter來實現,自定義filter需要實現GatewayFilter和Ordered接口。本章將結合開源的Bucket4j來實現,Bucket4j是基於令牌桶算法實現,Bucket4j代碼參考:https://github.com/vladimir-bukhtoyarov/bucket4j
-
首先修改api-gateway module,pom中添加Bucket4j依賴,最新版本是4.3.0,工程的版本已經在父工程中定義好了
<dependency> <groupId>com.github.vladimir-bukhtoyarov</groupId> <artifactId>bucket4j-core</artifactId> </dependency>
第二步,添加filter實現GatewayFilter和Ordered,添加相應的參數,並使用一個ConcurrentHashMap存儲ip以及bucket,實現filter方法,對客戶端訪問ip進行過濾
public class LimitFilter implements GatewayFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(LimitFilter.class); private int capacity; private int refillTokens; private Duration refillDuration; public LimitFilter(int capacity, int refillTokens, Duration refillDuration) { this.capacity = capacity; this.refillTokens = refillTokens; this.refillDuration = refillDuration; } private static final Map<String, Bucket> CACHE = new ConcurrentHashMap<>(); private Bucket createNewBucket() { Refill refill = Refill.greedy(refillTokens, refillDuration); Bandwidth limit = Bandwidth.classic(capacity, refill); return Bucket4j.builder().addLimit(limit).build(); } @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { String ip = exchange.getRequest().getRemoteAddress().getAddress().getHostAddress(); Bucket bucket = CACHE.computeIfAbsent(ip, k -> createNewBucket()); logger.info("IP: "+ip+", available tokens :"+bucket.getAvailableTokens()); if (bucket.tryConsume(1L)) { return chain.filter(exchange); } logger.info("IP: "+ip+", available tokens :"+bucket.getAvailableTokens()+" too many requests"); exchange.getResponse().setStatusCode(HttpStatus.TOO_MANY_REQUESTS); return exchange.getResponse().setComplete(); } @Override public int getOrder() { return 0; } }
第三步,添加自定義路由,添加配置類,RouteLocator構造器中添加filter以及相應的地址信息,設置同一ip同時只能訪問一次,多余的將被忽略。另外,由於我們在程序中配置了路由,需要將application.yml中的gateway相關屬性刪除。
@Configuration public class RouteLocatorConfig { @Bean public RouteLocator customRouteLocator(RouteLocatorBuilder builder) { System.out.println("============================RouteLocatorConfig "+ builder.routes()); return builder .routes() .route(r -> r.path("/*") .filters(f -> f.filter(new LimitFilter(1, 1, Duration.ofSeconds(1)))) .uri("http://localhost:10080/") .order(0) .id("user_route")) .build(); } }
最后,測試,重啟api-gateway,訪問http://localhost:8088/users,第一次訪問成功,頻繁刷新會出現空白頁,控制台會輸出相關信息
方法2:使用spring cloud 原生的redis方式
第一步,搭建redis服務器,具體方法參考redis官網
第二步,pom中添加redis依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis-reactive</artifactId> </dependency>
自定義resolver
@Configuration public class CustomResolver { @Bean public KeyResolver ipKeyResolver(){ System.out.println("##############ipKeyResolver########################"); return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName()); } }
第三步,修改application.yml配置
routes:
- id: user_route
uri: http://localhost:10080
predicates:
- Path=/*
filters:
- name: RequestRateLimiter
args:
redis-rate-limiter.replenishRate: 1
redis-rate-limiter.burstCapacity: 1
key-resolver: "#{@ipKeyResolver}
最后做測試,並使用monitor命令監控redis