本文大量内容系转载自以下文章,有删改,并参考其他文档资料加入了一些内容:
转载仅为方便学习查看,一切权利属于原作者,本人只是做了整理和排版,如果带来不便请联系我删除。
在开发高并发系统时有三把利器用来保护系统:缓存、降级和限流
漏桶算法能强行限制数据的传输速率为恒定速率,当流入的请求过大时就拒绝抛掉了。
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
并不能说明令牌桶一定比漏洞好,她们使用场景不一样:
总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。
Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法实现流量限制,使用十分方便,而且十分高效。
简单例子:
public void testAcquire() {
// 首先创建一个限流器,参数代表每秒生成的令牌数
RateLimiter limiter = RateLimiter.create(1);
for(int i = 1; i < 10; i = i + 2 ) {
// limiter.acquire以阻塞的方式获取令牌。
// 当然也可以通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌,
// 如果超timeout为0,则代表非阻塞,获取不到立即返回
double waitTime = limiter.acquire(i);
System.out.println("cutTime=" + System.currentTimeMillis() + " acq:" + i + " waitTime:" + waitTime);
}
}
输出结果:
cutTime=1535439657427 acq:1 waitTime:0.0
cutTime=1535439658431 acq:3 waitTime:0.997045
cutTime=1535439661429 acq:5 waitTime:2.993028
cutTime=1535439666426 acq:7 waitTime:4.995625
cutTime=1535439673426 acq:9 waitTime:6.999223
RateLimiter是预消费机制:
RateLimiter通过限制后面请求的等待时间,来支持一定程度的突发请求(预消费),在使用过程中需要注意这一点,具体实现原理后面再分析。
Guava有两种限流模式:
通过调用RateLimiter的create接口来创建实例,实际是调用的SmoothBuisty稳定模式创建的实例。
public static RateLimiter create(double permitsPerSecond) {
return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
SmoothBursty中的两个构造参数含义:
在解析SmoothBursty原理前,重点解释下SmoothBursty中几个属性的含义
/**
* The work (permits) of how many seconds can be saved up if this RateLimiter is unused?
* 在RateLimiter未使用时,令牌最多被存储几秒
* */
final double maxBurstSeconds;
/**
* The currently stored permits.
* 当前存储令牌数
*/
double storedPermits;
/**
* 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.
* 添加令牌时间间隔 = SECONDS.toMicros(1L) / permitsPerSecond;(每秒生产的令牌数)
* 比如5令牌,代表每秒内,间隔200ms 1个令牌
*/
double stableIntervalMicros;
/**
* The maximum number of stored permits.
* 最大存储令牌数 = maxBurstSeconds * stableIntervalMicros
* 即 最大存储令牌数 = 令牌最大空闲存在时间 * 令牌生产间隔时间
*/
double maxPermits;
/**
* The time when the next request (no matter its size) will be granted. After granting a request,
* this is pushed further in the future. Large requests push this further than small requests.
* 下一次请求可以获取令牌的起始时间
* 由于RateLimiter允许预消费,上次请求预消费令牌后
* 下次请求需要等待相应的时间到nextFreeTicketMicros时刻才可以获取令牌
*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future
接下来介绍几个关键函数
public final void setRate(double permitsPerSecond) {
checkArgument(
permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
synchronized (mutex()) {
// permitsPerSecond为每秒令牌数
// stopwatch.readMicros()是当前耗时
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}
@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {
// 生成令牌以及更新下一期令牌生成时间
resync(nowMicros);
// 计算出每次令牌生成间隔时间
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
/**
* Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.
* 基于当前耗时,更新当前存储的令牌以及下一次请求令牌的时间
*/
void resync(long nowMicros) {
// 只有当上一轮计算的预消费令牌计算出的需要等待时间大于当前消费时间时,才会不走以下逻辑。
// 此时会阻塞,时长为上一轮计算的预消费令牌计算出的需要等待时间减去当前轮消耗时间
if (nowMicros > nextFreeTicketMicros) {
// nextFreeTicketMicros初始为0,
// coolDownIntervalMicros是stableIntervalMicros,初始状态也为0
// 所以初始时,newPermits为无穷大Infinity
// 以后每次根据当前耗时减去上一轮耗时再除以令牌生成间隔,
// 生成这段时间内新的令牌数
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
// maxPermits和storedPermits初始都为0,所以这里storedPermits = 0
// 即初始时,当前存储令牌为0
// 以后每次根据时间来更新现有的令牌数
storedPermits = min(maxPermits, storedPermits + newPermits);
// 更新下次请求令牌时间为此次消耗时间
nextFreeTicketMicros = nowMicros;
}
}
根据令牌桶算法,桶中的令牌是持续生成存放的,有请求时需要先从桶中拿到令牌才能开始执行,谁来持续生成令牌存放呢?
一种解法是,开启一个定时任务,由定时任务持续生成令牌。这样的问题在于会极大的消耗系统资源,如,某接口需要分别对每个用户做访问频率限制,假设系统中存在6W用户,则至多需要开启6W个定时任务来维持每个桶中的令牌数,这样的开销是巨大的。
另一种解法则是延迟计算,如上resync函数。该函数会在每次获取令牌之前调用,其实现思路为,若当前时间晚于nextFreeTicketMicros,则计算该段时间内可以生成多少令牌,将生成的令牌加入令牌桶中并更新数据。这样一来,只需要在获取令牌时计算一次即可。
@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
// 初始为0
double oldMaxPermits = this.maxPermits;
// 更新最大令牌数为1乘以每秒令牌数
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
// Double.POSITIVE_INFINITY 代表无穷大
storedPermits = maxPermits;
} else {
// 更新当前令牌数,初始为0,
// 否则为 maxPermits / oldMaxPermits
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
在了解以上概念后,就非常容易理解RateLimiter暴露出来的方法。
@CanIgnoreReturnValue
public double acquire() {
return acquire(1);
}
不指定令牌数的申请其实就是调用以下方法,固定申请一个令牌。@CanIgnoreReturnValue
public double acquire(int permits) {
// 申请令牌,得到还需要阻塞时间
long microsToWait = reserve(permits);
// 阻塞
stopwatch.sleepMicrosUninterruptibly(microsToWait);
// 返回等待时间
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
checkPermits(permits);
// 使用了同步锁
synchronized (mutex()) {
// 调用reserveAndGetWaitLength获取等待时间
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
/**
* Reserves next ticket and returns the wait time that the caller must wait for.
*
* @return the required wait time, never negative
*/
final long reserveAndGetWaitLength(int permits, long nowMicros) {
// 得到上一轮取令牌时间
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
// 返回上一轮取令牌时间减去当前此轮已经消耗时间进行阻塞
// 只有当上一轮计算的预消费令牌计算出的需要等待时间大于当前消费时间时,才会需要阻塞,时长为上一轮计算的预消费令牌计算出的需要等待时间减去当前轮消耗时间
return max(momentAvailable - nowMicros, 0);
}
RateLimiter.reserveEarliestAvailable
首先通过resync
生成令牌以及同步nextFreeTicketMicros
时间戳,freshPermits
从令牌桶中获取令牌后还需要的令牌数量,通过storedPermitsToWaitTime
计算出获取freshPermits还需要等待的时间,在稳定模式中,这里就是(long) (freshPermits * stableIntervalMicros) ,然后更新nextFreeTicketMicros以及storedPermits,这次获取令牌需要的等待到的时间点, reserveAndGetWaitLength返回需要等待的时间间隔。
从reserveEarliestAvailable
可以看出RateLimiter的预消费原理,以及获取令牌的等待时间时间原理(可以解释示例结果),再获取令牌不足时,并没有等待到令牌全部生成,而是更新了下次获取令牌时的nextFreeTicketMicros,从而影响的是下次获取令牌的等待时间。
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
// 基于当前耗时,更新当前存储的令牌以及下一次请求令牌的时间
// 其实就是(当前消耗时间-上一轮耗时)/令牌间隔得到新的令牌数
// 拿这个数和每秒最大令牌数取较小的那个,作为新的现存令牌数
// 并以此轮耗时来更新nextFreeTicketMicros
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
// 消耗的令牌
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
// 还需要的令牌数
double freshPermits = requiredPermits - storedPermitsToSpend;
// 获得令牌尚需的时间,如果当前令牌足够就为0
// 否则就是令牌数*令牌生成间隔
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
// 更新下次获取令牌时间
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
// 更新当前令牌为当前令牌减去消耗令牌数
this.storedPermits -= storedPermitsToSpend;
// 返回上一轮取令牌时间
return returnValue;
}
stopwatch.sleepMicrosUninterruptibly
reserve
这里返回等待时间后,acquire
通过调用stopwatch.sleepMicrosUninterruptibly(microsToWait);
进行sleep操作,这里不同于Thread.sleep(), 这个函数的sleep是uninterruptibly不可中断的,内部实现:
public static void sleepUninterruptibly(long sleepFor, TimeUnit unit) {
//sleep 阻塞线程 内部通过Thread.sleep()
boolean interrupted = false;
try {
long remainingNanos = unit.toNanos(sleepFor);
long end = System.nanoTime() + remainingNanos;
while (true) {
try {
// TimeUnit.sleep() treats negative timeouts just like zero.
NANOSECONDS.sleep(remainingNanos);
return;
} catch (InterruptedException e) {
//如果被interrupt可以继续,更新sleep时间,循环继续sleep
interrupted = true;
remainingNanos = end - System.nanoTime();
}
}
} finally {
if (interrupted) {
// 如果被打断过,sleep过后再真正中断线程
Thread.currentThread().interrupt();
}
}
}
sleep之后,acquire
返回sleep的时间,阻塞结束,获取到令牌。
通过tryAcquire(int permits, long timeout, TimeUnit unit)来设置等待超时时间的方式获取令牌。
tryAcquire
尝试在timeout时间内获取令牌,如果可以则挂起等待相应时间并返回true,否则立即返回false。有三个版本:
public boolean tryAcquire(int permits) {
return tryAcquire(permits, 0, MICROSECONDS);
}
public boolean tryAcquire() {
return tryAcquire(1, 0, MICROSECONDS);
}
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;
}
canAcquire
用于判断timeout时间内是否可以获取令牌,通过判断当前时间+超时时间是否大于nextFreeTicketMicros
来决定是否能够拿到足够的令牌数,如果可以获取到,则过程同acquire,线程sleep等待。
如果通过canAcquire在此超时时间内不能回去到令牌,则可以快速返回,不需要等待timeout后才知道能否获取到令牌。
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}