https://www.cnblogs.com/csts/p/10286378.html
前言:在互联网应用中,特别是电商,高并发的场景非常多,比如:秒杀、抢购、双11等,在开始时间点会使流量爆发式地涌入,如果对网络流量不加控制很有可能造成后台实例资源耗尽。限流是指通过指定的策略削减流量,使到达后台实例的请求在合理范围内。本章将介绍spring cloud gateway如何实现限流。
前情回顾请参考:
Spring Cloud 微服务一:Consul注册中心
Spring Cloud 微服务二:API网关spring cloud zuul
Spring Cloud 微服务三: API网关Spring cloud gateway
Spring Cloud 微服务四:熔断器Spring cloud hystrix
令牌桶算法,有一个固定容量的桶,桶里存放着令牌(token)。桶最开始是空的,token以一个固定速率向桶中填充,直到达到桶的容量,多余的token会被丢弃。每当一个请求过来时,都先去桶里取一个token,如果没有token的话请求无法通过。
两种算法的最主要区别是令牌桶算法允许一定流量的突发,因为令牌桶算法中取走token是不需要时间的,即桶内有多少个token都可以瞬时拿走。基于这个特点令牌桶算法在互联网企业中应用比较广泛,我们在实现限流的时候也会基于这个算法。
com.github.vladimir-bukhtoyarov
bucket4j-core
第二步,添加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 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 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依赖
org.springframework.boot
spring-boot-starter-data-redis-reactive
自定义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