RateLimiter是Google Guava框架的一个限速器,通常用于控制对某个资源的访问速率。
限速常见的有两种实现方式,一种是令牌桶,另一种是漏桶。
RateLimiter选择了令牌桶作为其底层实现,按照固定速率投放令牌,同时支持突发流量。
在上一篇中,我们先简单解读了Ticker和Stopwatch,它们是RateLimiter底层时间计算的基础。
传送门:《Google Guava之RateLimiter核心源码解读(上)》
这一篇,我们将要解读的是创建RateLimiter实例和获取许可的核心代码,涉及到RateLimiter、SmoothRateLimiter和SmoothBursty。
代码仍然是基于Guava 23.0版本。
在进行源码分析之前,先来看一个简单的例子,找点感觉。
示例
@Test
public void testFirst() {
RateLimiter rateLimiter = RateLimiter.create(1);
while (true) {
System.out.println(rateLimiter.acquire(1));
}
}
/*
执行结果:
0.0
0.993779
0.976652
0.967317
0.9815
0.999405
0.997789
...
*/
在这段代码中,首先创建了一个RateLimiter实例,然后通过while循环不断从限速器中获取许可。
其中,RateLimiter.create(1)表示创建一个RateLimiter实例,每秒的许可数为1。也就是说,如果想从限速器中获得许可,那么每秒最多只能获取1个。
rateLimiter.acquire(1)表示从限速器中获取1个许可,该方法会阻塞直到获得了指定的许可数。
执行程序输出的结果,表示每次获得许可所等待的时间,单位为秒。显然,每获得1个许可需要花费约1秒的时间。这个1秒,就是在创建RateLimiter时,create方法传入的1,即限速器每秒可发放的许可数。
RateLimiter的核心设计思想
RateLimiter的一个主要特性,是能够提供稳定(或固定)的速率。对于这一点,可以通过在初始化时,传入一个期望的速率值。然后根据这个值,计算出一个具体的限制时间,使得线程在获取许可时进行相应的等待。
那么,具体要如何来实现对速率的控制呢?
最简单的方法,就是保留最后一次发放许可给线程请求的时间戳,并且确保在1/QPS秒之后才允许下一次请求许可通过。
其中,QPS指每秒的最大许可数,即每秒最多允许发放多少个许可。而1/QPS,表示每发放1个许可的间隔时间。
例如,对于QPS为5的速率(即每秒发送5个许可),我们只要确保距最后一次准许请求通过的时间差达到200ms,才发放下一个许可,即可满足预期的速率要求。
如果一个新的请求到来了,但距最后一个请求被批准的时间仅过了100ms,那么本次请求需要再等待100ms。
因此,以这样的速率,发送15个许可,总共需要3秒(15除以5)。
如果一个请求速率较高的任务到达一个比较空闲的RateLimiter,那么该任务将立即被授予指定的许可。然而,下一个请求将会受到额外的限制,因为前一个请求预取了一部分许可,需要由本次请求来承担额外的等待时间。
这其实就是RateLimiter支持突发访问流量的体现,但这同时也说明了RateLimiter并不提供多个请求间的公平保证。(想想先来的请求可能不需要等待,而下一个却要为前一个请求进行额外的等待)
基于以上的设计,如果RateLimiter长时间未使用,或者比较空闲的时候,那么空闲期间的许可我们是没有使用到的,这样将会被造成浪费,可能导到资源利用率低等问题。
为了充分利用资源,将空闲期间的许可给利用起来,需要有一个地方来记录空闲期间的许可数。在RateLimiter中,使用storedPermits来存储未被使用的许可数。
默认的RateLimiter配置可以保存最多一秒钟的未使用许可。
RateLimiter类图
RateLimiter的类关系图如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SDBwuOe4-1601827666832)(https://binarylife.icu/wp-content/uploads/2020/09/RateLimiter-Class-Diagram.png)]
RateLimiter和SmoothRateLimiter都是抽象类,SmoothRateLimiter继承了RateLimiter。
SmoothBursty和SmoothWarmingUp都继承SmoothRateLimiter,是RateLimiter的最终实现类。
同时,SmoothBursty和SmoothWarmingUp直接在SmoothRateLimiter内部定义,都是静态的内部类。
因此,在创建RateLimiter的具体实例时,实际上是在内部创建SmoothBursty或SmoothWarmingUp实例。
核心源码解读
创建RateLimiter实例
public abstract class RateLimiter {
/**
* 创建一个RateLimiter实例,并设置每秒的许可数.
*/
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
/**
* 创建一个默认实现为SmoothBursty的RateLimiter实例.
*
* @param permitsPerSecond 每秒许可数
* @param stopwatch 支持sleep的秒表
* @return
*/
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
// 创建一个RateLimiter实例,默认实现为SmoothBursty. 突发时间为1s
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond); // 设置速率,即每秒的许可数
return rateLimiter;
}
}
其中,SleepingStopwatch.createFromSystemTimer()表示创建一个支持sleep方法的秒表。SleepingStopwatch有两个方法,一个是获取从启动限速器到当前的时间,另一个是执行不可中断的睡眠。
public static final SleepingStopwatch createFromSystemTimer() {
return new SleepingStopwatch() {
// 初始化秒表
final Stopwatch stopwatch = Stopwatch.createStarted();
/**
* 从启动到现在经过的微秒数。相当于ticker.read() - startTick,即"当前纳秒 - 启动时的纳秒",然后转换为微秒。
*/
@Override
protected long readMicros() {
return stopwatch.elapsed(MICROSECONDS);
}
/**
* 不可中断的进行sleep,时间为微秒。<br>
* 实际上,如果遇到中断,会先忽略中断异常,继续sleep,直到等待了指定时长后才结束。
*/
@Override
protected void sleepMicrosUninterruptibly(long micros) {
if (micros