基于guava-29.0版本。
RateLimiter是一个基于令牌桶算法实现的限流器,常用于控制网站的QPS。与Semaphore不同,Semaphore控制的是某一时刻的访问量,RateLimiter控制的是某一时间间隔的访问量。
类结构
RateLimiter的类结构如下图所示:
RateLimiter是一个抽象类。
SmoothRateLimiter是RateLimiter的子类,也是一个抽象类。
SmoothBursty和SmoothWarmingUp是定义在SmoothRateLimiter里的两个静态内部类,是SmoothRateLimiter的真正实现类。
先来说一下RateLimiter的一个重要设计原则——透支未来令牌
如果说令牌池中的令牌数量为x,某个请求需要获取的令牌数量是y,只要x>0,即使y>x,该请求也能立即获取令牌成功。但是当前请求会对下一个请求产生影响,即会透支未来的令牌,使得下一个请求需要等待额外的时间。
举个例子,假设一个RateLimiter的QPS设定值是1,如果某个请求一次性获取10个令牌,该请求能够立即获取令牌成功,但是下一个请求获取令牌时,就需要额外等待10s时间。
示例程序:
RateLimiter rateLimiter = RateLimiter.create(1);
System.out.println(String.format("Get 10 tokens spend %f s", rateLimiter.acquire(10)));
System.out.println(String.format("Get 1 token spend %f s", rateLimiter.acquire(1)));
输出结果:
Get 10 tokens spend 0.000000 s
Get 1 token spend 9.997415 s
再来大致说一下SmoothBursty和SmoothWarmingUp的区别
1. SmoothBursty初始化的时候令牌池中的令牌数量为0,而SmoothWarmingUp初始化的时候令牌数量为maxPermits。
2. SmoothBursty从令牌池中获取令牌不需要等待,而SmoothWarmingUp从令牌池中获取令牌需要等待一段时间,该时间长短和令牌池中的令牌数量有关系,具体见下图:
上图中slope表示绿色实线的斜率,其计算方式如下:
slope = (stableIntervalMicros * coldFactor - stableIntervalMicros) / (maxPermits - thresholdPermits)
上图中横坐标是令牌池中的令牌数量,纵坐标是从令牌池中获取一个令牌所需的时间,因此红色实线对应的矩形面积、绿色实线对应的梯形面积的单位都是时间。
因此预热时间warmupPeriodMicros的定义如下(梯形面积):
从满状态的令牌池中取出(maxPermits - thresholdPermits)个令牌所需花费的时间。
至于为什么矩阵面积是梯形面积的0.5倍,在后续SmoothWarmingUp的代码实现里我们会看到。
假设当前令牌池中有x个令牌,
当x介于thresholdPermits和maxPermits之间时,SmoothWarmingUp从令牌池中获取一个令牌,需要等待的时间为:
stableIntervalMicros + (x - thresholdPermits) * slope
当x介于0和thresholdPermits之间时,SmoothWarmingUp从令牌池中获取一个令牌,需要等待的时间为:
stableIntervalMicros
上述情况发生在令牌池中令牌数量大于0,且前一个请求没有透支令牌时。如果前一个请求透支了令牌,还需要加上额外的等待时间。
记住SmoothWarmingUp的这张图和这几个关键公式,后续代码里会看到。
SmoothWarmingUp当前请求获取令牌的等待时间是由下一个请求承担的
示例程序:
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true) {
System.out.println(String.format("Get 10 tokens spend %f s", r.acquire(10)));
System.out.println(String.format("Get 10 tokens spend %f s", r.acquire(10)));
System.out.println(String.format("Get 10 tokens spend %f s", r.acquire(10)));
System.out.println(String.format("Get 10 tokens spend %f s", r.acquire(10)));
System.out.println("end");
}
输出结果
Get 10 tokens spend 0.000000 s
Get 10 tokens spend 6.498113 s
Get 10 tokens spend 4.995956 s
Get 10 tokens spend 4.996605 s
end
Get 10 tokens spend 4.994804 s
Get 10 tokens spend 4.999365 s
Get 10 tokens spend 4.996274 s
Get 10 tokens spend 4.999943 s
end
...
在这个例子中,我们新建了一个SmoothWarmingUp,其QPS是2,预热时间是3s。
- 第一次获取10个令牌时,无需等待额外的时间,因为无论是透支令牌产生的额外等待时间还是SmoothWarmingUp从令牌池中取令牌产生的额外等待时间,都由下一个请求来承担。
此时:
thresholdPermits = 3.0
storedPermits = 6.0
stableIntervalMicros = 0.5s
透支令牌产生的额外等待时间是:
(10 - storedPermits) * stableIntervalMicros = 2s
SmoothWarmingUp从令牌池中取令牌产生的额外等待时间是:
warmupPeriodMicros + warmupPeriodMicros * 0.5 = 4.5s
因此第一次请求对下一个请求造成的影响是使得下一个请求需要等待6.5s。
- 第二次获取10个令牌时,等待了6.5s,和我们分析的结果相同,这6.5s是第一个请求造成的影响,而第二次请求造成的额外等待时间,由第三次请求来承担。
此时:
storedPermits = 0.0
透支令牌产生的额外等待时间是:
(10 - storedPermits) * stableIntervalMicros