Guava的RateLimiter源码探究

admin 2025年4月9日20:41:18评论17 views字数 5348阅读17分49秒阅读模式

Guava RateLimiter是一个谷歌提供的限流工具,可以有效限定单个JVM实例上某个接口的流量。

RateLimiter抽象类提供限流的所有功能,它的实现类只有SmoothRateLimiter。而SmoothRateLimiter的具体策略又由它的两个内部子类来实现。

Guava的RateLimiter源码探究

SmoothBursty:兼容突发流量的令牌桶实现,也就是经典的令牌桶算法。

SmoothWarmingUp:带预热过程的令牌桶实现,即在桶较满时产生令牌的速度较慢,随着令牌的消耗慢慢增长到恒定速率。如下所示,x轴为令牌数,y轴为延迟,从右向左看的梯形区域就是令牌消耗的预热过程。

public static RateLimiter create(double permitsPerSecond) {    return create(permitsPerSecond, RateLimiter.SleepingStopwatch.createFromSystemTimer());}@visibleForTestingstatic RateLimiter create(double permitsPerSecond, RateLimiter.SleepingStopwatch stopwatch) {    RateLimiter rateLimiter = new SmoothBursty(stopwatch,maxBurstSeconds: 1.0D);     rateLimiter.setRate(permitsPerSecond);     return rateLimiter;}public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) {    Preconditions.checkArgument(b: warmupPeriod >= 0L,errorMessageTemplate:"warmupPeriod must not be negative:%s", warmupPeriod);     return create(permitsPerSecond,warmupPeriod, unit,coldFactor: 3.0D,RateLimiter.SleepingStopwatch.createFormSystemTimer();}@VisibleForTestingstatic RateLimiter create(double permitsPerSecond,long warmupPeriod, TimeUnit unit, double coldFactor, SleepingStopwatch stopwatch) {    RateLimiter rateLimiter = new SmoothRateLimiter.SmoothWarmingUp(stopwatch,warmupPeriod, unit, coldFactor);    rateLimiter.setRate(permitsPerSecond);     return rateLimiter;}

由上看出,通过RateLimiter的重载create()方法,返回不同的实现类。

SmoothBursty

以SmoothBursty为例。首先看SmoothRateLimiter四个参数的含义:

abstract class SmoothRateLimiter extends RateLimiter {    double storedPermits;     double maxPermits;    double stableIntervalMicros;    private long nextFreeTicketMicros;}

storedPermits:当前桶里有的令牌数量。

maxPermits:桶装满时的令牌数量,storedPermits不会比它大。

stableIntervalMicros:产生令牌的频率(时间间隔),单位为微秒。举个栗子,如果我们想限制系统的QPS为10,亦即每秒有10个令牌放入桶中,那么stableIntervalMicros的值就是100000。

nextFreeTicketMicros:下一个令牌可用的时间戳,也就是下一个请求能够被处理的时间戳,单位为微秒。该值会随着当前请求获得令牌而增大(因为时间是自然流动的)。若当前请求的令牌数超出可用令牌数,这个时间就被推后(需要时间产生新的令牌)。当然,如果有一段时间没有请求进入的话,它就会保持在上次请求的过去时间戳。

从后往前看获取令牌的过程:首先,RateLimiter提供了acquire()的默认实现,acquire()是阻塞式获得令牌的一种方法,其逻辑为:首先由reserve方法计算出需要获取指定数量领牌的时间,再sleep该时间。可以看出它的核心是reserve()方法。

@CanIgnoreReturnValuepublic double acquire() {    return this.acquire(permits:1);}@CanIgnoreReturnValuepublic double acquire(intpermits) {    long microsToWait = this.reserve(permits);    this.stopwatch.sleepMicrosUninterruptibly(microsToWait);    return 1.0D * (double)microsToWait / (double)TimeUnit.SECONDS.toMicros(duration:1L);}final long reserve(intpermits) {    checkPermits(permits);    synchronized(this.mutex()){        return this.reserveAndGetWaitLength(permits,this.stopwatch.readMicros());    }}

可以看出,reserve方法是通过同步加锁的方式来获取令牌的。其中加锁的对象是mutex()返回的一个单例对象:

private Object mutex() {    Object mutex = this.mutexDoNotUseDirectly    if (mutex == null) {        synchronized(this) {            mutex = this.mutexDoNotUseDirectly            if (mutex == null) {                this.mutexDoNotUseDirectly = mutex = new Object();            }        }    }    return mutex;}

回到reserve()方法,继续向上溯源:

final long reserveAndGetWaitLength(intpermitslong nowMicros) {    long momentAvailable = this.reserveEarliestAvailable(permits,nowMicros);     return Math.max(momentAvailable - nowMicros, OL);}abstract long queryEarliestAvailable(long var1);abstract long reserveEarliestAvailable(int var1, long var2)

reserveAndGetWaitLength方法的作用是预定令牌并且返回需要等待的时间。acquire和tryAcquire的底层调用的都是该方法。

最终,可以看出,RateLimiter交给其SmoothRateLimiter类实现的是reserveEarliestAvailable方法。

SmoothRateLimiter仍然是一个抽象类,它提供了部分方法的默认实现,比如setRate和reserveEarliestAvailable。从方法名中我们就可以看出来,reserveEarliestAvailable的功能是预定令牌。

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {    this.resync(nowMicros);    long returnValue = this.nextFreeTicketMicros;    double storedPermitsToSpend = Math.min((double)requiredPermits,this.storedPermits);     double freshPermits = (double)requiredPermits - storedPermitsToSpend;    long waitMicros = this.storedPermitsToWaitTime(this.storedPermits,storedPermitsToSpend) + (long)(freshPermits * this.stableIntervalMicros);     this.nextFreeTicketMicros = LongMath.saturatedAdd(this.nextFreeTicketMicros,waitMicros);     this.storedPermits -= storedPermitsToSpend;     return returnValue;}

其逻辑为:

  • 首先resync方法产生令牌;

  • 然后将需要的令牌的数量和桶中已有的令牌数量比较,作差,即得出还需要多少令牌,生产这些令牌还需多少时间(waitMicors),这里计算的等待时间包括两部分,一部分是获取已存在令牌时需要消耗的时间,另一部分是生成预获取令牌数需要消耗的时间。SmoothBursty和SmoothWarmingUp对于计算已获取令牌需要消耗的时间存在着不同的实现方式。对于稳定限流器来说,它是可以应对突增请求的,因此在获取已存在令牌时是不应该消耗时间的,因此SmoothBursty类中的storedPermitsToWaitTime方法直接return了0。对于预热限流器来说,它有一个预热的阶段。如果像稳定限流器一样,获取已存在的令牌不需要消耗时间,那么他就没有了预热效果。因此它在获取桶中已存在的令牌时,是需要计算消耗时间的。这样既保证下一次请求需要等待一段时间再通过,也可以在这段时间里再生成一些令牌,进而保证他的预热效果。

  • 然后根据所需的时间,将nextFreeTicketMicros向后推延。即起到了“预定”的效果。同时预扣减令牌库存。

resync方法详解

voidresync(long nowMicros) {    if (nowMicros > nextFreeTicketMicros) {        double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();        storedPermits = min(maxPermits, storedPermits + newPermits);        nextFreeTicketMicros = nowMicros;    }  }  // SmoothBursty.coolDownIntervalMicros()@OverridedoublecoolDownIntervalMicros() {    return stableIntervalMicros;}

生成令牌的逻辑为:

首先判断当前时间是否超过了下一个令牌时间戳,即可以处理请求的时间戳。

如果没有超过令牌时间戳,则无需更新令牌桶状况,此时桶内情况可以直接参与预定令牌的时间计算,得到的需要等待的时间。

如果当前时间已经超过了该时间戳,则会计算出从时间戳到当前时间共产生了多少令牌,更新桶中的情况和时间戳。结合上文,此时的时间戳还需要加上未来成产所需令牌的时间,才会是下一次可处理请求的时间。

可见,RateLimiter的令牌是延迟(lazy)生成的,也就是说每次受理当前请求时,如果系统已经空闲了一定时间,才会计算上次请求到当前时间应该产生多少个令牌,而不是使用单独的任务来定期产生令牌——因为定时器无法保证较高的精度,并且性能不佳。当然,如果令牌已经“超支”,当前就不需要再更新令牌了。

extra: semaphore信号量也有着类似于令牌桶的概念,可以对比学习。

......

Guava的RateLimiter源码探究

文章作者 | 张德轩

原文始发于微信公众号(EBCloud):Guava的RateLimiter源码探究

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年4月9日20:41:18
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Guava的RateLimiter源码探究https://cn-sec.com/archives/3934998.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息