说到限流,大体上可以分为两种实现。
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也有两个实现:
区别在于一些算法的实现。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。