最近看Eureka的源码,看到InstanceInfoReplicator对象的onDemandUpdate方法中采用令牌桶算法,来对方法进行限流,防止服务状态频繁变化导致scheduler中的任务过多。
这个令牌桶限流实现的非常简单,写得特别精简。
默认的限流是每分钟4个任务。
public boolean onDemandUpdate() {
// burstSize = 2
/// allowedRatePerMinute 默认值是 4
// allowedRatePerMinute = 60 * this.burstSize / this.replicationIntervalSeconds =
// = 60 * 2 / 30 = 4
if (rateLimiter.acquire(burstSize, allowedRatePerMinute)) {
if (!scheduler.isShutdown()) {
scheduler.submit(new Runnable() {
@Override
public void run() {
Future latestPeriodic = scheduledPeriodicRef.get();
if (latestPeriodic != null && !latestPeriodic.isDone()) {
logger.debug("Canceling the latest scheduled update, it will be rescheduled at the end of on demand update");
latestPeriodic.cancel(false);
}
InstanceInfoReplicator.this.run();
}
});
return true;
} else {
logger.warn("Ignoring onDemand update due to stopped scheduler");
return false;
}
} else {
logger.warn("Ignoring onDemand update due to rate limiter");
return false;
}
}
RateLimiter这个类非常简单,也就是100多行代码
public RateLimiter(TimeUnit averageRateUnit)
只支持两种级别:SECONDS(秒级别)和MINUTES(分钟级别)
最后都会转换成毫秒,因为后面的计算精确级别都是毫秒级的。
public RateLimiter(TimeUnit averageRateUnit) {
switch (averageRateUnit) {
case SECONDS:
rateToMsConversion = 1000;
break;
case MINUTES:
rateToMsConversion = 60 * 1000;
break;
default:
throw new IllegalArgumentException("TimeUnit of " + averageRateUnit + " is not supported");
}
}
对象获取令牌的方法public boolean acquire(int burstSize, long averageRate)
需要两个参数
burstSize:就是初始化桶的容量
averageRate:表示限流的速率,配合构造函数中指定的级别
如果构造函数是秒SECONDS的话,averageRate=2,就表示每秒钟生成2个令牌,
如果构造函数传入的是分钟MINUTES的话,averageRate=2,就表示每分钟生成2个令牌。
acquire方法是获取令牌的方法, true 表示获取成功,false 表示获取失败
获取令牌的第一步是检查桶是否需要进行填充,通过refillToken方法实现
public boolean acquire(int burstSize, long averageRate) {
return acquire(burstSize, averageRate, System.currentTimeMillis());
}
public boolean acquire(int burstSize, long averageRate, long currentTimeMillis) {
if (burstSize <= 0 || averageRate <= 0) { // Instead of throwing exception, we just let all the traffic go
return true;
}
refillToken(burstSize, averageRate, currentTimeMillis);
return consumeToken(burstSize);
}
private void refillToken(int burstSize, long averageRate, long currentTimeMillis) {
/// 获取上一次填充令牌的时间(将时间转为毫秒),如果是第一的话,值为0
long refillTime = lastRefillTime.get();
/// 时间差(毫秒) 当前时间和上一次时间的间隔,如果是第一次,这个时间差就是当前时间
long timeDelta = currentTimeMillis - refillTime;
/// 根据时间差 除以 速率, 来计算出,这段时间应该生成多少个令牌
如果是第一次, 计算出来的值会很大
long newTokens = timeDelta * averageRate / rateToMsConversion;
/// 如果计算出来的值大于0,就表示这段时间应该产生新的令牌
if (newTokens > 0) {
计算最新的填充时间, 这里其实直接赋值当前时间currentTimeMillis 就可以
///因为refillTime + newTokens * rateToMsConversion / averageRate 的计算结果也就是currentTimeMillis
long newRefillTime = refillTime == 0
? currentTimeMillis
: refillTime + newTokens * rateToMsConversion / averageRate;
/// 通过 CAS 来判断,是否有其他线程修改过,解决多线的问题
if (lastRefillTime.compareAndSet(refillTime, newRefillTime)) {
///
while (true) {
/// 当前桶里已经消费的令牌数
int currentLevel = consumedTokens.get();
调整令牌数, 不能超过burstSize 桶的总容量,防止桶容量减少的情况
int adjustedLevel = Math.min(currentLevel, burstSize); // In case burstSize decreased
已经消费的令牌 减去 新成的令牌,进行复原 最小为0
int newLevel = (int) Math.max(0, adjustedLevel - newTokens);
/// 通过 CAS + while 直到修改成功
if (consumedTokens.compareAndSet(currentLevel, newLevel)) {
return;
}
}
}
}
}
private boolean consumeToken(int burstSize) {
while (true) {
/// 获取到当前已经消费的令牌数量
int currentLevel = consumedTokens.get();
/// 如果 大于或者等于了 桶的容量,说明没有令牌可消费了,返回 false
if (currentLevel >= burstSize) {
return false;
}
/// 否则进行 通过 CAS + while 循环进行消费,数量加1
if (consumedTokens.compareAndSet(currentLevel, currentLevel + 1)) {
return true;
}
}
}