RateLimiter: 服务限流的简单实现

RateLimiter

在限流算法中,典型的有令牌桶和漏桶,RateLimiter类提供了令牌桶的实现以及方便的使用接口。RateLimiter由Google Guava提供,因此在使用之前首先需要引入相关依赖:

1
2
3
4
5
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>32.0.1-jre</version>
</dependency>

简单使用

acquire

一个简单的入门例子如下:

1
2
3
4
5
6
final RateLimiter rateLimiter = RateLimiter.create(2.0); // the num of permit per second
int[] arr = {1, 2, 3, 4, 5, 6, 7, 8, 9, 0};
for (int i : arr) {
rateLimiter.acquire(2); // permit needed
System.out.println(i);
}

首先通过工厂方法create创建出一个RateLimiter类。在创建的时候需要给定一个double类型的数字,该值表示每秒钟产生的令牌数量。而进行限流的核心操作则是acquire(int),该方法表示需要获取令牌才能继续执行,当令牌数不够的时候,会阻塞在该方法上。该方法接受一个int类型的值,表示此次操作需要的令牌数量,默认为1。acquire()返回一个double类型的值,该值表示此次方法阻塞的时间,单位为秒。

因此上面的示例代码实际的运行效果就是每一秒钟输出数组中的一个值。

值得注意的一点是,RateLimiter有一个特性就是预先支付令牌,并且所需要等待的时间在下次获取令牌的时候再实际执行,也就是每次acquire的时候实际上是在偿还上次的令牌,阻塞对应的时间。这种特性使得RateLimter能够应对突发流量。

观察如下测试代码:

1
2
3
4
RateLimiter rateLimiter = RateLimiter.create(2.0);
System.out.println("get 4 permits: " + rateLimiter.acquire(4) + "s");
System.out.println("get 1 permits: " + rateLimiter.acquire(1) + "s");
System.out.println("get 2 permits: " + rateLimiter.acquire(2) + "s");

输出如下:

1
2
3
get 4 permits: 0.0s
get 1 permits: 1.997184s
get 2 permits: 0.493471s

直接观察上面的结果,会发现似乎获取令牌的数目和时间对不上,但是实际上这就是RateLimiter预先支付令牌的特性。

首先创建RateLimiter对象,该对象每秒钟均匀产生2个令牌,即0.5秒产生一个令牌。执行到第一个acquire方法,由于预先支付令牌,因此首次acquire无需等待直接返回。之后执行到第二个acquire方法,它需要偿还第一个acquire方法所需的4个令牌,因此阻塞接近2秒。接下来到第三个acquire方法,它需要偿还第二个acquire方法所需的1个令牌,因此阻塞接近0.5s。

tryAcquire

另一个非常有用的方法是tryAcquire(),该方法包括多种重载实现,不过核心参数只有三个,分别是需要获取的令牌数、超时时间以及时间单位。tryAcquire()返回一个布尔值,该方法在超时时间内执行acquire,如果在时间内获取成功,则返回true,反之则返回false。

下面是测试代码:

1
2
3
4
5
RateLimiter rateLimiter = RateLimiter.create(2.0);
System.out.println(rateLimiter.acquire(4));
System.out.println(rateLimiter.tryAcquire(8, 1, TimeUnit.SECONDS));
System.out.println(rateLimiter.acquire(2));
System.out.println(rateLimiter.tryAcquire(6, 1, TimeUnit.SECONDS));

这里同样需要注意RateLimiter的预先支付令牌的特性。tryAcquire相当于给acquire提供了一个超时时间,因此对于每个tryAcquire方法,我们实际上需要观察上一个acquire所需的令牌数量。测试代码输出如下:

1
2
3
4
0.0
false
1.997894
true

第一次acquire直接返回,消耗0.0秒。接下来执行tryAcquie,它需要偿还第一次acquire申请的4个令牌。由于无法在1秒内获得,因此返回false。接下来执行acquire。由于上面tryAcquire偿还失败,此次acuqire来继续偿还第一次acquire申请的4个令牌,因此消耗大约2秒。最后一次tryAcquire,需要偿还上次acquire申请的两个令牌,能够在一秒之内获得,因此返回ture。

warmup

在利用工厂方法完成RateLimiter对象创建的时候,也能够提供额外的参数,使得RateLimiter具备预热功能。RateLimiter的预热指的是带有预热期的平滑限流。它在启动之后会有一段时间的预热期,令牌生成的频率在这段时间内逐步平滑过滤到设置的目标频率。如果没有设置预热的话,则是启动直接达到目标频率。

1
2
3
4
RateLimiter rateLimiter = RateLimiter.create(2, 3, TimeUnit.SECONDS);
for (int i = 0; i < 10; i++) {
System.out.println(rateLimiter.acquire());
}

代码运行结果如下:

1
2
3
4
5
6
7
8
9
10
0.0
1.330551
0.991947
0.661334
0.496869
0.498878
0.494644
0.497765
0.496662
0.496844

在上面的代码中,我们设置了预热期限为3秒,因此在前面几次的acquire中,阻塞的时间逐渐变短,到大约三秒之后,才变为了正常的阻塞时间,即生成令牌的频率变为目标频率。

参考文章

  1. Quick Guide to the Guava - RateLimiter
  2. RateLimiter 解读

RateLimiter: 服务限流的简单实现
http://example.com/2023/07/08/RateLimiter-服务限流的简单实现/
作者
EverNorif
发布于
2023年7月8日
许可协议