Skip to content
On this page

guava

RateLimit

guava中集成的RateLimiter是一个用于控制处理速率的类,它基于令牌桶算法实现。令牌桶算法是一种限流算法,它的原理是有一个容量固定的桶,桶中存放着令牌,按照一定的速率往桶中添加令牌,当桶满时,多余的令牌会被丢弃。当有请求到来时,需要从桶中取出一个令牌,如果桶为空,则请求被拒绝或者等待。

guava提供了两种令牌桶算法的实现:SmoothBursty和SmoothWarmingUp。SmoothBursty是一种平滑突发限流器,它允许在没有请求的情况下累积一定数量的令牌,以应对突发的请求。SmoothWarmingUp是一种平滑预热限流器,它在启动时会逐渐提升发放令牌的速率,直到达到预设的速率,以避免冷启动时的突发流量。

你可以使用RateLimiter.create方法来创建一个RateLimiter对象,并指定每秒允许的令牌数。然后你可以使用acquire方法来获取一个令牌,如果没有可用的令牌,该方法会阻塞直到获取到令牌。你也可以使用tryAcquire方法来尝试获取一个令牌,如果没有可用的令牌,该方法会立即返回false。

令牌桶算法和漏桶算法的区别

令牌桶算法和漏桶算法都是用于控制流量的算法,但它们有以下几点不同:

令牌桶算法允许突发流量,即在桶内有足够的令牌时,可以一次性发送多个请求。漏桶算法则不允许突发流量,即每次只能发送一个请求,并且发送速率是固定的。 令牌桶算法可以调节令牌的生成速率和桶的容量,以适应不同的场景和需求。漏桶算法则只能调节桶的容量和出水速率,而且出水速率必须小于等于入水速率。 令牌桶算法在桶满时会丢弃新来的令牌,而不会影响已有的请求。漏桶算法在桶满时会丢弃新来的请求,而不会影响已有的水滴。 令牌桶算法可以实现平滑的限流效果,即在稳定速率下允许一定程度的突发流量,或者在预热期内逐渐增加许可的发放速率。漏桶算法则只能实现均匀的限流效果,即以恒定的速率发送请求。

漏桶算法示例

  • 漏桶算法可以用一个FIFO队列来实现,队列中存放着数据包。如果流量由固定大小的数据包组成(例如ATM网络中的单元),那么每个时钟周期就从队列中移除固定数量的数据包。如果流量由不同大小的数据包组成,那么每个时钟周期就从队列中移除固定数量的字节。下面是一个使用Java实现的漏桶算法的代码示例,它模拟了一个每秒发送10个字节的漏桶:
java
import java.util.LinkedList;
import java.util.Queue;

public class LeakyBucketExample {

  public static void main(String[] args) {
    // create a leaky bucket with capacity of 20 bytes and leak rate of 10 bytes per second
    LeakyBucket leakyBucket = new LeakyBucket(20, 10);

    // create a thread to simulate the leak process
    Thread leakThread = new Thread(() -> {
      while (true) {
        // leak the bucket every second
        try {
          Thread.sleep(1000);
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
        leakyBucket.leak();
      }
    });

    // start the leak thread
    leakThread.start();

    // create a thread to simulate the input process
    Thread inputThread = new Thread(() -> {
      // generate some random packets with different sizes
      int[] packets = {5, 15, 10, 8, 12, 6, 4};
      for (int packet : packets) {
        // input the packet into the bucket
        System.out.println("Inputting packet of size " + packet + " bytes");
        leakyBucket.input(packet);
        // wait for some random time
        try {
          Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
          e.printStackTrace();
        }
      }
    });

    // start the input thread
    inputThread.start();
  }
}

// a class that represents a leaky bucket
class LeakyBucket {

  private int capacity; // the maximum capacity of the bucket in bytes
  private int leakRate; // the leak rate of the bucket in bytes per second
  private Queue<Integer> queue; // a FIFO queue that holds the packets in the bucket
  private int currentSize; // the current size of the bucket in bytes

  // constructor that initializes the fields
  public LeakyBucket(int capacity, int leakRate) {
    this.capacity = capacity;
    this.leakRate = leakRate;
    this.queue = new LinkedList<>();
    this.currentSize = 0;
  }

  // a method that inputs a packet into the bucket
  public synchronized void input(int packet) {
    // check if the packet can fit into the bucket
    if (packet + currentSize <= capacity) {
      // add the packet to the queue
      queue.offer(packet);
      // update the current size of the bucket
      currentSize += packet;
      System.out.println("Packet of size " + packet + " bytes added to the bucket");
      System.out.println("Current bucket size: " + currentSize + " bytes");
    } else {
      // reject the packet
      System.out.println("Packet of size " + packet + " bytes rejected");
    }
  }

  // a method that leaks the bucket at a constant rate
  public synchronized void leak() {
    // check if the queue is empty
    if (queue.isEmpty()) {
      System.out.println("Bucket is empty, nothing to leak");
    } else {
      // get the size of the first packet in the queue
      int packet = queue.peek();
      // check if the packet can be leaked in one cycle
      if (packet <= leakRate) {
        // remove the packet from the queue
        queue.poll();
        System.out.println("Packet of size " + packet + " bytes leaked from bucket");
      } else {
        // reduce the packet size by the leak rate
        packet -= leakRate;
        System.out.println("Packet of size " + leakRate + " bytes leaked from bucket");
      }
      // update the current size of the bucket
      currentSize -= leakRate;
      System.out.println("Current bucket size: " + currentSize + " bytes");
    }
  }
}

  • guava 实现漏桶算法 guava的RateLimiter类实际上是基于令牌桶算法的,但是它也可以实现类似漏桶算法的效果,即以恒定的速率发送请求。这可以通过设置RateLimiter的预热期为0来实现,这样就相当于没有预热期,许可的发放速率就是稳定的。下面是一个使用guava的RateLimiter来模拟漏桶算法的代码示例,它创建了一个每秒允许10个字节的漏桶,并使用它来限制一些任务的执行:
java
import com.google.common.util.concurrent.RateLimiter;

public class LeakyBucketGuavaExample {

  public static void main(String[] args) {
    // create a rate limiter with 10 bytes per second and no warmup period
    RateLimiter rateLimiter = RateLimiter.create(10.0, 0, TimeUnit.SECONDS);

    // submit 20 tasks with different sizes
    for (int i = 0; i < 20; i++) {
      // generate a random task size between 1 and 20 bytes
      int taskSize = (int) (Math.random() * 20) + 1;
      // acquire the task size from the rate limiter
      rateLimiter.acquire(taskSize);
      // do some work
      System.out.println("Task of size " + taskSize + " bytes done at " + System.currentTimeMillis());
    }
  }
}

适用场景

令牌桶算法和漏桶算法都可以用于限制API的请求速率,但它们有以下几点不同:

令牌桶算法适用于那些需要在稳定速率下允许一定程度的突发流量的场景,例如视频流或音频流。令牌桶算法可以保证请求的平均速率不超过限制,同时也可以在有足够的令牌时发送多个请求。 漏桶算法适用于那些需要以恒定的速率发送请求的场景,例如网络拥塞控制或数据包调度。漏桶算法可以保证请求的峰值速率不超过限制,同时也可以平滑地处理突发流量。 令牌桶算法和漏桶算法都可以用于流量整形,即调节流量的形状和大小。但是,令牌桶算法更灵活,因为它可以调节令牌的生成速率和桶的容量,以适应不同的需求。漏桶算法则更简单,因为它只需要调节桶的容量和出水速率。

RateLimiter的工作原理

限流是指防止一个操作的频率超过一定的约束。在大规模的系统中,限流通常用于保护底层的服务和资源,以及控制成本和质量。RateLimiter是一个类,它允许我们调节一些处理发生的速率。如果我们创建一个有N个许可的RateLimiter,那么意味着每秒最多可以发出N个许可。 RateLimiter的工作原理是基于令牌桶算法,它是一种流量整形算法。令牌桶算法的核心思想是:有一个容量为N的桶,每隔一定时间往桶里放入一个令牌,直到桶满为止。每当有一个请求到来时,就从桶里取出一个令牌,如果桶为空,则拒绝或等待请求。这样就可以保证请求的平均速率不超过每秒N个。

RateLimiter还提供了不同的模式来实现不同的限流策略,如SmoothBursty和SmoothWarmingUp。SmoothBursty模式允许在稳定速率下发生突发流量,但会增加等待时间。SmoothWarmingUp模式则在预热期内平滑地增加许可的发放速率,直到达到稳定的速率。

使用RateLimiter的代码示例

java
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterExample {

public static void main(String[] args) {
    // create a rate limiter with 2 permits per second
    RateLimiter rateLimiter = RateLimiter.create(2.0);

    // a loop that simulates some tasks
    for (int i = 0; i < 10; i++) {
    // acquire a permit before each task
    rateLimiter.acquire();
    // do some work
    System.out.println("Task " + i + " done");
    }
}
}
  • RateLimiter可以配置一个预热期,在这个期间,每秒发出的许可数逐渐增加,直到达到稳定的速率。例如,假设我们有一个任务列表要执行,但我们不想每秒提交超过2个任务:
java
import com.google.common.util.concurrent.RateLimiter;

public class RateLimiterComplexExample {

    public static void main(String[] args) {
        // create a rate limiter with 2 permits per second and a warmup period of 10 seconds
        RateLimiter rateLimiter = RateLimiter.create(2.0, 10, TimeUnit.SECONDS);

        // submit 20 tasks with different delays
        for (int i = 0; i < 20; i++) {
        // simulate some delay between tasks
        try {
            Thread.sleep((long) (Math.random() * 1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // acquire a permit before each task
        rateLimiter.acquire();
        // do some work
        System.out.println("Task " + i + " done at " + System.currentTimeMillis());
        }
    }
}

预热期

使用guava的RateLimiter来实现令牌桶算法,你可以通过传入一个double类型的参数来设置每秒可以生成的令牌数,也就是令牌生成速率。 但是,guava的RateLimiter并没有提供一个直接的方法来设置桶内可以存放的最大令牌数,也就是桶的容量。 不过,你可以通过调整RateLimiter的预热期来间接地设置桶的容量。预热期是指从冷启动到达稳定速率所需的时间。在预热期内,RateLimiter会以一个较低的速率生成令牌,直到达到稳定速率。这样就可以避免在启动时产生突发流量。 你可以使用RateLimiter类的静态方法create()来创建一个带有预热期的RateLimiter对象。这个方法接受三个参数:一个double类型的参数表示稳定速率,一个long类型的参数表示预热期的长度,一个TimeUnit类型的参数表示预热期的时间单位。

假设期望每秒可以生成10个令牌,并且桶内最多可以存放50个令牌: 为了设置桶的容量为50个令牌,你需要计算出合适的预热期,使得在预热期内生成的令牌数刚好等于50个。这个计算可以用以下公式来表示:

这个公式是基于guava的RateLimiter使用SmoothWarmingUp模式来实现预热效果的假设。SmoothWarmingUp模式使用了一个平滑曲线来计算许可的发放时间和存储的许可数。你可以在中看到SmoothWarmingUp模式的源码和详细解释。 将桶容量设为50个令牌,稳定速率设为10个令牌每秒,代入公式,得到: 因此,你可以将预热期设置为大约2.16秒,这样就可以使桶容量接近50个令牌。下面是一个使用guava的RateLimiter来配置每秒可以生成10个令牌,并且桶内最多可以存放50个令牌的代码示例:
java
import com.google.common.util.concurrent.RateLimiter;

public class TokenBucketGuavaCapacityExample {

  public static void main(String[] args) {
    // create a rate limiter with 10 permits per second and a warmup period of 2.16 seconds
    RateLimiter rateLimiter = RateLimiter.create(10.0, 2160, TimeUnit.MILLISECONDS);

    // a loop that simulates some tasks
    for (int i = 0; i < 10; i++) {
      // acquire a permit before each task
      rateLimiter.acquire();
      // do some work
      System.out.println("Task " + i + " done at " + System.currentTimeMillis());
    }
  }
}

如果我不想阻塞等待令牌,应该怎么做?

可以使用RateLimiter类的tryAcquire()方法来尝试获取一个或多个许可,而不会等待。这个方法接受一个int类型的参数,表示要获取的许可数,和一个long类型的参数,表示最多等待的时间。它返回一个boolean类型的值,表示是否成功获取了许可。

例如,下面的代码使用tryAcquire()方法来尝试获取一个许可,最多等待100毫秒:

java
// create a rate limiter with 2 permits per second
RateLimiter rateLimiter = RateLimiter.create(2.0);

// try to acquire a permit without waiting
boolean result = rateLimiter.tryAcquire(1, 100, TimeUnit.MILLISECONDS);

// check the result
if (result) {
  // do some work
  System.out.println("Task done");
} else {
  // handle the failure
  System.out.println("Task failed");
}

RateLimiter有什么优势和缺点?

guava的RateLimiter是一个用于实现令牌桶算法的类,它可以用来调节一些处理发生的速率。guava的RateLimiter有以下一些优势和缺点:

  • 优势:
    • guava的RateLimiter是一个轻量级的类,它不涉及任何线程,因此可以创建数千个RateLimiter对象而不影响性能。
    • guava的RateLimiter提供了不同的模式来实现不同的限流策略,如SmoothBursty和SmoothWarmingUp。SmoothBursty模式允许在稳定速率下发生突发流量,但会增加等待时间。SmoothWarmingUp模式则在预热期内平滑地增加许可的发放速率,直到达到稳定的速率。
    • guava的RateLimiter提供了简单易用的API,如create(),acquire()和tryAcquire()方法,可以方便地创建和使用RateLimiter对象。
  • 缺点:
    • guava的RateLimiter是基于令牌桶算法的,因此它不能保证请求的峰值速率不超过限制,只能保证请求的平均速率不超过限制。如果需要保证请求的峰值速率不超过限制,可以使用漏桶算法或其他限流算法。
    • guava的RateLimiter是基于阻塞等待令牌的方式实现的,因此如果有大量的异步调用对受限制的服务发起请求,可能会导致很多线程被阻塞,甚至耗尽可用线程。如果需要避免阻塞等待令牌,可以使用非阻塞方式获取令牌,或者使用其他限流算法。
    • guava的RateLimiter是基于单机环境实现的,因此它不能在分布式环境中保证全局的限流效果。如果需要在分布式环境中实现限流,可以使用分布式缓存或其他分布式限流算法。

Released under the MIT License.