令牌桶算法
- 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时则拒绝新的请求;漏桶则是按照常量固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝;
- 令牌桶限制的是平均流入速率,允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,4个令牌;漏桶限制的是常量流出速率,即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;
- 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流出速率;
Guava RateLimiter
Guava的 RateLimiter提供了令牌桶算法实现:平滑突发限流(SmoothBursty)和平滑预热限流(SmoothWarmingUp)实现。
平滑突发限流
使用 RateLimiter的静态方法创建一个限流器,设置每秒放置的令牌数为5个。
返回的RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。
java
public void testSmoothBursty() {
RateLimiter r = RateLimiter.create(5);
while (true) {
System.out.println("get 1 tokens: " + r.acquire() + "s");
}
/**
* output: 基本上都是0.2s执行一次,符合一秒发放5个令牌的设定。
* get 1 tokens: 0.0s
* get 1 tokens: 0.182014s
* get 1 tokens: 0.188464s
* get 1 tokens: 0.198072s
* get 1 tokens: 0.196048s
* get 1 tokens: 0.197538s
* get 1 tokens: 0.196049s
*/
}
令牌累计
Guava RateLimiter的令牌积累数量是根据设定的速率和时间间隔来计算的。具体计算方式如下:
- 首先,RateLimiter会根据设定的速率(每秒生成的令牌数量)计算出令牌生成的时间间隔。
例如,如果设定的速率是2个令牌/秒,那么每个令牌生成的时间间隔是0.5秒(1秒/2个令牌)。 - 当RateLimiter开始工作时,它会记录当前时间,并将令牌桶中的令牌数量初始化为0。
- 当一个请求到达时,RateLimiter会计算当前时间与上一次记录时间之间的时间间隔,并根据设定的速率和时间间隔计算出应该生成的令牌数量。
- 如果计算出的令牌数量小于等于令牌桶中的剩余令牌数量,请求将被允许通过,并且令牌桶中的令牌数量减少。
- 如果计算出的令牌数量大于令牌桶中的剩余令牌数量,请求将被限制或延迟处理,直到令牌桶中有足够的令牌可用。
总结来说,Guava RateLimiter根据设定的速率和时间间隔来计算应该生成的令牌数量,并根据令牌桶中的剩余令牌数量来决定请求是否被允许通过。
java
public void testSmoothBursty2() {
// RateLimiter使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
RateLimiter r = RateLimiter.create(2);
while (true) {
try {
Thread.sleep(3000);
} catch (Exception e) {
}
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.499147s
end
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.0s
get 1 tokens: 0.499904s
*/
}
}
在上面的代码中,令牌桶中最多只能积累2个令牌是由于创建RateLimiter时指定的速率为2。RateLimiter.create(2)表示每秒生成2个令牌。令牌桶的大小取决于速率和时间间隔之间的关系。在这种情况下,速率为2个令牌/秒,意味着每秒生成2个令牌放入令牌桶中。
根据令牌桶算法,令牌桶的大小可以理解为令牌桶的容量,即最多可以容纳的令牌数量。在这里,令牌桶的容量为2,也就是最多可以积累2个令牌。
如果在某个时间点上,令牌桶中已经有2个令牌,而没有请求来消耗这些令牌,那么令牌桶中不会继续积累更多的令牌。新的令牌只有在之前的令牌被消耗后才会生成并放入令牌桶中。
因此,根据上述代码和速率设置,令牌桶中最多只能积累2个令牌。
平滑预热限流
RateLimiter的 SmoothWarmingUp是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。 比如下面代码中的例子,创建一个平均分发令牌速率为2,预热期为3分钟。由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来"热身"的场景。
java
public void testSmoothwarmingUp() {
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true)
{
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 1 tokens: 0.0s
* get 1 tokens: 1.329289s
* get 1 tokens: 0.994375s
* get 1 tokens: 0.662888s 上边三次获取的时间相加正好为3秒
* end
* get 1 tokens: 0.49764s 正常速率0.5秒一个令牌
* get 1 tokens: 0.497828s
* get 1 tokens: 0.49449s
* get 1 tokens: 0.497522s
*/
}
}
在平滑预热限流的情况下,令牌是不会积累的。
RateLimiter.create(2, 3, TimeUnit.SECONDS);
- 参数1:每秒产生2个令牌
- 参数2/3:在前3秒内,产生3个令牌,且从慢到快
- 如果RateLimiter.create(2, 4, TimeUnit.SECONDS); 代表在前4秒内,产生4个令牌,且从慢到快
核心函数说明
函数 | 说明 | 示例 |
---|---|---|
public static RateLimiter create(double permitsPerSecond) | 每秒产生permitsPerSecond个令牌 | RateLimiter.create(2) |
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit) | 每秒产生permitsPerSecond个令牌,在前warmupPeriod单位时间内,产生warmupPeriod个令牌,从慢到快 | RateLimiter.create(2, 3, TimeUnit.SECONDS) |
public double acquire(int permits) | 取得指定数量的令牌,并返回等待时间 | limiter.acquire(1) |
public boolean tryAcquire(int permits) | 尝试取得指定数量的令牌,返回成功或失败 | limiter.tryAcquire(1) |
public boolean tryAcquire(Duration timeout) | 尝试取得1个令牌,并等待指定的时间,返回成功或失败 | limiter.tryAcquire(Duration.ofSeconds(1)) |
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) | 尝试取得指定数量的令牌,并等待指定的时间,返回成功或失败 | limiter.tryAcquire(2, 10, TimeUnit.SECONDS) |