常见的限流算法
在高并发环境下,为了保证系统的稳定,通常会用到限流、降级、熔断等手段,来保证系统的稳定可用。
限流顾名思义就是限制服务处理的流量,其实熔断、降级本质上也是限流的一种,都是阻断了请求流量,本篇文章重点介绍常见的限流算法。
为什么限流
为什么需要限流呢?这个问题比较好理解,就是请求服务的流量过大,会导致服务崩溃,为了避免这种情况的发生,所以要对流量进行限制。
在以下这些常见的情况下可能会引起流量激增:
- 促销活动
- 恶意用户刷单或请求服务
- 网络爬虫
- 正常的用户量增大
这些情况都会导致流量的增加,加入我们服务的QPS最大只能支持到1000,当某一时刻的请求增大到1000以上时,我们就要对流量限制在1000以内,这样才能保证服务的稳定运行,这就是限流。
漏桶算法
漏桶算法思路比较简单,就好比有一个漏斗,只能按照漏斗口的大小往外出水,当流入漏斗的水流过大时,则会溢出去,溢出的水不会从漏斗口流出。
可以看出,漏桶算法的核心逻辑为:
- 缓存请求
- 匀速处理
- 多出的丢弃
漏桶算法会强行限制请求速率,这一特点导致漏桶算法有以下缺点:
不能应对突发流量
比如漏桶的流出速率为10QPS,容量为30,当来一波20次/s的请求,共维持10秒,那么到第3秒时,漏桶容量便已存满,这以后的请求都会被丢弃,直到漏桶有新的容量被释放。但是这种情况是很常见的,直接丢弃这种方式比较粗暴。
不能有效利用资源
因为漏桶的出口速率固定,假设设置为10QPS,那么当请求数量为12,15,10,8,5这种情况请求时,整体来看平均请求速率也是10QPS,但是在前三秒因为速率设定为10,则会直接丢弃7个请求。
令牌桶算法
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。从名字上很多人会和漏桶算法混淆,但是它们有很大的差别。令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
令牌桶算法的过程主要包含以下内容:
- 系统匀速的产生令牌存放到令牌桶中;
- 令牌桶的容量固定,当令牌桶填满后,再放入其中的令牌会被丢弃;
- 每个请求从令牌桶中获取令牌,如果获取成功则处理请求,如果失败则丢弃请求。
那么为什么说令牌桶算法可以应对突发请求呢?
首先我们来理解一下什么是突发请求。假如我们希望服务速率是100QPS,正常情况我们的请求不会超过100/s,如果某一秒请求到达150,这就是突发请求。
如果是漏桶算法,如果第一秒有80个请求,第二秒有150个请求,那么就会有50个请求被丢弃。
但是在令牌桶算法中我们设定每秒产生100个令牌,我们可以设定令牌桶的容量为120,那么第1秒80个请求会消耗80个令牌,到第一秒的150个请求到达时,可以处理120个请求,只丢弃30个。那么是否意味着只要桶的容量设置的足够大就都可以处理了呢?当然不是,因为超过120可能就已经远远超出了服务处理的能力,桶容量设置的超过服务能力也就丢失了限流的作用了。
RateLimiter
talk is cheap, show me the code!
在Guava中的RateLimiter便是使用令牌桶算法实现的。
运行结果:
从运行结果可以看到,很准确的保证了每秒最多只有三个线程获取到令牌,很好的对请求做了控制。
Guava RateLimiter
的的acquire()
方法默认是阻塞的,如果在获取不到令牌时会阻塞等待,知道获取成功。
RateLimiter
除了acquire()
方法外还有其他的使用方法。
- tryAcquire():尝试获取1个令牌,如果失败则直接返回
- tryAcquire(int permits):尝试获取多个令牌
- tryAcquire(long timeout, TimeUnit unit):尝试获取一个令牌如果当前没有会等待指定时间,不会阻塞
- tryAcquire(int permits,long timeout, TimeUnit unit):尝试获取多个令牌如果当前没有会等待指定时间,不会阻塞
需要特别注意的是,RateLimiter只能支持单机的限流,所以它无法针对集群做流量限制。
集群环境如果需要做流量限制,常见的方式是使用Redis,针对每秒的时间戳对请求数进行累加,超过限制数量则拒绝服务。
总结
本文主要介绍了两种常见的限流算法:漏桶算法和令牌桶算法。
漏桶算法因为强制限定请求速率,所以不能应对突发流量;
令牌桶算法能够一定程度的应对突发流量,具体能应对的阈值取决于服务的能力;
当然,在某些场景下如果需要严格保证请求速率,漏桶算法则比令牌桶更适合。
Guava RateLimiter使用令牌桶算法实现,可以做为单机限流方案的一种选择,集群环境下的限流方案可以选择Redis实现。
文章转载自公众号:小黑说java