带你手撸令牌桶限流算法
带你手撸令牌桶限流算法
1.漏桶算法和令牌桶算法
限流为流量控制策略中的一种,流量控制策略一共可大致分流、限流、降级熔断三种。
漏桶算法和令牌桶算法都属于限流的基本算法,但是各自有各自的特点
漏桶算法
上图是从网上拷的一张算法示意图,其中,桶的体积表示能够处理请求的最大值,水龙头的水表示外部请求,漏下去的水表示处理的请求。
也就是说,不管水龙头的水怎么往下流,开最大也好,关掉也好,都不会影响到桶往下滴水的速度,这也是漏桶算法最核心的一点,能够保证流量的平滑性(请求处理的速度基本一致),如果外部请求超过了桶的容积,那么水就流到桶外去了(这些请求也就都抛弃掉了)。因此,漏桶算法并不能满足对于突发流量高峰的场景,所以漏桶算法的具体实现不再阐述。
令牌桶算法
同样的,从网上拷的一张令牌桶的示意图。
令牌桶相对于漏桶算法来说,首先多了一个缓存区,使其能够处理突发的流量高峰(就是削峰填谷的作用),并且令牌的生成是每时每刻都在生成的,而不像漏桶算法,如果一段时间一直没有请求,那么桶就一直是空的,这样就能够保证,在一段时间内,请求的处理的平均速度等于令牌生成的速度。
2.开始手撸令牌桶
关于令牌桶的算法,有很多很完善的实现,比如用的最多的Guava库中的RateLimiter就用到了令牌桶算法。但是,如果只是看源码,可能短时间内就忘得干干净净了,其实令牌桶的概念很简单,不如我们直接根据概念手撸一下,更能加深记忆。
通过上一步的令牌桶算法示意图,基本上可以先规划出所用到的数据结构。
首先,我们要规定一个速率,也就是要限制的qps,
然后,产生令牌也不能无限产生,要有一个可容纳令牌的最大值maxPermits,自然也要知道,目前有多少令牌可用currentPermits.
这样一来,数据结构就定义完成了,如下
1 | // 令牌生成的速度 |
对于,如何实现持续增加令牌数,有两种方法可以做,一个是起新的线程,持续增加,一个是惰性增加,在用到令牌的时候才去增加。如果起新的线程去增加,会涉及到对令牌资源增减的线程安全问题,所以先使用简单一点的方式实现,惰性增加。既然要惰性增加,那么就要知道本次请求距离上次请求经过了多长时间,因为知道经过了多长时间,才能知道要加多少个令牌,所以,要在数据结构里增加一个字段,用于记录上次请求的时间。那么,现在数据结构就变为了如下
1 | // 令牌生成的速度 |
但是,之前有讲过,令牌桶算法可以应对突发流量的状况,也就是说就算所需令牌大于当前已有令牌,请求仍能够执行,那么,记录上次请求的时间显然是不准确的,因为,上个请求已经预支了以后请求的令牌了,所以,这里可以更改一下,变为记录下次请求能够执行的时间,这样,就算请求预支了令牌,以后的请求仍能够感知到。
1 | // 令牌生成的速度 |
在具有了以上数据结构后,就可以给出惰性增加令牌方法的实现
1 | // 惰性增加令牌数 |
现在,增加令牌实现了,那我们就可以实现消耗令牌的方法了,也就是实际请求时的方法
1 | /** |
在getSpendTime方法中,涉及到了对临界资源的修改,所以要使用锁控制一下,也有两种方法,一种是整个方法加关键字synchronized锁一下,另一种是创建一个锁对象,用锁对象当锁synchronized一下。当然这两种方法都是针对单机版的,不过对于一般网关应用来说单机也够用了。
从以上代码,可以看出,令牌桶的概念其实真没有多少,以上代码再抽象优化一下,其实跟RateLimiter的SmoothBursty的限流实现就基本一致了。对于这些简单的算法,自己实现一下,可能就会记很久很久。
3.关于Guava的RateLimiter使用
在Guava的 RateLimiter的令牌桶算法实现中,提供了两种不同的实现策略
平滑突发限流(SmoothBursty)
SmoothBursty在上一节中详细讲解了实现,就不过多介绍了,直接讲如何使用。
1
2
3
4// 设置每秒多少个令牌,也就相当于QPS
RateLimiter r = RateLimiter.create(100);
// 每个请求获取多少令牌
r.acquire(5)平滑预热限流(SmoothWarmingUp)
SmoothWarmingUp相对于SmoothBursty最大的区别就是产生令牌的策略有所不同,平滑预热限流其中的预热两字能很好的说明他的特点,使用预热限流时要设置一个预热时间,在预热时间内,令牌产生的速率会随着时间和令牌数的变化会缓慢增长,并在预热时间结束后到达设置的产生速率。
1
2
3
4// 第一个参数还是令牌的产生速度,后两个参数表示预热时间,这里设置了三十秒
RateLimiter r = RateLimiter.create(100, 30, TimeUnit.SECONDS);
// 每个请求获取多少令牌
r.acquire(5)
RateLimiter中的acquire方法用于获取令牌,但是该方法会造成阻塞,一直会阻塞到获取了足够的令牌才会返回,如果在系统中,瞬间流量过高,使用acquire方法会造成大量线程排队阻塞进而造成严重问题,用户体验也不够好,所以我们可以使用tryAcquire(int permits, long timeout, TimeUnit unit)方法进行代替,tryAcquire可以设置一个超时时间,如果超过了该时间仍没有获取到足够的令牌,会直接返回失败,不会继续阻塞下去。