漏桶算法、令牌桶算法思路及使用场景
在介绍RateLimiter
之前我们先看下常说的令牌桶算法和漏桶算法,看下两种算法的思想和适用场景:
令牌桶算法:
结合上面的图,令牌桶算法就是以固定速率生成令牌放入桶中,每个请求都需要从桶中获取令牌,没有获取到令牌的请求会被阻塞限流(桶中的令牌不够的时候),当令牌消耗速度小于生成的速度时,令牌桶内就会预存这些未消耗的令牌(直到桶的上限),当有突发流量进来时,可以直接从桶中取出令牌,而不会被限流。
漏桶算法:
结合上面的图,漏桶算法就是将请求放入桶中,然后始终以一个固定的速率从桶中取出请求来处理,当桶中等待的请求数超过上限后(桶的容量固定),后续的请求就不再加入桶中,而是执行拒绝策略(比如降级)
适用于需要以固定速率的场景,而在多数业务场景中,我们并不需要严格的速率,并且需要有一定的应对突发流量的能力,所以会使用令牌桶算法限流。
不管是令牌桶算法还是漏桶算法都可以用延迟计算的方式来实现,延迟计算指的是不需要单独的线程来定时生成令牌或者从漏桶中定时取出请求,而是由调用限流器的线程自己去计算是否有足够的令牌以及需要sleep的时间,延迟计算的方式可以节省一个线程资源。Guava提供的RateLimiter
类就是以延迟计算的方式实现限流。
RateLimiter
实现原理
先看下RateLimiter
类图:
可以看出有两层抽象,RateLimiter
类本身是一个抽象类,子类SmoothRateLimiter
又做了层抽象,SmoothRateLimiter
有两个子类SmoothBursty
和SmoothWarmingUp
,可以说SmoothWarmingUp
是为了SmoothBursty
的升级版,是为了弥补SmoothBursty
的不足的(详细见后面的分析), 这里以限流核心方法acquire()
为入口,自上而下的从RateLimiter
类说起。
acquire()
原理分析
整个限流实现过程主要分为生产令牌、获取令牌、计算阻塞时间、阻塞线程四步,既然RateLimiter
做了抽象,那么说明提取了共性,在RateLimiter
里的共性是阻塞线程的逻辑,所以在acquire()
里将阻塞线程这个共性点提取了出来,而将具体生产令牌、获取令牌、计算阻塞时间的细节由子类SmoothRateLimiter
去实现,先看下RateLimiter
类的acquire()
方法代码:
@CanIgnoreReturnValue
public double acquire() {
// 获取一个令牌
return acquire(1);
}
public double acquire(int permits) {
// 预支令牌并获取需要阻塞的时间
long microsToWait = reserve(permits);
// 根据microsToWait来sleep线程(共性)
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
// 生产令牌、获取令牌、计算阻塞时间细节由子类实现
abstract long reserveEarliestAvailable(int permits, long nowMicros);
接着看二级抽象类SmoothRateLimiter
对reserveEarliestAvailable(int permits, long nowMicros)
的实现逻辑,我们先看下这个类的几个重要属性:
nextFreeTicketMicros
:下一次请求被允许的时间。当令牌数不足时,需要由当前请求的线程负责延迟计算出令牌的生产数及耗时并更新这个值,即使需要等待,当前线程也不会去阻塞等待,而是提前预支令牌,而这个预支的代价会转嫁给下一个请求,这样做的目的是为了减少线程阻塞,详细见后面源码分析stableIntervalMicros
:每产生一个令牌需要消耗的微秒数,这个值是根据构造器传入的permitsPerSecond
换算成微秒数得来(1秒=1000000微秒)maxPermits
:桶中允许存放的最大令牌数storedPermits
:桶中当前缓存的未消耗的令牌数,当令牌消耗速度小于令牌产生速度时,桶内就会开始堆积令牌,但是这个数不会大于maxPermits
通过这几个属性已经能看出reserveEarliestAvailable(int permits, long nowMicros)
方法的核心逻辑,详细代码如下:
@Override
final long reserveEarliestAvailable(int<