限流的三种算法
https://www.cnblogs.com/forezp/p/10140316.html
概述
限流要解决的问题
防止QPS过高导致的服务器崩溃,特别是一些耗时、耗资源的操作,限流可以保护服务器负载可控
基于业务上考虑(比如IP限流、用户限流 ),限流可以避免业务上的异常运行
- 防止用户异常的操作,例如开挂抢商品、抢火车票
- 防止攻击服务器,导致正常的请求没法及时处理,DDOS攻击
典型限流的应用场景:
- 秒杀 防止刷单
- 耗资源的一些操作(比如微信token的限流,因为是 1对多,不限流 容易崩)
如何限流?
一般网关都有这种功能。 gateway、nginx、zuul等
限流:一定时间内,只允许N次请求。
从计算机友好的角度出发,是希望能在单位时间内均摊掉请求,使用漏斗算法可以达到这种效果。
但是漏斗算法有个弊端,就是先快后慢的这种请求,那么峰值的请求也只能排队等待被消费。实际上计算机是具备一定的高并发处理能力的,只要不是一直处于高并发下即可。所以 计数器限流和 漏洞限流折中的算法,令牌限流成为现在最主流的算法。
方案一:计数器限流
(Redis 结合expire方案可以实现)
第一次请求开始计时,例如1s以内,达到100次请求就拒绝访问了,直到1s过后,重新开始计数。
优点:
- 实现简单
- 可以保证一定时间内,一定能处理完限流次数的请求。比如1s限流1000次请求,那么无论是先快后慢,还是先慢后快,都可以完成1000次请求。
缺点:短暂的峰值过高对服务器不友好。服务器希望能把请求尽量的均摊开来,这样可以充分利用计算机资源。
- 先到先得,可能1ms就有1000次请求打满了,999ms里面都是空闲,对服务器而言,这样并不友好。
- 而且1s的最后1ms有1000次请求,下一秒开始头1ms又有1000次请求,会导致连续的峰值过高,对服务器特别不友好。
方案二:漏洞限流
消费的速度是恒定的,对于服务器而言是最友好的。
在算法实现方面,可以准备一个队列,用来保存请求,另外通过一个线程池(ScheduledExecutorService)来定期从队列中获取请求并执行,可以一次性获取多个并发执行。
参数:消费速度、桶容量(超过就抛弃,可以避免内存过大,有过多的等待的任务)
优点:
- 实现限流的功能的同时,又可以充分利用计算机资源,非常友好的一种方案
- 不会因为开始计数时间导致异常的峰值
缺点:
- 并不能保证规定时间内消费预期的流量,比如1s内限流1000次,1ms1个请求的模型,如果前面没有请求,第900ms时有大量的请求,那么1s之内的处理速度时100个。
- 先快后慢的时候,比如前100ms有500个请求,后900ms有500个请求。1s也是1000个请求,但是前500个请求只能排队,1ms处理一个,实际上计算机是有一定的高并发能力的,只要不是长期处于高并发的模式都行的。这样前面快的请求不需要盲目等待。
方案三:令牌限流
令牌桶算法是比较常见的限流算法之一,大概描述如下:
1)所有的请求在处理之前都需要拿到一个可用的令牌才会被处理;
2)根据限流大小,设置按照一定的速率往桶里添加令牌;
3)桶设置最大的放置令牌限制,当桶满时、新添加的令牌就被丢弃活着拒绝;
4)请求达到后首先要获取令牌桶中的令牌,拿着令牌才可以进行其他的业务逻辑,处理完业务逻辑之后,将令牌直接删除;
5)令牌桶有最低限额,当桶中的令牌达到最低限额的时候,请求处理完之后将不会删除令牌,以此保证足够的限流;
这种算法,既可以保证系统由一定的高并发能力,比如当前令牌桶容量是100,一开始直接消费掉100个请求。有保证服务器不会因为短暂的爆发,而导致server端空闲,因为令牌桶还会持续的生产令牌。
既有一定的并发能力,又不至于完全失去控制,可控的兼具并发力和流量控制的限流算法.是计数器算法(一定的并发处理能力)和漏洞限流(高峰过后仍然会持续的产生令牌)的折中算法。
Gateway的限流
- 针对用户限流
- 针对url限流
- 针对IP限流
@Bean
KeyResolver userKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getQueryParams().getFirst("user"));
}
@Bean
public KeyResolver ipKeyResolver() {
return exchange -> Mono.just(exchange.getRequest().getRemoteAddress().getHostName());
}
@Override
public Mono<String> resolve(ServerWebExchange exchange) {
return Mono.just(exchange.getRequest().getURI().getPath());
}