【Java】guava(四) RateLimiter

本文详细介绍了Guava包下的RateLimiter限流工具,基于令牌桶算法实现,通过控制资源访问频率来防止系统过载。文章解析了其内部工作原理,包括令牌生成、等待时间和超时设置等关键机制。

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

说到限流,大体上可以分为两种实现。

1.漏桶或者令牌桶;思路之前介绍过:https://blog.youkuaiyun.com/u010900754/article/details/88568881

2.计数器;

关于计数器,这里简单介绍下。

大体是,维护一个整型变量count,每一次调用都以cas的方式将count减一,调用完将count加一。整体上是这样,当然里面有很多诸如同步,多线程等细节问题需要考虑。

所以它的思路是限定同一时刻访问某一资源的数目。

 

这次介绍的RateLimiter是guava包下的一个很好用的限流工具,模型是基于令牌桶的思路实现的。

使用

public static void main(String args[]) {
        RateLimiter rateLimiter = RateLimiter.create(1);
        for (int i = 0 ; i < 5 ; i++) {
            rateLimiter.acquire();
            new Thread(() -> {
                System.out.println(LocalDateTime.now());
            }).start();
        }
    }

输出:

2019-07-06T23:06:32.765
2019-07-06T23:06:33.647
2019-07-06T23:06:34.648
2019-07-06T23:06:35.647
2019-07-06T23:06:36.649

直接调用create方法构建一个限流器,入参就是qps。调用时直接acquire即可,当然可以acquire多个。

 

原理

单看令牌桶的逻辑含义,似乎需要实现一个定时器的功能,每隔多少秒往桶里放一个令牌。如果直接按照这个思路来设计,那么每一个限流器都要一个定时器,资源代价略高。你可能会说不就一个定时器吗?大型项目中开一个线程可能都需要定夺很久,对资源的消耗需要很苛刻的要求,否则会影响性能。更致命的这种思路的定时器是O(n)量级的,一个未知数,鬼知道会用多少个限流器,所以这种设计不是良好的设计。

guava使用了另一种设计思路。大致如下:每一次获取,都记录下一次可以获取的时刻next;那么下一次获取时,就会判断是否达到了next时刻,如果没有就sleep,直到到达next时刻。当然里面还有很多设计的细节,比方说令牌数目怎么存储。

 

源码

 * <p>Rate limiters are often used to restrict the rate at which some physical or logical resource
 * is accessed. This is in contrast to {@link java.util.concurrent.Semaphore} which restricts the
 * number of concurrent accesses instead of the rate (note though that concurrency and rate are
 * closely related, e.g. see <a href="http://en.wikipedia.org/wiki/Little%27s_law">Little's
 * Law</a>).

注释解释了限流器和信号量的区别:

限流器限制的是qps或者说是速率,而信号量限制的是数目。

下面直接看下acquire方法:

  @CanIgnoreReturnValue
  public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

非常简洁明了,第一步计算出需要等待的时间,也就是microsToWait。第二步直接sleep这个时间。

  final long reserve(int permits) {
    checkPermits(permits);
    synchronized (mutex()) {
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

调用reserveAndGetWaitLength方法计算。同时传入当前的时刻。

  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    return max(momentAvailable - nowMicros, 0);
  }

这里调用的reserveEarliestAvailable方法才是真正计算等待时间的函数:

  abstract long reserveEarliestAvailable(int permits, long nowMicros);

这是一个抽象方法,RateLimiter其实也是一个抽象类,该方法的实现是在SmoothRateLimiter类中:

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

    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }
  /** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
  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;
    }
  }

这个方法在干嘛?

是在放令牌。

nextFreeTickMicros是一个成员变量,指的就是下一个可以获取令牌的时刻。这里的逻辑是如果当前时刻已经超过nextFreeTickMicros,那么就可以累积令牌了。因为这意味着距离上一次获取已经过去了一段时间。

那么可以累积多少个令牌呢?

newpermits就是计算新令牌数目的。coolDownIntervalMicros就是每一秒可以生成的令牌数目,其实就等于create时入参的倒数。

  /**
   * Returns the number of microseconds during cool down that we have to wait to get a new permit.
   */
  abstract double coolDownIntervalMicros();

what?这又是一个抽象方法。

其实smoothRateLimiter也有两个实现:

区别在于一些算法的实现。SmoothBursty比较基础,SmoothWarmingUp有一个预热功能,比较复杂些。

这里只看下SmoothBursty:

    @Override
    double coolDownIntervalMicros() {
      return stableIntervalMicros;
    }

  /**
   * The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
   * per second has a stable interval of 200ms.
   */
  double stableIntervalMicros;

这个值就是qps的倒数。

比如create(5),那么限流就是qps=5,那么每秒可以生成0.2个令牌。

再回到之前的rescyn方法,至此已经算出了此次调用之前,限流器生成了多少个新的令牌。

storedPermits也是一个成员变量,记录的是限流器已有的令牌。maxpermits是上限。

  /** The currently stored permits. */
  double storedPermits;

  /** The maximum number of stored permits. */
  double maxPermits;

这里就更新了当前storedPermits数目。

再回到reserveEarliestAvailable方法:

storedPermitsToSpend变量计算出了需要从当前令牌数目中扣除的令牌数,如果不够则需要另外补充freshPermits个令牌。

如果确实需要补充,那么下一次可以获得令牌的时刻就需要延后waitMicros时间。

这个时间由两部分组成,第一部分是通过一个方法计算出来的,这个方法在SmoothBursty里实现就是返回0:

    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
      return 0L;
    }

所以waitMicros直接等于freshPermits * stableIntervalMicros

所以整体上,当前调用需要等待的时间数目就是原本的next时刻加上waitMicros

至此,等待时间也计算出来了。最后更新下nextFreeTicketMicros时刻。

 

再回到之前的acquire方法:

  @CanIgnoreReturnValue
  public double acquire(int permits) {
    long microsToWait = reserve(permits);
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

接下来就直接进入sleep方法了。

 

那么设置等待超时又如何实现呢?

也就是这个api:

 public boolean tryAcquire(long timeout, TimeUnit unit)

这个按理说也是一个定时器,但是基于之前的讨论,实现这个其实很容易。

  public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
    long timeoutMicros = max(unit.toMicros(timeout), 0);
    checkPermits(permits);
    long microsToWait;
    synchronized (mutex()) {
      long nowMicros = stopwatch.readMicros();
      if (!canAcquire(nowMicros, timeoutMicros)) {
        return false;
      } else {
        microsToWait = reserveAndGetWaitLength(permits, nowMicros);
      }
    }
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
    return true;
  }

这里多了一次canAquire的逻辑:

  private boolean canAcquire(long nowMicros, long timeoutMicros) {
    return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
  }

  @Override
  final long queryEarliestAvailable(long nowMicros) {
    return nextFreeTicketMicros;
  }

这里就直接判断下下一次可以访问时间-当前时间是否大于超时时间,如果大于,那么就不能访问。很简单。

 

限流的实现大致就是这样。

整体感觉设计非常简洁,简洁就意味着坚固,坚固是工程中最最重要的指标。很多组件非常坚固耐用,为什么?很大程度上就是设计记录的简单。比方说线程池。

 

总结:

每一次获取,先计算新生成的令牌数目;

再计算当前获取需要等待的时间;

sleep。

 

 

<think>我们讨论的是Guava RateLimiter,它是Google Guava库中提供的一个限流工具,用于控制系统的请求速率。 主要功能:通过限制单位时间内可执行的操作数量,来保护系统免受过载的影响。 实现原理:基于令牌桶算法(Token Bucket)实现。 令牌桶算法原理: - 系统以固定的速率向桶中添加令牌(token)。 - 当有请求到来时,会尝试从桶中取出一个或多个令牌(取决于请求需要的令牌数)。 - 如果桶中有足够的令牌,则请求被允许执行,同时令牌被消耗。 - 如果桶中没有足够的令牌,则请求会被限流(等待或直接拒绝)。 Guava RateLimiter提供了两种限流模式: 1. 平滑突发限流(SmoothBursty):允许突发流量,即如果桶中有足够的令牌,可以一次性取走。 2. 平滑预热限流(SmoothWarmingUp):在系统启动时有一段预热期,逐步将分发令牌的速率提高到配置的速率,避免冷启动时系统被压垮。 下面我们分别给出使用示例,并解释代码。 首先,我们需要在项目中引入Guava依赖(以Maven为例): ```xml <dependency> <groupId>com.google.guava</groupId> <artifactId>guava</artifactId> <version>31.0.1-jre</version> <!-- 请根据实际情况选择版本 --> </dependency> ``` 示例1:使用SmoothBursty(平滑突发限流) ```java import com.google.common.util.concurrent.RateLimiter; public class RateLimiterDemo { public static void main(String[] args) { // 创建一个每秒发放2个令牌的RateLimiter RateLimiter rateLimiter = RateLimiter.create(2.0); // 每秒2个令牌,即每500毫秒一个 // 模拟处理请求 for (int i = 0; i < 5; i++) { // 请求一个令牌,如果令牌不足则等待,直到获取到令牌 double waitTime = rateLimiter.acquire(); // 返回等待的时间(秒) System.out.println("获取令牌成功,等待时间: " + waitTime + "秒, 执行操作" + i); } } } ``` 解释: - RateLimiter.create(2.0) 创建了一个每秒产生2个令牌的限流器。 - acquire()方法会阻塞直到获取到令牌,返回值为等待的时间(以秒为单位)。 - 在循环中,我们连续获取5次令牌。由于令牌产生速率是每秒2个,所以前两次获取可能不需要等待(或等待时间很短),因为一开始桶中有令牌。后面每次获取大约需要等待0.5秒。 示例2:使用SmoothWarmingUp(平滑预热限流) ```java import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.TimeUnit; public class RateLimiterWarmUpDemo { public static void main(String[] args) { // 创建一个每秒发放5个令牌的RateLimiter,预热时间为3秒 // 在预热期内,RateLimiter每秒增加的令牌数会逐步增加,直到达到设定的速率 RateLimiter rateLimiter = RateLimiter.create(5.0, 3, TimeUnit.SECONDS); for (int i = 0; i < 10; i++) { double waitTime = rateLimiter.acquire(); System.out.println("获取令牌成功,等待时间: " + waitTime + "秒, 执行操作" + i); } } } ``` 解释: - RateLimiter.create(5.0, 3, TimeUnit.SECONDS) 创建了一个限流器,目标速率是每秒5个令牌,预热时间为3秒。 - 在预热期间,令牌的分发速率会从慢变快,直到达到设定的速率。这样可以让系统逐渐增加负载,避免在冷启动时突然有大量请求。 另外,RateLimiter还提供了非阻塞的方法tryAcquire: - tryAcquire():尝试获取令牌,立即返回是否成功。 - tryAcquire(int permits, long timeout, TimeUnit unit):在指定的超时时间内尝试获取令牌。 示例3:使用tryAcquire ```java import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.TimeUnit; public class TryAcquireDemo { public static void main(String[] args) { RateLimiter rateLimiter = RateLimiter.create(2.0); if (rateLimiter.tryAcquire()) { // 立即获取令牌成功 System.out.println("操作执行"); } else { System.out.println("被限流"); } // 尝试在500毫秒内获取令牌 if (rateLimiter.tryAcquire(500, TimeUnit.MILLISECONDS)) { System.out.println("在500毫秒内获取到令牌,操作执行"); } else { System.out.println("在500毫秒内未获取到令牌,被限流"); } } } ``` 注意事项: - RateLimiter是单机的限流工具,如果是分布式系统,则需要考虑分布式限流方案。 - RateLimiter不支持修改速率,如果需要动态修改速率,可以使用其setRate方法(但要注意线程安全)。 - RateLimiter会累积令牌,如果长时间没有请求,则桶中令牌会累积到上限,从而允许突发流量。 以上就是对Guava RateLimiter的简要介绍和示例。 §§相关问题§§: 1. 令牌桶算法和漏桶算法的区别是什么? 2. 如何动态调整RateLimiter的速率? 3. 在分布式系统中如何实现限流? 4. 除了Guava RateLimiter,还有哪些常用的限流工具或框架?</think>### Guava RateLimiter 详解 Guava RateLimiter 是 Google Guava 库提供的限流工具,基于令牌桶算法实现,用于控制单位时间内操作的执行速率。它能有效防止系统过载,支持突发流量和平滑限流两种模式。 #### 核心功能: 1. **平滑突发限流**:允许短时间内突发请求 2. **平滑预热限流**:系统启动时逐步增加处理速率 3. **阻塞/非阻塞获取**:支持阻塞等待或立即返回结果 #### 基础使用示例: ```java import com.google.common.util.concurrent.RateLimiter; public class BasicRateLimiterDemo { public static void main(String[] args) { // 创建每秒允许2次操作的限流器 RateLimiter rateLimiter = RateLimiter.create(2.0); for (int i = 1; i <= 5; i++) { // 阻塞获取令牌 double waitTime = rateLimiter.acquire(); System.out.printf("操作%d执行, 等待时间: %.2fs%n", i, waitTime); } } } ``` #### 预热限流示例(冷启动保护): ```java import com.google.common.util.concurrent.RateLimiter; import java.util.concurrent.TimeUnit; public class WarmupRateLimiterDemo { public static void main(String[] args) { // 预热型限流器:3秒内从0提升到5QPS RateLimiter rateLimiter = RateLimiter.create(5.0, 3, TimeUnit.SECONDS); for (int i = 1; i <= 10; i++) { System.out.printf("操作%d执行, 等待时间: %.2fs%n", i, rateLimiter.acquire()); } } } ``` #### 非阻塞尝试获取令牌: ```java import com.google.common.util.concurrent.RateLimiter; public class NonBlockingRateLimiterDemo { public static void main(String[] args) { RateLimiter rateLimiter = RateLimiter.create(1.0); // 1QPS for (int i = 1; i <= 5; i++) { if (rateLimiter.tryAcquire()) { System.out.println("执行操作: " + i); } else { System.out.println("操作被限流: " + i); } Thread.sleep(300); // 模拟操作间隔 } } } ``` #### 批量操作限流: ```java import com.google.common.util.concurrent.RateLimiter; public class BulkOperationDemo { public static void main(String[] args) { RateLimiter rateLimiter = RateLimiter.create(10.0); // 10QPS // 批量获取3个令牌 double waitTime = rateLimiter.acquire(3); System.out.printf("批量操作执行, 等待时间: %.2fs%n", waitTime); } } ``` ### 关键特性解析: 1. **令牌桶算法**: - 系统以固定速率向桶中添加令牌 - 操作执行时需获取令牌,无令牌则阻塞/拒绝 - 支持突发流量(桶中积累的令牌可一次性使用) 2. **预热机制**: - 解决冷启动问题 - 限流器启动时逐步提升到目标速率 - 公式:`稳定间隔 = 冷启动间隔 / 3` 3. **精度控制**: - 使用高精度Stopwatch计时(纳秒级) - 避免传统sleep方法的精度问题 ### 最佳实践: 1. 对于数据库访问等I/O操作,使用预热限流 2. 在REST API入口处添加限流保护 3. 结合tryAcquire实现优雅降级 4. 监控实际等待时间调整限流参数 ### 性能注意事项: 1. 单机限流器性能极高(>1,000,000 QPS) 2. 分布式系统需结合Redis等实现集群限流 3. 避免在超高速循环中频繁创建RateLimiter实例 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值