Eureka 的令牌桶算法RateLimiter

最近看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这个类

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");
        }
    }
两个参数 burstSize、averageRate

对象获取令牌的方法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);
    }
填充令牌refillToken
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;
                    }
                }
            }
        }
    }
消费令牌consumeToken
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;
            }
        }
    }

你可能感兴趣的:(Java,源码)