参考:
https://www.jianshu.com/p/5d4fe4b2a726
https://blog.csdn.net/charleslei/article/details/53152883
Guava 的 RateLimiter 采用令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
示例
如果我们要以 1000 的 tps 完成某个数据同步任务。
例1:传输速度比 RateLimiter 设定 tps 快,通过 RateLimiter 降低传输 tps。
public static void main(String[] args) throws Exception {
int[] messageCountList = new int[] {10000, 1000, 100};
// 1000 tps
RateLimiter rateLimiter = RateLimiter.create(1000);
for (int messageCount : messageCountList) {
rateLimiter.acquire(messageCount);
fastRun(messageCount);
}
}
private static void fastRun(int messageCount) {
SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss:SSS");
System.out.println("transfer start time : " + formatter.format(new Date()));
System.out.println("transfered message count : " + messageCount);
System.out.println("transfer end time : " + formatter.format(new Date()));
}
输出:
start time : 03-十一月-2019 00:08:41:497
transfer start time : 03-十一月-2019 00:08:41:515
transfered message count : 10000
transfer end time : 03-十一月-2019 00:08:41:515
transfer start time : 03-十一月-2019 00:08:51:520
transfered message count : 1000
transfer end time : 03-十一月-2019 00:08:51:520
transfer start time : 03-十一月-2019 00:08:52:519
transfered message count : 100
transfer end time : 03-十一月-2019 00:08:52:519
可见:
1. 第一次尽管需要 10000 个 permits,但仍然是立即就拿到了。
2. 第二次只需要 1000 个 permits,但需要补足第一次欠的 10000 个 permits,所以等了 10s。
3. 第三次补足第二次欠的 1000 个 permits,所以等了 1s。
4. 前后共耗时:11s。显然尽管消息传输很快,但总传输速度被降下来了。
5. 从上可知:获取 permits 时,是先获取,再补足的方式。
例2:传输速度比 RateLimiter 设定的 tps 慢。
private static void slowRun(int messageCount) throws Exception {
Thread.sleep(messageCount * 2);
SimpleDateFormat formatter = new SimpleDateFormat("dd-MMM-yyyy HH:mm:ss:SSS");
System.out.println("transfer start time : " + formatter.format(new Date()));
System.out.println("transfered message count : " + messageCount);
System.out.println("transfer end time : " + formatter.format(new Date()));
}
public static void main(String[] args) throws Exception {
int[] messageCountList = new int[] {10000, 1000, 100};
// 1000 tps
RateLimiter rateLimiter = RateLimiter.create(1000);
for (int messageCount : messageCountList) {
rateLimiter.acquire(messageCount);
slowRun(messageCount);
}
}
输出:
start time : 03-十一月-2019 00:06:43:688
transfer start time : 03-十一月-2019 00:06:43:704
transfered message count : 10000
transfer end time : 03-十一月-2019 00:07:03:707
transfer start time : 03-十一月-2019 00:07:03:707
transfered message count : 1000
transfer end time : 03-十一月-2019 00:07:05:709
transfer start time : 03-十一月-2019 00:07:05:709
transfered message count : 100
transfer end time : 03-十一月-2019 00:07:05:912
可见:
1. 第一次获取 10000 个 permits,仍然是立即就拿到了。
2. 第二次获取 1000 个 permits 时,由于第一次的 10000 条数据传输过慢,RateLimiter 已经累积了许多 permits,所以不需等待,直接获取了 1000 个 permits。
3. 总共耗时 22s,由于 RateLimiter 的 tps 比传输速度快,所以 RateLimter 并不会额外限流。
RateLimite 源代码
public double acquire() {
return acquire(1);
}
public double acquire(int permits) {
checkPermits(permits); //检查参数是否大于0
long microsToWait;
synchronized (mutex) { //应对并发情况需要同步
microsToWait = reserveNextTicket(permits, readSafeMicros()); //获得需要等待的时间,前面例子已经看出,RateLimiter 对于第一次 acquire 令牌,是立即准许,而在第二次 acquire 令牌时,再补偿性加等待时间以限流的。
}
ticker.sleepMicrosUninterruptibly(microsToWait); //等待,当未达到限制时,microsToWait为0
return 1.0 * microsToWait / TimeUnit.SECONDS.toMicros(1L);
}
private long reserveNextTicket(double requiredPermits, long nowMicros) {
resync(nowMicros); //补充之前 acquire 超用的令牌
long microsToNextFreeTicket = nextFreeTicketMicros - nowMicros;
double storedPermitsToSpend = Math.min(requiredPermits, this.storedPermits); //获取这次请求消耗的令牌数目
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
this.storedPermits -= storedPermitsToSpend; // 减去消耗的令牌
return microsToNextFreeTicket;
}
private void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
// 这里就是补偿超用的令牌
if (nowMicros > nextFreeTicketMicros) {
storedPermits = Math.min(maxPermits,
storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
nextFreeTicketMicros = nowMicros;
}
}
可见 RateLimiter 并非是很直观地开一个线程不停往令牌桶中放令牌,而是比较巧妙地采用先使用后补偿的形式。