谷歌Guava限流工具RateLimiter

本文深入解析Guava的RateLimiter,它基于令牌桶算法实现限流。RateLimiter分为SmoothBursty和SmoothWarmingUp两种策略,前者初始化时令牌数为0,后者则有预热过程。文章通过实例分析了两种策略的等待时间和令牌获取方式,揭示了它们的设计原理和应用场景。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

基于guava-29.0版本。

RateLimiter是一个基于令牌桶算法实现的限流器,常用于控制网站的QPS。与Semaphore不同,Semaphore控制的是某一时刻的访问量,RateLimiter控制的是某一时间间隔的访问量。

类结构

RateLimiter的类结构如下图所示:

image.png

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从令牌池中获取令牌需要等待一段时间,该时间长短和令牌池中的令牌数量有关系,具体见下图:

image.png

上图中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 
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值