说到限流,大体上可以分为两种实现。

1.漏桶或者令牌桶;思路之前介绍过

2.计数器;

关于计数器,这里简单介绍下。

大体是,维护一个整型变量count,每一次调用都以cas的方式将count减一,调用完将count加一。整体上是这样,当然里面有很多诸如同步,多线程等细节问题需要考虑。

所以它的思路是限定同一时刻访问某一资源的数目。

 

这次介绍的RateLimiter是guava包下的一个很好用的限流工具,模型是基于令牌桶的思路实现的。

使用

public static void main(String args[]) {
RateLimiter rateLimiter = RateLimiter.create(1);
for (int i = 0 ; i < 5 ; i++) {
rateLimiter.acquire();
new Thread(() -> {
System.out.println(LocalDateTime.now());
}).start();
}
}

输出:

2019-07-06T23:06:32.765
2019-07-06T23:06:33.647
2019-07-06T23:06:34.648
2019-07-06T23:06:35.647
2019-07-06T23:06:36.649

直接调用create方法构建一个限流器,入参就是qps。调用时直接acquire即可,当然可以acquire多个。

 

原理

单看令牌桶的逻辑含义,似乎需要实现一个定时器的功能,每隔多少秒往桶里放一个令牌。如果直接按照这个思路来设计,那么每一个限流器都要一个定时器,资源代价略高。你可能会说不就一个定时器吗?大型项目中开一个线程可能都需要定夺很久,对资源的消耗需要很苛刻的要求,否则会影响性能。更致命的这种思路的定时器是O(n)量级的,一个未知数,鬼知道会用多少个限流器,所以这种设计不是良好的设计。

guava使用了另一种设计思路。大致如下:每一次获取,都记录下一次可以获取的时刻next;那么下一次获取时,就会判断是否达到了next时刻,如果没有就sleep,直到到达next时刻。当然里面还有很多设计的细节,比方说令牌数目怎么存储。

 

源码

* <p>Rate limiters are often used to restrict the rate at which some physical or logical resource
* is accessed. This is in contrast to {@link java.util.concurrent.Semaphore} which restricts the
* number of concurrent accesses instead of the rate (note though that concurrency and rate are
* closely related, e.g. see <a href="http://en.wikipedia.org/wiki/Little%27s_law">Little's
* Law</a>).

注释解释了限流器和信号量的区别:

限流器限制的是qps或者说是速率,而信号量限制的是数目。

下面直接看下acquire方法:

@CanIgnoreReturnValue
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

非常简洁明了,第一步计算出需要等待的时间,也就是microsToWait。第二步直接sleep这个时间。

final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}

调用reserveAndGetWaitLength方法计算。同时传入当前的时刻。

final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}

这里调用的reserveEarliestAvailable方法才是真正计算等待时间的函数:

abstract long reserveEarliestAvailable(int permits, long nowMicros);

这是一个抽象方法,RateLimiter其实也是一个抽象类,该方法的实现是在SmoothRateLimiter类中:

@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);

this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
/** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time. */
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
storedPermits = min(maxPermits, storedPermits + newPermits);
nextFreeTicketMicros = nowMicros;
}
}

这个方法在干嘛?

是在放令牌。

nextFreeTickMicros是一个成员变量,指的就是下一个可以获取令牌的时刻。这里的逻辑是如果当前时刻已经超过nextFreeTickMicros,那么就可以累积令牌了。因为这意味着距离上一次获取已经过去了一段时间。

那么可以累积多少个令牌呢?

newpermits就是计算新令牌数目的。coolDownIntervalMicros就是每一秒可以生成的令牌数目,其实就等于create时入参的倒数。

/**
* Returns the number of microseconds during cool down that we have to wait to get a new permit.
*/
abstract double coolDownIntervalMicros();

what?这又是一个抽象方法。

其实smoothRateLimiter也有两个实现:

【Java】guava(四) RateLimiter_guava

区别在于一些算法的实现。SmoothBursty比较基础,SmoothWarmingUp有一个预热功能,比较复杂些。

这里只看下SmoothBursty:

@Override
double coolDownIntervalMicros() {
return stableIntervalMicros;
}

/**
* The interval between two unit requests, at our stable rate. E.g., a stable rate of 5 permits
* per second has a stable interval of 200ms.
*/
double stableIntervalMicros;

这个值就是qps的倒数。

比如create(5),那么限流就是qps=5,那么每秒可以生成0.2个令牌。

再回到之前的rescyn方法,至此已经算出了此次调用之前,限流器生成了多少个新的令牌。

storedPermits也是一个成员变量,记录的是限流器已有的令牌。maxpermits是上限。

/** The currently stored permits. */
double storedPermits;

/** The maximum number of stored permits. */
double maxPermits;

这里就更新了当前storedPermits数目。

再回到reserveEarliestAvailable方法:

storedPermitsToSpend变量计算出了需要从当前令牌数目中扣除的令牌数,如果不够则需要另外补充freshPermits个令牌。

如果确实需要补充,那么下一次可以获得令牌的时刻就需要延后waitMicros时间。

这个时间由两部分组成,第一部分是通过一个方法计算出来的,这个方法在SmoothBursty里实现就是返回0:

@Override
long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
return 0L;
}

所以waitMicros直接等于freshPermits * stableIntervalMicros

所以整体上,当前调用需要等待的时间数目就是原本的next时刻加上waitMicros

至此,等待时间也计算出来了。最后更新下nextFreeTicketMicros时刻。

 

再回到之前的acquire方法:

@CanIgnoreReturnValue
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

接下来就直接进入sleep方法了。

 

那么设置等待超时又如何实现呢?

也就是这个api:

public boolean tryAcquire(long timeout, TimeUnit unit)

这个按理说也是一个定时器,但是基于之前的讨论,实现这个其实很容易。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {
long timeoutMicros = max(unit.toMicros(timeout), 0);
checkPermits(permits);
long microsToWait;
synchronized (mutex()) {
long nowMicros = stopwatch.readMicros();
if (!canAcquire(nowMicros, timeoutMicros)) {
return false;
} else {
microsToWait = reserveAndGetWaitLength(permits, nowMicros);
}
}
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return true;
}

这里多了一次canAquire的逻辑:

private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}

@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}

这里就直接判断下下一次可以访问时间-当前时间是否大于超时时间,如果大于,那么就不能访问。很简单。

 

限流的实现大致就是这样。

整体感觉设计非常简洁,简洁就意味着坚固,坚固是工程中最最重要的指标。很多组件非常坚固耐用,为什么?很大程度上就是设计记录的简单。比方说线程池。

 

总结:

每一次获取,先计算新生成的令牌数目;

再计算当前获取需要等待的时间;

sleep。