Guava RateLimiter使用和源码分析

使用

示例

   RateLimiter limiter = RateLimiter.create(5); // 这里的1表示每秒允许处理的量为1个
    for (int i = 1; i <= 10; i++) { 
        limiter.acquire();// 请求RateLimiter, 超过permits会被阻塞
    }
double waitTime=rateLimiter.acquire(1);//acquire方法传入的是需要的令牌个数,当令牌不足时会进行等待,该方法返回的是等待的时间
boolean isAcquire = limiter.tryAcquire(); //立刻失败
boolean isAcquire = limiter.tryAcquire(100, TimeUnit.MILLISECONDS);//等待一段时间后返回失败

需要注意的是,当令牌不足时,acquire方法并不会阻塞本次调用,而是会算在下次调用的头上
比如第一次调用时,令牌桶中并没有令牌,但是第一次调用也没有阻塞,而是在第二次调用的时候阻塞了1秒。
也就是说,每次调用欠的令牌(如果桶中令牌不足)都是让下一次调用买单

https://blog.csdn.net/fanrenxiang/article/details/80949079
Quick Guide to the Guava RateLimiter | Baeldung

原理

Guava的RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
[image:9A478AC0-7A3D-4615-8C13-414D39F053C5-35974-0002F48FC4E6F157/153F211F-1D78-4444-B6C6-8A0D527AA49B.png]

以SmoothBursty 为例,SmoothBursty中的几个关键字段:

// 桶中最多存放多少秒的令牌数
final double maxBurstSeconds;
//桶中的令牌个数
double storedPermits;
//桶中最多能存放多少个令牌,=maxBurstSeconds*每秒生成令牌个数
double maxPermits;
//加入令牌的平均间隔,单位为微秒,如果加入令牌速度为每秒5个,则该值为200ms
double stableIntervalMicros;
//下一个请求需要等待的时间
private long nextFreeTicketMicros = 0L;

获取令牌acquire函数如下所示,它会调用reserve函数计算获取目标令牌数所需等待的时间,然后使用SleepStopwatch进行休眠,最后返回等待时间。

public double acquire(int permits) {
    // 计算获取令牌所需等待的时间
  long microsToWait = reserve(permits);
    // 进行线程sleep
  stopwatch.sleepMicrosUninterruptibly(microsToWait);
  return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
  checkPermits(permits);
  // 由于涉及并发操作,所以使用synchronized进行并发操作
  synchronized (mutex()) {
    return reserveAndGetWaitLength(permits, stopwatch.readMicros());
  }
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
    // 计算从当前时间开始,能够获取到目标数量令牌时的时间
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    // 两个时间相减,获得需要等待的时间
    return max(momentAvailable - nowMicros, 0);
}

可以看出主要逻辑都在reserveEarliestAvailable方法中;
reserveEarliestAvailable是刷新令牌数和下次获取令牌时间nextFreeTicketMicros的关键函数。
它有三个步骤:

  1. 一是调用resync函数增加令牌数,
  2. 二是计算预支付令牌所需额外等待的时间,
  3. 三是更新下次获取令牌时间nextFreeTicketMicros和存储令牌数storedPermits。
    这里涉及RateLimiter的一个特性,也就是可以预先支付令牌,并且所需等待的时间在下次获取令牌时再实际执行。详细的代码逻辑的解释请看注释。
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
  // 刷新令牌数,相当于每次acquire时在根据时间进行令牌的刷新
  resync(nowMicros);
  long returnValue = nextFreeTicketMicros;
    // 目前即可得到的令牌数=min(请求目标令牌数,当前已有的令牌数)
  double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    // 需要预先支付的令牌=目标令牌数-目前即可得到的令牌数
  double freshPermits = requiredPermits - storedPermitsToSpend;
  //watiMicros=预先支付的令牌所需等待的时间=需要预先支付的令牌*加入令牌的平均间隔
  long waitMicros =
      storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
          + (long) (freshPermits * stableIntervalMicros);
  // 更新nextFreeTicketMicros,本次预先支付的令牌所需等待的时间让下一次请求来实际等待。
  this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
  // 更新令牌数,最低数量为0
  this.storedPermits -= storedPermitsToSpend;
  // 返回旧的nextFreeTicketMicros数值,无需为预支付的令牌多加等待时间。
  return returnValue;
}
//storedPermitsToWaitTime在SmoothWarmingUp和SmoothBuresty的实现不同,用于实现预热缓冲期 
//SmoothBuresty 实现直接返回0
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
    return 0L;
}
//刷新令牌数storedPermits和下次请求需要等待的时间nextFreeTicketMicros
void resync(long nowMicros) {
  // if nextFreeTicket is in the past, resync to now
  if (nowMicros > nextFreeTicketMicros) {
    //获取新增令牌数=(当前时间-下次请求需要等待的时间)/加入令牌的平均间隔
    double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
    storedPermits = min(maxPermits, storedPermits + newPermits);
    nextFreeTicketMicros = nowMicros;
  }
}
double coolDownIntervalMicros() {
    return stableIntervalMicros;
}

其中 ::double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();::
就是新增令牌的核心逻辑。当前时间大于nextFreeTicketMicros时进行刷新,否则直接返回。

下面我们举个例子,让大家更好的理解resync和reserveEarliestAvailable函数的逻辑。
比如RateLimiter的stableIntervalMicros为500,也就是1秒发两个令牌,storedPermits为0,
nextFreeTicketMicros为155391849 5748。
线程一acquire(2),当前时间为155391849 6248,首先resync函数计算,(155391849 6248 - 155391849 5748)/500 = 1,所以当前可获取令牌数为1,但是由于可以预支付,所以
nextFreeTicketMicros= nextFreeTicketMicro + 1 * 500 = 155391849 6748。线程一无需等待。
线程二也来acquire(2),首先resync函数发现当前时间早于nextFreeTicketMicros,所以无法增加令牌数,所以需要预支付2个令牌, nextFreeTicketMicros= nextFreeTicketMicro + 2 * 500 = 155391849 7748。
线程二需要等待155391849 6748时刻,也就是线程一获取时计算的nextFreeTicketMicros时刻。
同样的,线程三获取令牌时也需要等待到线程二计算的nextFreeTicketMicros时刻。

来谈谈限流-RateLimiter源码分析 - 简书
使用Guava RateLimiter限流以及源码解析 - 简书

©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

相关阅读更多精彩内容

  • 前言 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流 缓存 缓存的目的是提升系统访问速度和增大系统处理...
    人在码途阅读 51,486评论 4 57
  • 在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流 缓存 缓存的目的是提升系统访问速度和增大系统处理容量 ...
    tracy_668阅读 4,563评论 0 3
  • 缓存,降级和限流是大型分布式系统中的三把利剑。目前限流主要有漏桶和令牌桶两种算法。 缓存:缓存的目的是减少外部调用...
    zhong0316阅读 8,864评论 0 5
  • 前言 最近需要在网关层做一个限流的需求,由于需要对一个机房内的集群做统一的限流管理,所以可能需要用到redis,而...
    ro9er阅读 7,661评论 1 12
  • 1. 我不可以随便开玩笑 不可以发脾气 无论自己多委屈 也不许发脾气 只能撒娇 总之不能让对方不开心 不要让对方难...
    沐一佳阅读 1,391评论 0 1

友情链接更多精彩内容