在设计高并发系统时候,限流是必不可少的一环。刚好最近项目中使用了限流功能,也上网看了些限流的方案,代码层有简单限流,滑动窗口限流,漏桶限流、令牌桶限流,当然限流器也分为单机版本和分布式版本。项目中使用了谷歌Guava成熟的单机版令牌桶限流方案。出于对限流设计的好奇,以及与简单限流和滑动窗口两种方案相比的优势点,因此分析了Guava限流源码实现方式。
各限流算法思想可参见:https://www.cnblogs.com/linjiqin/p/9707713.html
令牌桶
--引用别人的^.^
“令牌桶算法则是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。桶中存放的令牌数有最大上限,超出之后就被丢弃或者拒绝。当流量或者网络请求到达时,每个请求都要获取一个令牌,如果能够获取到,则直接处理,并且令牌桶删除一个令牌。如果获取不同,该请求就要被限流,要么直接丢弃,要么在缓冲区等待。”
代码示例
public class Limiter {
private RateLimiter rateLimiter = RateLimiter.create(50);//初始化的时候设置速率为50/秒
@Test
public void test1() {
//实际使用设置速率为100/秒
rateLimiter.setRate(100);
//未获取到令牌一直阻塞,否则立即返回
rateLimiter.acquire();
}
@Test
public void test2() {
rateLimiter.setRate(100);
//获取或未获取到令牌都立即返回
boolean acquireFlag1 = rateLimiter.tryAcquire();
System.out.println(acquireFlag1);
//未获取令牌阻塞指定时间,否则立即返回
boolean acquireFlag2 = rateLimiter.tryAcquire(10, TimeUnit.MILLISECONDS);
System.out.println(acquireFlag2);
}
}
继承关系
RateLimiter有两个实现类:SmoothBursty和SmoothWarmingUp,由于项目里使用的是前者,因此分析SmoothBursty
重要属性
permitsPerSecond是方法参数,可以计算出stableIntervalMicros和maxPermits
1.maxBurstSeconds 令牌桶存储多少秒生成的令牌,是SmoothBursty独有属性,默认为1s
2.stableIntervalMicros 生成令牌的间隔时间(微秒),1000*1000/permitsPerSecond
3.maxPermits 令牌桶存储的最大令牌数,计算公式maxBurstSeconds*stableIntervalMicros
4.storedPermits 令牌桶剩余的令牌数
5.nextFreeTicketMicros 下一个请求需要等待的时间
创建限流器
public static RateLimiter create(double permitsPerSecond) {
return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond);
}
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
final void doSetRate(double permitsPerSecond, long nowMicros) {
resync(nowMicros);
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
doSetRate(permitsPerSecond, stableIntervalMicros);
}
基于当前时间补充令牌桶剩余的令牌数storedPermits并更新nextFreeTicketMicros如当前时间在nextFreeTicketMicros之后,则满足更新的条件进行更新;否则不更新。
void resync(long nowMicros) {
// if nextFreeTicket is in the past, resync to now
if (nowMicros > nextFreeTicketMicros) {
storedPermits = min(maxPermits,
storedPermits
+ (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());
nextFreeTicketMicros = nowMicros;
}
}
基于设置的速率permitsPerSecond,更新生成令牌间隔时间stableIntervalMicros
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
基于设置的速率permitsPerSecond,更新令牌桶最大令牌数maxPermits,并按比例设置storedPermits
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
maxPermits = maxBurstSeconds * permitsPerSecond;
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
// if we don't special-case this, we would get storedPermits == NaN, below
storedPermits = maxPermits;
} else {
storedPermits = (oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}
}
至此,RateLimiter实例创建完成,实际上是根据当前的时间和速率初始化了几个重要属性
获取令牌
阻塞型
1.代码
public double acquire(int permits) {
//获取需要等待的时间(微秒)
long microsToWait = reserve(permits);
//如果时间大于0,则阻塞指定时间,底层使用了Tim
stopwatch.sleepMicrosUninterruptibly(microsToWait);
//返回等待了多少秒
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
final long reserveAndGetWaitLength(int permits, long nowMicros) {
//关键代码,获取需要等待到的绝对时间
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
2.获取需要等待到的绝对时间(非常重要)
@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);
try {
//加上“减去剩余令牌数之外的令牌数所需等待的绝对时间”
this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
} catch (ArithmeticException e) {
this.nextFreeTicketMicros = Long.MAX_VALUE;
}
//减去
this.storedPermits -= storedPermitsToSpend;
//最后返回的是当前时间或者当前之后的时间,依赖于resync(nowMicros)这个方法是否执行
//执行了就是当前时间,未执行就是当前之后的时间
return returnValue;
}
这里建立一个基本认识:
调用了resync方法之后,nextFreeTicketMicros的值大于等于当前的时间
这里有两种情况
-
令牌桶中剩余令牌数足够当前所请求的令牌数,则freshPermits=0,waitMicros=0,storedPermits-=freshPermits;因为freshPermits=0,因此nextFreeTicketMicros不变
-
令牌桶中剩余令牌数不够当前所请求的令牌数,则freshPermits>0,waitMicros>0,storedPermits-=storedPermits,即为0;因为freshPermits>0,因此nextFreeTicketMicros需要再往后推迟。
仔细分析会发现第二种情况:当前请求获取令牌过程设置了nextFreeTicketMicros为未来的一个时间(预支了一些令牌),下次请求需要买单,即等待到上次设置的nextFreeTicketMicros时间点后才能获取到令牌)。以此循环,因为下次就变成了当前,下下次变成了下次。第二种情况细分如下:
returnValue=nextFreeTicketMicros是当前时间,acquire方法并不会阻塞本次调用,虽然本次之后nextFreeTicketMicros会被设置为未来的时间,相当于预获取了,那么后面再获取令牌的时候就必须在nextFreeTicketMicros时间之后才能获取到,因为从当前时间到nextFreeTicketMicros时间之间的令牌数已经被预支出了- 如果
returnValue=nextFreeTicketMicros是未来的一个时间,那么acquire方法会阻塞,因为本次调用reserveEarliestAvailable会返回一个阻塞时间
引用别人的^.^ - “打个比方:速率器相当于工厂,获取令牌许可的线程相当于经销商,经销商过来取货,工厂每天的生产的货品是一定的(100吨/天),A经销商来取货,第一天取了200吨货,工厂没有这么多货,怎么办呢?为了留住这个经销商,厂长做了决定,给200吨,现在的100吨先给你,明天的100吨也给你,然后把200吨货品的提取清单给了A经销商,A很满意的离开了。过了一会,B来了,B要10吨货物,这个时候,厂长就没有那么好说话了(谁让大客户已经到手了呢?),说10吨货物可以,你后天来吧,明天和今天的活已经都卖完了。这个时候通过这种方式,来限制一天只卖/生产100吨的货物。”
3.阻塞处理
@Override
void sleepMicrosUninterruptibly(long micros) {
if (micros > 0) {
//此处底层使用JUC包里面的TimeUnit工具类,且为Thread.sleep方法实现
Uninterruptibles.sleepUninterruptibly(micros, 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;
}
快速判断获取令牌能力(非常重要)
private boolean canAcquire(long nowMicros, long timeoutMicros) {
return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}
@Override
final long queryEarliestAvailable(long nowMicros) {
return nextFreeTicketMicros;
}
实际为:return nowMicros + timeoutMicros >= nextFreeTicketMicros;
这里有两种情况
-
如果当前时间加上需要等待的时间,仍然小于nextFreeTicketMicros,那么说明1)当前这次请求必须到nextFreeTicketMicros指定的时间,2)然后去计算能否获取足够令牌,如果不能满足1)则到了nowMicros + timeoutMicros必定不能获取到令牌,所以返回false,表示获取令牌失败;
-
如果当前时间加上需要等待的时间,仍然大于等于nextFreeTicketMicros,那么说明满足了上述的1),此时返回了ture,接下来就是去计算获取相应令牌数是否需要等待相应时间,并根据结果进行阻塞
一种特殊情况如下
-
如果延时timeoutMicros为0,则必定满足第二种情况,且由于nextFreeTicketMicros的值大于等于当前的时间,所以阻塞的时间会是0,也就是不阻塞,并且整个tryAcquire返回true
小结
1.向令牌桶添加令牌
在调用设置速率setRate方法,以及调用获取令牌的方法时,后置的去填充令牌数。因为在这两个地方调用了resync方法,该方法就是向令牌桶补充了令牌。
这里不是使用定时任务去生成。反过来想,如果使用了定时任务去生成,那么必然每个实例就会有一个定时任务,如果在项目系统中使用了很多RateLimiter实例去限流的话,系统中就会有很多定时任务,这样对系统资源的也是一种消耗
2.令牌预支取功能
本次请求消费令牌下次买单(详见上述分析)。
3.处理突发流量
当令牌桶中有剩余令牌数,可以给突然到来的流量提供令牌
4.实时修改限流速率
RateLimiter提供setRate方法动态修改限流速率permitsPerSecond
5.不支持修改maxBurstSeconds(该值默认固定为1s)
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
-
RateLimiter没有提供设置的方法,没有提供相应参数的create方法
-
SmoothRateLimiter、SmoothBursty类都是默认包的权限,因此无法在其它包下使用,无法做到扩展,其它包中仅能使用RateLimiter类和该类的方法
方案一:设置maxBurstSeconds目的是为了修改maxPermits=maxBurstSeconds*stableIntervalMicros,那么可以通过setRate方法间接设置生成令牌的间隔时间stableIntervalMicros,这样也能达到修改maxPermits的功能
方案二:在项目中新建与SmoothRateLimiter类一样的包路径,在该路径下新建扩展类,提供可以指定maxBurstSeconds参数的创建方法