spring cloud gateway - 限流
搭建API网关
技术栈:spring cloud gateway + hystrix + erueka client
网关的项目配置都是基于代码方式(除了一些必要的配置)
实现限流
计数器算法
计数器算法采用计数器实现限流有点简单粗暴,一般我们会限制一秒钟的能够通过的请求数,比如限流qps为100,算法的实现思路就是从第一个请求进来开始计时,在接下去的1s内,每来一个请求,就把计数加1,如果累加的数字达到了100,那么后续的请求就会被全部拒绝。等到1s结束后,把计数恢复成0,重新开始计数。具体的实现可以是这样的:对于每次服务调用,可以通过AtomicLong#incrementAndGet()方法来给计数器加1并返回最新值,通过这个最新值和阈值进行比较。这种实现方式,相信大家都知道有一个弊端:如果我在单位时间1s内的前10ms,已经通过了100个请求,那后面的990ms,只能眼巴巴的把请求拒绝,我们把这种现象称为“突刺现象”
漏桶算法
漏桶算法为了消除"突刺现象",可以采用漏桶算法实现限流,漏桶算法这个名字就很形象,算法内部有一个容器,类似生活用到的漏斗,当请求进来时,相当于水倒入漏斗,然后从下端小口慢慢匀速的流出。不管上面流量多大,下面流出的速度始终保持不变。不管服务调用方多么不稳定,通过漏桶算法进行限流,每10毫秒处理一次请求。因为处理的速度是固定的,请求进来的速度是未知的,可能突然进来很多请求,没来得及处理的请求就先放在桶里,既然是个桶,肯定是有容量上限,如果桶满了,那么新进来的请求就丢弃。
令牌桶算法
从某种意义上讲,令牌桶算法是对漏桶算法的一种改进,桶算法能够限制请求调用的速率,而令牌桶算法能够在限制调用的平均速率的同时还允许一定程度的突发调用。在令牌桶算法中,存在一个桶,用来存放固定数量的令牌。算法中存在一种机制,以一定的速率往桶中放令牌。每次请求调用需要先获取令牌,只有拿到令牌,才有机会继续执行,否则等待可用的令牌、或者直接拒绝。放令牌这个动作是持续不断的进行,如果桶中令牌数达到上限,就丢弃令牌,所以就存在这种情况,桶中一直有大量的可用令牌,这时进来的请求就可以直接拿到令牌执行,比如设置qps为100,那么限流器初始化完成一秒后,桶中就已经有100个令牌了,这时服务还没完全启动好,等启动完成对外提供服务时,该限流器可以抵挡瞬时的100个请求。所以,只有桶中没有令牌时,请求才会进行等待,最后相当于以一定的速率执行。
实现思路:可以准备一个队列,用来保存令牌,另外通过一个线程池定期生成令牌放到队列中,每来一个请求,就从队列中获取一个令牌,并继续执行。
实现代码
添加依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis-reactive</artifactId>
</dependency>
添加属性配置
# redis配置
spring.redis.host=localhost
spring.redis.port=6379
spring.redis.password=root123
spring.redis.timeout=10000
# 令牌桶总容量
limiter.replenishRate=100
# 令牌桶每秒填充平均速率
limiter.burstCapacity=100
添加限流配置类
LimiterConfig.java
@Configuration
public class LimiterConfig {
/**
* 令牌桶总容量
*/
@Value("${limiter.replenishRate}")
private Integer replenishRate;
/**
* 令牌桶每秒填充平均速率
*/
@Value("${limiter.burstCapacity}")
private Integer burstCapacity;
/**
* IP限流
* @return KeyResolver
*/
@Bean
@Primary
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostString());
}
/**
* 用户限流
* @return KeyResolver
*/
@Bean
public KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("userId"));
}
/**
* 接口限流
* @return KeyResolver
*/
@Bean
public KeyResolver apiKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getPath().value());
}
/**
* redis 限流配置类
* @param redisTemplate redisTemplate
* @param redisScript redisScript
* @param defaultValidator defaultValidator
* @return MyRedisRateLimiter
*/
@Bean
@Primary
public MyRedisRateLimiter myRedisRateLimiter(
ReactiveRedisTemplate<String, String> redisTemplate,
@Qualifier(RedisRateLimiter.REDIS_SCRIPT_NAME) RedisScript<List<Long>> redisScript,
Validator defaultValidator) {
MyRedisRateLimiter myRedisRateLimiter = new MyRedisRateLimiter(redisTemplate,redisScript,defaultValidator);
myRedisRateLimiter.setLimiterConfig(replenishRate,burstCapacity);
return myRedisRateLimiter;
}
}
重写限流实现类( copy于RedisRateLimiter源码 - 只添加了setLimiterConfig配置方法 )
/**
* 基于redis的限流实现类
* copy于RedisRateLimiter源码
* 增加了一个设置令牌数据的方法
* @author magicHat
* @date 2019/8/19 13:58
*/
public class MyRedisRateLimiter extends AbstractRateLimiter<RedisRateLimiter.Config>
implements ApplicationContextAware {
......
public void setLimiterConfig(int defaultReplenishRate, int defaultBurstCapacity){
this.defaultConfig = new RedisRateLimiter.Config().setReplenishRate(defaultReplenishRate)
.setBurstCapacity(defaultBurstCapacity);
}
......
}
改造路由配置
@Resource
private KeyResolver ipKeyResolver;
@Resource
private MyRedisRateLimiter myRedisRateLimiter;
/**
* 权限微服务
* @param builder 路由设置
* @return RouteLocator
*/
@Bean
public RouteLocator routeLocatorPermission(RouteLocatorBuilder builder) {
RedisRateLimiter redisRateLimiter = new RedisRateLimiter(1,3);
return builder.routes()
.route("permission", r -> r.path("/demo/permission/**")
.filters(f -> f
.stripPrefix(3)
.hystrix(config -> config.setName("hystrixName").setFallbackUri("forward:/fallback"))
.requestRateLimiter(config -> config.setKeyResolver(ipKeyResolver).setRateLimiter(myRedisRateLimiter))
)
.uri("lb://BOSS-BES-PERMISSION")).build();
}