Guava RateLimiter源码深度解析

原文链接https://blog.csdn.net/superscorpio/article/details/86598984

简单使用

参考官方给的示例,可以直接调用acquire,默认是获取1个令牌,也可以通过参数指定令牌数量。

//每秒限流2个的RateLimiter
final RateLimiter rateLimiter = RateLimiter.create(2.0);

//限制提交任务给线程池的速率
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}}
// 每秒限流5000的RateLimiter,这里的5000是数据包的字节数
final RateLimiter rateLimiter = RateLimiter.create(5000.0); 

void submitPacket(byte[] packet) {
    //aquire带有参数,获取数据包长度的令牌数
    rateLimiter.acquire(packet.length);
    networkService.send(packet);
}}

 

源码解析

RateLimiter是一个抽象类,子类SmoothRateLimiter实现了主要功能,SmoothRateLimiter的子类分别是SmoothWarmingUp和SmoothBursty,SleepingStopwatch是一个带sleep的计时器。本次主要分析SmoothBursty相关的实现。

RateLimiter的方法

以上方法主要分几类:1、创建,2、获取令牌或者尝试获取令牌;3、计算令牌;4、getter、setter;5、其他辅助比如互斥量、校验

RateLimiter的抽象方法

abstract double doGetRate();

abstract void doSetRate(double permitsPerSecond, long nowMicros);

abstract long queryEarliestAvailable(long nowMicros);

abstract long reserveEarliestAvailable(int permits, long nowMicros);

 

创建

4个create方法都是static,可通过RateLimiter.create传入参数直接创建,主要参数是double类型的rate,使用double类型能够支持更细粒度的速率,比如0.1/s。

 

代码解析

根据示例代码,每次获取令牌都调用acquire方法,就从这里开始。

acquire

/**
   * 获取一个令牌,取不到就阻塞。返回一个睡眠时间,单位是秒。
   * 如果没有令牌可用,acquire会自己sleep,调用方得到返回值之后不需要sleep。
   */
  @CanIgnoreReturnValue
  public double acquire() {
    return acquire(1);
  }
  @CanIgnoreReturnValue
   //从当前RateLimiter获取给定数量的令牌,阻塞到获取请求可以通过。
    //它会返回一个以秒记的睡眠时间,如果有的话。
    //返回值是可以忽略的,因为方法内部已经处理了等待时间,调用方不需要sleep
  public double acquire(int permits) {
    //获取指定数量的令牌,得到一个需要等待的微秒数
    long microsToWait = reserve(permits);
    //睡眠并且不响应interrupt
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    //转换成秒,先乘以1.0变成浮点数,避免整数运算丢失精度
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

aquire中最重要的还是reserve,具体如下:

//从当前RateLimiter中获取给定数量的令牌,返回当前时刻到令牌可用时刻的微秒数
final long reserve(int permits) {
    //检查参数,没什么可说的
    checkPermits(permits);
    //RateLimiter是线程安全的,获取互斥量,全局使用一个延迟初始化的互斥量
    synchronized (mutex()) {
      //获取令牌数量和等待时间 
      //stopwatch.readMicros()其中的实现是读取当前的系统时间
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
}

这些方法都很短小,至于是否内聚性够高,职责够明确,还需要自行体会。

//预留令牌并返回调用者必须等待的时间
final long reserveAndGetWaitLength(int permits, long nowMicros) {
    //返回这些令牌最早可用的时刻
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    //最早可用时刻-当前时刻就是等待时间,如果小于0则取0
    return max(momentAvailable - nowMicros, 0);
}
//抽象方法,需要看子类实现
abstract long reserveEarliestAvailable(int permits, long nowMicros);

唯一的子类SmoothRateLimiter了,子类的实现是final的。

先看看子类的field

//当前桶里已有的令牌数。之前一个时间段没用掉的令牌存着留着将来用。
double storedPermits;

//最大可保存的令牌数,即令牌桶的容量
double maxPermits;

//两个请求之间的静态间隔,比如每秒限速5个请求,静态间隔就是200ms
//以买小笼包为例,蒸一笼包子要多久
//感慨一下,这个命名真的很用心。
double stableIntervalMicros;

//下一个令牌可用的时间。
//一旦给一个请求发令牌,这个时间就会被推后,请求数量越大,这个时间被推后的越远。

//这个时间也可能是过去的某个值。
//以上面每秒5个请求为例,假设第一个请求在T时刻拿到令牌,则第二个能拿到令牌的时间T+200ms
//但是这时请求可能没有到来(比如说没有用户点击或者没有任务),
//然后等到T+500ms时,nextFreeTicketMicros就是过去的值
private long nextFreeTicketMicros = 0L; // could be either in the past or future

 

到这里可以终于看到桶容量、桶里的令牌数。另外,因为匀速放令牌入桶,所以用初始速率算出来一个固定时间间隔,这样用(当前时间-上一次发令牌时间)/时间间隔就能算出这段时间的产生的令牌数了。

 

核心逻辑从这里开始,先看看resync,它只关注产生令牌的逻辑

//根据当前时间更新桶里的令牌数和下一个可用令牌的时间,
//需要注意:resync只关注产生的逻辑,不关注消耗的逻辑,而且只产生过去某时刻到现在的

// 因为是根据当前时间计算的过去那段时间应产生的令牌,所以本次计算完之后,
// 桶里的令牌数不一定够本次acquire的数量,那就只能把nextFreeTicketMicros再推后了
// 但是本方法只负责计算到现在
void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    // 如果上次计算出来的可用令牌时间已经过去了,那这段时间会产生新令牌
    // 上面解释了,第一次取令牌之后过了很久才取第二个,那nextFreeTicketMicros就是过去的时刻
    if (nowMicros > nextFreeTicketMicros) {
      //新令牌数=过去的时间/每个令牌的间隔,在SmoothBursty中coolDownIntervalMicros返回的是间隔
      // 如果想来个风骚的走位,不以固定速率产生令牌,这个coolDownIntervalMicros可以覆盖一下
      // 比如coolDownIntervalMicros可以判断当前时间,夜深人静23:00-5:00间隔长一点。当然更复杂可以去某个存储中读取多个配置高峰期,比如8:00-10:00 0.1ms, 14:00-20:00 1ms
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      
      // 控制桶的容量,多余的就丢掉,
      storedPermits = min(maxPermits, storedPermits + newPermits);
      
      // 对过去一段时间产生了令牌之后,下一个可用令牌时间就变成现在了
      // 再次明确:本方法只关注产生逻辑,所以nextFreeTicketMicros回被更新到当前
      nextFreeTicketMicros = nowMicros;
    }

    //如果nowMicros <= nextFreeTicketMicros,说明还没到产生的时间,本次啥也不干
 }

再看看整个产生和消费的逻辑

@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    //产生上次到现在令牌并更新时间。每次消费之前先产生令牌,本方法只负责产生不考虑消费
    resync(nowMicros);
    
    //从这里开始处理消费了
    long returnValue = nextFreeTicketMicros;
    
    //桶里产生了很多令牌,storedPermitsToSpend表示本次要被用掉的数量。
    //如果桶里的令牌数不够,那差额就只能耗费一定的时间等产生
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    
    //需要新产生的令牌数
    //早上在店里买包子也是一样,刚蒸好的都给你,还不够的话就等下一笼吧。如果够了,freshPermits是0
    double freshPermits = requiredPermits - storedPermitsToSpend;
    
    //freshPermits * stableIntervalMicros 产生这几个新令牌需要的等待时间
    //SmoothBursty的storedPermitsToWaitTime实现返回0,
    //新产生令牌需要的时间waitMicros=数量*间隔即freshPermits * stableIntervalMicros 
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);
    
    //下一次能取得令牌的时间,赋值之前nextFreeTicketMicros就是当前时刻,加上等待时间就是下一个时刻。
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    
    // 本次获取令牌之后,更新桶里最后剩余的令牌
    //因上面有min操作,故storedPermitsToSpend小于等于storedPermits,最后storedPermits>=0
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
}
上面的分析以SmoothBursty为主,SmoothWarmingUp的后面再补上。

补充资料

 

 

 

  • 3
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值