Guava是Google的一个库,提供了很多有用的功能,其中的RateLimiter可以很方便的实现限流功能,使用的是定时向令牌桶里放令牌的方式实现的。
maven导入:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.0.1-jre</version>
</dependency>
RateLimiter是一个抽象类,其下面有两种实现,SmoothBursty和SmoothWarmingUp:
- SmoothBursty
可以应对突发流量的限流器;空闲时,最大可以保留1s(该保留时间写死在源码里)的令牌数,突发流量时可以迅速获取这1s保留的令牌而无需等待;
- SmoothWarmingUp
带有预热效果的限流器;空闲时,最大也会保留1s的令牌数,但从空闲时开始获取令牌会存在预热效果,即使保留了1s的令牌数,也会等待一段比较长的时间获取令牌(具体等待时间可根据函数求得),会在设定的预热时间期间,等待时间逐渐加速到正常限流速率;
创建RateLimiter有3个方法可以调用(RateLimiter类中):
- public static RateLimiter create(double permitsPerSecond)
- public static RateLimiter create(double permitsPerSecond, Duration warmupPeriod)
- public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit)
第一个方法生成的是SmoothBursty,第二三个方法生成的是SmoothWarmingUp(其中第二个方法转换了下参数直接调用了第三个方法),permitsPerSecond指定每秒生成的令牌数(相当于设定QPS);
获取令牌的方法总体分为2类:阻塞获取和非阻塞获取;
- 阻塞获取的方法(返回等待时间秒,返回0.0表示无等待):
public double acquire() 获得一个令牌,实际调用acquire(1);
public double acquire(int permits) 获得指定个数令牌;
- 非阻塞获取的方法(返回true表示获得令牌成功,否则返回false,还可以设置等待时间,在等待时间内获得令牌也返回true):
public boolean tryAcquire() 尝试获得一个令牌;
public boolean tryAcquire(Duration timeout) 尝试获得一个令牌并设置等待时间;
public boolean tryAcquire(int permits) 尝试获得多个令牌;
public boolean tryAcquire(int permits, Duration timeout) 尝试获得定数令牌并设置等待时间;
public boolean tryAcquire(long timeout, TimeUnit unit) 尝试获得一个令牌并设置等待时间;
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) 尝试获得指定数量令牌并设置等待时间;(上面5个try方法最终都会调整参数调用该方法)
RateLimiter还有一个可以重新设置令牌生成速率的方法:public final void setRate(double permitsPerSecond)
测试Demo:
/**
* 2021年12月20日下午4:41:26
*/
package testGuava;
import java.text.SimpleDateFormat;
import java.time.Duration;
import java.util.Date;
import java.util.concurrent.TimeUnit;
import com.google.common.util.concurrent.RateLimiter;
/**
* @author XWF
*
*/
public class TestGuavaRateLimiter {
/**
* @param args
* @throws InterruptedException
*/
public static void main(String[] args) throws InterruptedException {
SimpleDateFormat format = new SimpleDateFormat("hh:mm:ss.SSS");
System.out.println("____test SmoothBursty____");
//每秒放10个令牌(QPS=10,0.1s加一个令牌)
RateLimiter limit = RateLimiter.create(10);
Thread.sleep(2000);//等待令牌桶里存放10个令牌(默认最多缓存1s的令牌数)
for (int i = 0; i < 20; i++) {
double waitSeconds = limit.acquire();//等同于acquire(1);阻塞直到获得令牌,返回等待时间(秒),返回0s说明没有等待
System.out.println(i + "_time:" + format.format(new Date()) + "_等待(秒):" + waitSeconds);
}
System.out.println("_________________________");
for (int i = 0; i < 5; i++) {
System.out.println(format.format(new Date()) + "____" + limit.tryAcquire());//等同于tryAcquire(1);不阻塞,获取成功返回true;
//其他tryAcquire实际最终都是调用public boolean tryAcquire(int permits, long timeout, TimeUnit unit);带超时等待时间
Thread.sleep(50);
}
System.out.println("____test SmoothWarmingUp____");
// Duration duration = Duration.ofSeconds(2);
// limit = RateLimiter.create(10, duration);//实际调用create(permitsPerSecond, toNanosSaturated(warmupPeriod), TimeUnit.NANOSECONDS);
//带600ms的预热时间
//(即使桶里有10个令牌也不会立马获得10个,需要600ms逐渐加速到可以每0.1s获得一个令牌)
RateLimiter limit2 = RateLimiter.create(10, 600, TimeUnit.MILLISECONDS);
Thread.sleep(2000);//等待令牌桶里存放10个令牌
for (int i = 0; i < 20; i++) {
if(i == 10) {
Thread.sleep(600);//再次缓存6个令牌
}
double waitSeconds = limit2.acquire(2);//每次阻塞取2个令牌
System.out.println(i + "_time:" + format.format(new Date()) + "_等待(秒):" + waitSeconds);
}
System.out.println("finished.");
}
}
运行结果:
第一个测试可以看到缓存了1s的令牌数,直接无需等待就可以获得10个令牌,后面没有可用令牌时就会等待每0.1s生成一个令牌;
第二个测试可以看到也缓存了1s的令牌数,但有600ms的预热时间,在获取完第一个令牌后,第二个等待了0.46619s,第三个等待了0.233571s,第四个才到了0.199002s(设置的一次获取2令牌,0.1s增加一个,0.2s才能有2个令牌),后面一直是正常速率,中间又空闲了600ms缓存了一部分令牌,第十个也立即获得,但第十一个等待了0.238144,后面才正常速率,这就是预热效果;