逐行拆解Guava限流器RateLimiter

逐行拆解Guava限流器RateLimiter

常见限流算法

计数器法

  • 设置一个时间窗口内允许的最大请求量,如果当前窗口请求数超过这个设定数量,则拒绝该窗口内之后的请求。

  • 关键词:时间窗口,计数器。

  • 举个例子,我们设置1秒钟的最大请求数量为100,使用一个计数器来记录这一秒中的请求数。每一秒开始时,计数器都从0开始记录请求量,如果在这一秒内计数器达到了100,则在这一秒未结束时,之后的请求全部拒绝。

  • 计数器法是一个简单粗暴的方法。也就是因为简单粗暴,会有一定的问题。比如0.9秒时有100个请求,1.1秒时也有100个请求。按照计数器法的计算,第一秒和第二秒确实都在各自的时间范围内限制了100个请求,但是从0.5秒1.5秒这一秒之间有200个请求,这是计数器法的临界问题。如果请求量集中在两个时间窗口的临界点,会产生请求的尖峰。

  • 在这里插入图片描述

  • 我们可以引用滑动窗口的方式更加精细的划分时间窗口解决这个问题。将1秒钟细分为10个格子,每个格子用单独的计数器计算当前100毫秒的请求。随着时间的流逝,我们的窗口也跟着时间滑动,每次都是计算最近10个格子的请求量。如果最近10个格子的请求总量超过了100,则下一个格子里就拒绝全部的请求。格子数量越多,我们对流量控制的也就越精细,这部分逻辑可以借助LinkedList来实现。

  • 而滑动窗口的缺点是,需要一直占用内存空间保存最近一个时间窗口内每个格子的请求。

漏桶算法

  • 我们想象有一个固定容量的桶,桶底有个洞。请求就是从上往下倒进来的水,可能水流湍急,也可能是涓涓细流。因为桶的容量是固定的,所以灌进来太多的水,溢出去了我们就用不了了。所以无论怎样,桶底漏出来的水,都是匀速流出的。而我们要做的,就是处理这些匀速流出的水就好了。
  • 关键词:流入速率不定,流出速率恒定,只处理流出的请求。
  • 漏桶的优点就是,需要处理的请求一直都是固定速率的,特别稳定。而对应的缺点就是,漏桶没有办法解决突发流量的情况。

令牌桶算法

  • 相比于漏桶而言,令牌桶的处理方式完全相反。我们想象依旧有一个固定容量的桶,不过这次是以稳定的速率生成令牌放在桶中。当有请求时,需要获取令牌才能通过。因为桶的容量固定,令牌满了就不生产了。桶中有充足的令牌时,突发的流量可以直接获取令牌通过。当令牌取光后,桶空了,后面的请求就得等生成令牌后才能通过,这种情况下请求通过的速率就变得稳定了。
  • 关键词:令牌生成速率固定,能取到令牌即可通过。
  • 令牌桶的优点是可用处理突发流量,而且被广泛的应用于各种限流工具中。比如Guava的RateLimiter。下面我们就来由浅及深的了解一下,RateLimiter中的令牌桶是怎么实现的吧。

RateLimiter令牌桶解析

教你如何用令牌桶的方式卖包子

  • 我们先不看源码,举个形象的例子先感受一下,RateLimiter类是如何实现令牌桶逻辑的。
  • 小白包子铺因为小笼包皮薄馅香汤汁多而生意兴隆,每天都会有很多人排队来吃。因为蒸笼个数有限,后厨师傅的人力也有限,每分钟只能出锅一屉小笼包,每个小时一共能蒸出60屉小笼包供顾客享用。如果这60屉小笼包没人买,就先不蒸了,反正也没地方放。
  • 每位顾客需要先在门口付买包子的钱,然后进到铺子里吃包子。如小笼包都卖没了,门口的顾客就只能付完钱等着,等有新出炉的小笼包可卖时,再进门享用。
  • 包子铺每天早上7点准时开门,师傅开始制作这60屉小笼包【RateLimiter rateLimiter = RateLimiter.create(60)】。顾客蜂拥而至排队,第一个顾客先付一屉小笼包的钱【rateLimiter.acquire()】,等一分钟后小笼包出锅,便可以进屋吃包子了。这时伙计会和顾客说:“您本次等待了1分钟,久等了。”【rateLimiter.acquire()返回的值】。之后是第二个顾客,付钱,等一分钟,进屋吃包子。于是,早上大家就这样一直稳定有序的进屋吃包子。这事来了个火急火燎的上班族,说:“我着急吃饭,不想等,有现成的包子吗?”【rateLimiter.tryAcquire()】伙计看了一眼后厨说:“没有现成的包子,需要等会哦。”一看还要等,第三个顾客就走了。
  • 早高峰过去后,买包子的人少了,师傅做完60个包子后,就休息了。
  • 中午12点,隔壁科技园的程序员们下班了,顾客蜂拥而至。因为已经有了60个库存,大家不用等,付了钱就可以直接进屋吃包子,屋子里一下就涌入了60个顾客。看到小笼包都买光了,后厨师傅又赶紧忙了起来。
  • 这时来了个大胃王,大嗓门喊道:“我想要30屉包子。”【rateLimiter.acquire(30)】伙计一愣,心想,这一下也做不出这么多包子呀。但是本着客户至上的原则,他对大胃王说:“好的先生,不过我们的包子都是现做的,您先吃着,我们蒸好了陆陆续续给您端上来可以吗?”大胃王说:“哈哈,好!好饭不怕晚!”于是交钱进门吃包子了。这时候又来了个顾客A,说我想要一屉包子。伙计心里一算,现在是一点,等刚才那个大胃王要的包子上齐了,才能给这位顾客上包子,所以需要等到1:30才能给这位顾客吃上,好惨一顾客。于是说:“不好意思这位顾客,小笼包现在没有了,您需要等一段时间。”顾客A说:“好吧,我交了钱等着吧。”于是过了半个小时,后厨做出来了第31屉包子,顾客A就满足的进屋吃包子了。

RateLimiter的重要思想

  • 上一节的例子里,我们可以这样理解:每一屉小笼包就是一个令牌,包子铺的后厨就是令牌桶。
    1. 令牌桶有最大令牌数限制,最多只能装60个令牌,再多就装不下了。
    2. 令牌的生成也是匀速的,每个小时生成60个令牌,如果时刻都有人来取令牌,那这些人会保持均匀的速率领取。(SmoothBursty模式和SmoothWarmingUp模式稳定期)
    3. 如果之前累积了大量的令牌,一旦有大量的请求来获取令牌,这些请求都会被通过。
    4. RateLimiter是预获取令牌模式,即:如果一次性被消耗掉大于已有数量的令牌,RateLimiter的策略是先满足你,不够的令牌数我先欠着,等后续生成了足够的令牌后,再给下一个请求发放令牌。
    5. 令牌桶通过记录下一次请求可发放令牌的时间点来确定是否接受下一个请求,以及下一个请求需要等待的时间。

RateLimiter类源码解析

  • RateLimiter的源码其实很简单,只涉及到RateLimiter和SmoothRateLimiter两个类文件。其中RateLimiter类是一个抽象类,定义了限流器常用的方法。具体的逻辑都是通过其子类SmoothRateLimiter来实现的。
  • 在这里我们主要看最核心的部分,来了解中RateLimiter的基本工作原理。
计时器
  • 首先要说一下RateLimiter类中的计时器。

  • 计时器随限流器初始化时创建,贯穿始终,伴随限流器漫长的一生。令牌桶中数量的计算、是否到达可以接收请求的时间点都需要计时器参与。我认为计时器是限流器成立的基础。

  • 限流器使用的是SleepingStopwatch这个内部类作为计时器。而SleepingStopwatch的则是通过持有一个Stopwatch对象来实现计时和休眠。

  • /**
     * The underlying timer; used both to measure elapsed time and sleep as necessary. A separate
     * object to facilitate testing.
     * 一个底层的限流器,用于测量运行时间和睡眠时间
     */
    private final SleepingStopwatch stopwatch;
    
    abstract static class SleepingStopwatch {
        /**
         * Constructor for use by subclasses.
         */
        protected SleepingStopwatch() {
        }
    
        /**
         * 获取当前时间点
         *
         * @return
         */
        protected abstract long readMicros();
    
        /**
         * 按照计算的睡眠时间等待休眠
         *
         * @param micros
         */
        protected abstract void sleepMicrosUninterruptibly(long micros);
    
        public static SleepingStopwatch createFromSystemTimer() {
            return new SleepingStopwatch() {
                final Stopwatch stopwatch = Stopwatch.createStarted();
    
                @Override
                protected long readMicros() {
                    return stopwatch.elapsed(MICROSECONDS);
                }
    
                @Override
                protected void sleepMicrosUninterruptibly(long micros) {
                    if (micros > 0) {
                        Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS);
                    }
                }
            };
        }
    }
    
重要的变量
  • 再看一下SmoothRateLimiter类中我们需要记住的几个变量。令牌桶的令牌生成、消耗、领取和等待都是对这几个变量的操作,这些变量约束控制令牌桶的空间状态和令牌生成速度。

  • 具体每个变量是做什么的,关注代码里面的注释哦。

  • /**
     * The currently stored permits.
     * 当前桶中已存在的令牌数,如果请求需要的令牌数量小于已存在的令牌数,就可以直接领了令牌去搞事情啦
     */
    double storedPermits;
    
    /**
     * The maximum number of stored permits.
     * 令牌桶可以保存的最大令牌数,达到这个数量,就不再生产令牌了,装不下啦
     */
    double maxPermits;
    
    /**
     * 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.
     * 稳定令牌生成间隔时间,单位是微秒。其实这个数字是根据设置令牌桶时计算的:1s/每秒生成令牌数量(QPS)。当然,后期如果修改了每秒生成令牌的数量,这个值也是会变的。
     */
    double stableIntervalMicros;
    
    /**
     * 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.
     * 下一个请求可以被允许获取令牌的时间点,单位是微秒。
     */
    private long nextFreeTicketMicros = 0L;
    
核心方法使用与原理
  • 了解了控制限流器时间和空间相关的变量,我们就来看一下核心流程,限流器是如何工作的吧。

  • Guava对RateLimiter有两种实现方式。

    • SmoothBursty:平滑突发限流,以稳定的速率生成令牌。下文中称SmoothBursty实现的限流器为稳定限流器。
    • SmoothWarmingUp:平滑预热限流,随着请求量的增加,令牌生成速率会缓慢提升直到一个稳定的速率。下文称SmoothWarmingUp实现的限流器为预热限流器。
  • 其中,稳定限流器支持处理突发请求。比如令牌桶现有令牌数为5,这时连续进行10个请求,则前5个请求会全部直接通过,没有等待时间,之后5个请求则每隔200毫秒通过一次。

  • 而预热限流器则只会让第一个请求直接通过,之后的请求都会有等待时间,等待时间不断缩短,直到稳定在每隔200毫秒通过一次。这样,就会有一个预热的过程。

  • 先上一个简单的demo,直观的感受一下这两种限流器的差别。

  • /**
     * 突发请求情况对比
     */
    public static void compareBurstyRequest() {
        RateLimiter burstyLimiter = RateLimiter.create(5);
        RateLimiter warmingUpLimiter = RateLimiter.create(5, 2, TimeUnit.SECONDS);
    
        DecimalFormat df = new DecimalFormat("0.00");
        // 积攒1秒
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
        System.out.println("稳定限流器");
        IntStream.range(0, 10).forEach(a -> {
            double acquire = burstyLimiter.acquire();
            System.out.println("第" + a + "次请求等待时间:" + df.format(acquire));
        });
    
        System.out.println("预热限流器");
        IntStream.range(0, 10).forEach(a -> {
            double acquire = warmingUpLimiter.acquire();
            System.out.println("第" + a + "次请求等待时间:" + df.format(acquire));
        });
    }
    
  • 代码执行结果对比如下:

  • 在这里插入图片描述

  • 结果一目了然。

  • 好啦,那我们开始代码拆解之旅吧。

创建限流器

  • RateLimiter提供了两个静态方法来创建限流器,其中一个是创建稳定限流器,创建时只需要传入每秒生成的令牌数即可。具体逻辑和讲解都在代码的注释里啦,注意跟着代码走哦~

  • // 创建稳定限流器
    RateLimiter burstyLimiter = RateLimiter.create(5);
    
  • 它是由SmoothBursty实现的,比较简单:

  • static final class SmoothBursty extends SmoothRateLimiter {
        /**
         * The work (permits) of how many seconds can be saved up if this RateLimiter is unused?
         * 用于计算maxPermits,在初始化时默认设置为1秒。所以最多存储1秒内生成的令牌数
         */
        final double maxBurstSeconds;
    
        SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
            super(stopwatch);
            this.maxBurstSeconds = maxBurstSeconds;
        }
    
        @Override
        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 {
                // 如果oldMaxPermits为0,则说明是初始化限流器,当前令牌桶已存在令牌置为0。
                // 否则按照新的最大令牌数等比例的扩大已存在的令牌数
                storedPermits = (oldMaxPermits == 0.0) ? 0.0  : storedPermits * maxPermits / oldMaxPermits;
            }
        }
    
        @Override
        long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
            return 0L;
        }
    
        @Override
        double coolDownIntervalMicros() {
            return stableIntervalMicros;
        }
    }
    
  • 另一个是预热限流器,创建时需要传入每秒生成的令牌数和预热时间。

  • // 创建预热限流器
    RateLimiter warmingUpLimiter = RateLimiter.create(5, 2, TimeUnit.SECONDS);
    
  • 预热限流器由SmoothWarmingUp实现,有些复杂。我们先看涉及到构造方法的这部分就好,后面的storedPermitsToWaitTime方法等到下一节涉及到获取令牌时的逻辑再看。

  • static final class SmoothWarmingUp extends SmoothRateLimiter {
        /**
         * 预热时间,单位毫秒
         */
        private final long warmupPeriodMicros;
        /**
         * The slope of the line from the stable interval (when permits == 0), to the cold interval
         * (when permits == maxPermits)
         * 斜率,用于计算坐标系中梯形面积
         */
        private double slope;
    
        /**
         * 预热期与稳定期令牌数临界值
         */
        private double thresholdPermits;
    
        /**
         * 冷却期因子,用于计算冷却期产生令牌时间间隔
         */
        private double coldFactor;
    
        SmoothWarmingUp(
                SleepingStopwatch stopwatch, long warmupPeriod, TimeUnit timeUnit, double coldFactor) {
            super(stopwatch);
            this.warmupPeriodMicros = timeUnit.toMicros(warmupPeriod);
            this.coldFactor = coldFactor;
        }
    
        /**
         * @param permitsPerSecond     每秒生成令牌数,SmoothWarmingUp用不上
         * @param stableIntervalMicros 稳定期令牌生成间隔时间
         */
        @Override
        void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
            double oldMaxPermits = maxPermits;
            // 冷却期产生令牌时间间隔 = 稳定期令牌生成间隔时间 * 冷却期因子
            double coldIntervalMicros = stableIntervalMicros * coldFactor;
            // 临界值 = 0.5 * 预热时间 / 稳定期令牌生成间隔时间
            // 源码的注释中说,从maxPermits到thresholdPermits,需要用warmupPeriod的时间,而从thresholdPermits到0需要warmupPeriod一半的时间
            // 因为前面coldFactor被硬编码为3.好吧~
            thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;
            // 最大令牌数 = 临界值 + 2 * 预热时间 / (稳定期令牌生成间隔时间 + 冷却期产生令牌时间间隔)
            // 为什么是这么算出来的呢?源码的注释已经给出了推导过程。因为坐标系中右边的梯形面积就是预热的时间,即:
            // warmupPeriodMicros = 0.5 * (stableInterval + coldInterval) * (maxPermits - thresholdPermits)
            // 因此可以推导求出maxPermits:
            maxPermits = thresholdPermits + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);
    
            // 咳咳,这里其实仔细算算,一顿操作猛如虎,因为coldFactor默认为3,所以其实:
            // maxPermits = warmupPeriodMicros / stableIntervalMicros
            // thresholdPermits = maxPermits / 2
    
            // 斜率 = (冷却期产生令牌时间间隔 - 稳定期令牌生成间隔时间) / (最大令牌数 - 临界值)
            // 看图,是小学数学题的求斜率哦
            slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);
    
            // 这部分和稳定限流器的逻辑不一样,初始化时默认storedPermits就为maxPermits
            if (oldMaxPermits == Double.POSITIVE_INFINITY) {
                // if we don't special-case this, we would get storedPermits == NaN, below
                storedPermits = 0.0;
            } else {
                storedPermits = (oldMaxPermits == 0.0) ? maxPermits : storedPermits * maxPermits / oldMaxPermits;
            }
        }
    
        /**
         * 计算令牌桶的等待时间
         * 怎么计算时间呢?我们用给的这个坐标系来计算。
         * 其中纵坐标是生成令牌的间隔时间,横坐标是桶中的令牌数。
         * 我们要计算消耗掉permitsToTake个令牌数需要的时间,实际上就是求坐标系中的面积
         * 所以storedPermitsToWaitTime方法实际上就是求三种情况下的面积
         *
         * @param storedPermits 令牌桶中存在的令牌数
         * @param permitsToTake 本次消耗掉的令牌数
         * @return
         */
        @Override
        long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
            // 当前令牌桶内的令牌数和临界值的差值
            double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
            long micros = 0;
            // measuring the integral on the right part of the function (the climbing line)
            // 高于临界值,则需要根据预热算法计算时间
            if (availablePermitsAboveThreshold > 0.0) {
                double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
                // TODO(cpovirk): Figure out a good name for this variable.
                // 吐槽:你看,大佬写代码时也不会起变量名
                // 而且这个todo好几年了也没有do,所以不要写todo,这和"下次一定"没啥差别
                double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
                micros = (long) (permitsAboveThresholdToTake * length / 2.0);
                permitsToTake -= permitsAboveThresholdToTake;
            }
            // measuring the integral on the left part of the function (the horizontal line)
            // 如果不走上面的分支,则说明当前限流器不处于预热阶段,那么,等待时间 = 消耗掉的令牌数 * 稳定令牌生成间隔时间
    
            micros += (long) (stableIntervalMicros * permitsToTake);
            return micros;
        }
    
        private double permitsToTime(double permits) {
            return stableIntervalMicros + permits * slope;
        }
    
        @Override
        double coolDownIntervalMicros() {
            return warmupPeriodMicros / maxPermits;
        }
    }
    
  • 对于注释中所说的简化计算maxPermits = warmupPeriodMicros / stableIntervalMicros的过程,推导如下:

  • 在这里插入图片描述

获取令牌

  • 限流器创建成功后,就可以开始使用了。无论是稳定限流器,还是预热限流器,获取令牌的方法都是一致的。
  • 其中包括acquire方法和tryAcquire方法。
acquire
  • acquire方法有两个:

  • acquire(int permits):获取一定数量的令牌,等待并返回等待时间

  • acquire():等同于acquire(1)

  • 通过源码来看看获取令牌数时都做了什么。

  • 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()) {
            return reserveAndGetWaitLength(permits, stopwatch.readMicros());
        }
    }
    
tryAcquire
  • tryAcquire方法一共有四个,但是实际上最终调用的都是tryAcquire(int permits, long timeout, TimeUnit unit)方法。

  • /**
     * 如果不能在超时时间内获取到令牌,则直接返回false
     * 如果可以,则在超时时间内等待并获取令牌,返回true
     */
    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;
    }
    
  • 简单总结一下,acquire是直接获取令牌,如果还没有到达接受下一个请求的时间点,就继续等待。而tryAcquire则如其名,我先试试,如果在我允许的时间范围内获取不到就不等了,如果能获取到那我就等着。

  • 我们可以发现,acquire和tryAcquire方法都是通过reserveAndGetWaitLength方法获取等待时间。而reserveAndGetWaitLength方法也是根据reserveEarliestAvailable这个核心方法来计算等待时间的。所以我们就来扒一扒reserveEarliestAvailable是怎么实现的吧。

reserveEarliestAvailable
  • 该方法主要做了以下事情:

  • 根据当前时间,计算出桶内已有的令牌数。

  • 根据本次请求需要的令牌数和桶内的令牌数,计算出可以直接获取的令牌数和还需要预获取的令牌数。

  • 计算直接获取令牌数需要消耗的时间和预获取令牌数消耗的时间,计算出下一次可以获取令牌数的时间点,并返回本次请求可以获取令牌的时间点。

  • /**
     * Reserves next ticket and returns the wait time that the caller must wait for.
     * 预定令牌并获取等待的时间长度,acquire和tryAcquire最后调用的都是它
     *
     * @return the required wait time, never negative
     */
    final long reserveAndGetWaitLength(int permits, long nowMicros) {
        long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
        return max(momentAvailable - nowMicros, 0);
    }
    
    /**
     * 该方法返回最早可以获取令牌数的时间点,并重新计算nextFreeTicketMicros和storedPermits
     *
     * @param requiredPermits
     * @param nowMicros
     * @return
     */
    @Override
    final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
        // 根据当前时间判断是否产生了新的令牌,如果是则更新当前时间点的桶内令牌数和下一个可获取令牌的时间点
        resync(nowMicros);
        // 本次请求可以获取令牌的时间点,那自然是nextFreeTicketMicros啦
        long returnValue = nextFreeTicketMicros;
        // 本次实际获取的令牌数量。比较一下需要的数量和当前桶中的数量,只能两者取其小了。
        double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
        // 领取后还需要预获取的令牌数
        double freshPermits = requiredPermits - storedPermitsToSpend;
    
        // 分别计算storedPermitsToSpend和freshPermits的等待时间
        // 预获取的令牌还需要占用的时间 = 预获取的令牌数 * 令牌生成间隔
        // 通过storedPermitsToWaitTime方法计算本次获取令牌消耗的时间
        // 在使用默认限流器时,直接返回0。如果是预热限流器,那就需要根据限流器当前的状态计算额外需要等待的时间
        long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros);
        // 重置nextFreeTicketMicros,即当前的nextFreeTicketMicros+新增的等待时间
        this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
        // 重置storedPermits,减去本次消耗的令牌数,就是当前桶中剩余的令牌数
        this.storedPermits -= storedPermitsToSpend;
    
        return returnValue;
    }
    
    void resync(long nowMicros) {
        // if nextFreeTicket is in the past, resync to now
        // 如果nextFreeTicket时间点已经过期了,那就更新成当前时间
        if (nowMicros > nextFreeTicketMicros) {
            // 从nextFreeTicket时间点到当前时间差,除以令牌生成时间间隔,就是这段时间产生的令牌数
            double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
            // nextFreeTicket时间点桶内存在的数据 + 这段时间新生成的令牌数,就是当前时间点桶内的令牌数。
            // 但是桶有限制,所以我们只能和桶的最大容量比较一下,取二者中较小的值,置为新的桶内存在的令牌数
            storedPermits = min(maxPermits, storedPermits + newPermits);
            // nextFreeTicketMicros置为当前时间点
            nextFreeTicketMicros = nowMicros;
        }
    }
    
  • 获取令牌的逻辑就是以上这些啦。

  • 现在我们回到刚才创建限流器那节说的,限流器的storedPermitsToWaitTime方法是做什么用的。

计算获取令牌时间

  • 我们回到刚才reserveEarliestAvailable方法中的这一行:

  • // 分别计算storedPermitsToSpend和freshPermits的等待时间
    // 预获取的令牌还需要占用的时间 = 预获取的令牌数 * 令牌生成间隔
    // 通过storedPermitsToWaitTime方法计算本次获取令牌消耗的时间
    // 在使用默认限流器时,直接返回0。如果是预热限流器,那就需要根据限流器当前的状态计算额外需要等待的时间
    long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend) + (long) (freshPermits * stableIntervalMicros);
    
  • 这里计算的等待时间包括两部分,一部分是获取已存在令牌时需要消耗的时间,另一部分是生成预获取令牌数需要消耗的时间。

  • 无论是稳定限流器还是预热限流器,生成预获取令牌数需要消耗的时间计算方法都是一致的,用令牌数×稳定的令牌生成间隔时间。因为都要预获取了,说明令牌桶里的令牌是不够用的,预热限流器已经进入了稳定生成令牌的阶段,所以两种限流器的算法是一致的。

  • 而二者的差别在于如何计算已获取令牌需要消耗的时间。

  • 对于稳定限流器来说,它是可以应对突增请求的,因此在获取已存在令牌时是不应该消耗时间的,因此SmoothBursty类中的storedPermitsToWaitTime方法直接return了0。

  • 对于预热限流器来说,它有一个预热的阶段。如果像稳定限流器一样,获取已存在的令牌不需要消耗时间,那么他就没有了预热效果。因此它在获取桶中已存在的令牌时,是需要计算消耗时间的。这样既保证下一次请求需要等待一段时间再通过,也可以在这段时间里再生成一些令牌,进而保证他的预热效果。

  • 我们再回过头看看预热限流器的storedPermitsToWaitTime方法是怎么实现的。仔细看代码中的注释哈。

  • /**
     * 计算获取令牌桶中已有令牌的等待时间
     * 怎么计算时间呢?我们用源代码中给的这个坐标系来计算。
     *          ^ throttling
     *          |
     *    cold  +                  /
     * interval |                 /.
     *          |                / .
     *          |               /  .   ← "warmup period" is the area of the trapezoid between
     *          |              /   .     thresholdPermits and maxPermits
     *          |             /    .
     *          |            /     .
     *          |           /      .
     *   stable +----------/  WARM .
     * interval |          .   UP  .
     *          |          . PERIOD.
     *          |          .       .
     *        0 +----------+-------+--------------→ storedPermits
     *          0 thresholdPermits maxPermits
     * 其中纵坐标是生成令牌的间隔时间,横坐标是桶中的令牌数。
     * 我们要计算消耗掉permitsToTake个令牌数需要的时间,实际上就是求坐标系中x轴permitsToTake这部分和图像围成的面积
     * 所以storedPermitsToWaitTime方法实际上就是求三种情况下的面积
     *
     * @param storedPermits 令牌桶中存在的令牌数
     * @param permitsToTake 本次消耗掉的令牌数
     * @return
     */
    @Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
        // 当前令牌桶内的令牌数和临界值的差值
        double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
        long micros = 0;
        // measuring the integral on the right part of the function (the climbing line)
        // 高于临界值,则需要根据预热算法计算时间
        if (availablePermitsAboveThreshold > 0.0) {
            double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);
            // TODO(cpovirk): Figure out a good name for this variable.
            // 吐槽:你看,大佬写代码时也不会起变量名
            // 而且这个todo好几年了也没有do,所以不要写todo,这和"下次一定"没啥差别
            double length = permitsToTime(availablePermitsAboveThreshold) + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake);
            micros = (long) (permitsAboveThresholdToTake * length / 2.0);
            permitsToTake -= permitsAboveThresholdToTake;
        }
        // measuring the integral on the left part of the function (the horizontal line)
        // 如果不走上面的分支,则说明当前限流器不处于预热阶段,那么,等待时间 = 消耗掉的令牌数 * 稳定令牌生成间隔时间
        micros += (long) (stableIntervalMicros * permitsToTake);
        return micros;
    }
    
  • 注释中说的三种情况,就是在以下三种情况下,求函数图像和x轴之间围成的面积,如下: 第一种,限流器在预热期,获取permitsToTake个令牌后,桶中的令牌数多于thresholdPermits,如图:

  • 在这里插入图片描述

  • 第二种,限流器在预热期,获取permitsToTake个令牌后,桶中的令牌数少于thresholdPermits,如图:

  • 在这里插入图片描述

  • 第三种,限流器处于稳定期,如图:

  • 在这里插入图片描述

  • 这样就容易理解一些了。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/381211.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

解决“使用Edge浏览器每次鼠标点击会出现一个黑色边框”的问题

目录 一 问题描述 二 解决方案 三 方案来源 四 参考资料 & AI工具 一 问题描述 为了方便进行收藏夹同步,开始从Chrome浏览器切换到Edge浏览器。在使用Edge浏览器过程中发现“每次鼠标点击会出现一个黑色边框”(效果如下图所示)&#…

基于物联网的实时数据分析(简单介绍)

在当今这个信息化、数字化飞速发展的时代,物联网(Internet of Things, IoT)和实时数据分析成为了技术革新的两大支柱。对于刚入行的新手来说,理解这两个概念及其相互作用不仅是迈入这一领域的第一步,更是掌握未来技术趋…

Android 粒子喷泉动效

一、前言: 在学习open gl es实现动效的时候,打算回顾了一下用普通的2D坐标系实现粒子效果和 open gl 3d 坐标系的区别,以及难易程度,因此本篇以Canvas 2D坐标系实现了一个简单的demo。 粒子动效原理: 粒子动效本质上…

Springboot 整合 Elasticsearch(二):使用HTTP请求来操作ES

📁前情提要:Springboot整合Elasticsearch(一):Linux下安装 Elasticsearch 8.x 目录 一、使用 elasticsearch-head 插件连接 1、下载压缩包 2、在 chrome 浏览器中添加扩展程序 3、修改IP地址,点击连接 …

Excel+VBA处理高斯光束

文章目录 1 图片导入与裁剪2 获取图片数据3 数据拟合 1 图片导入与裁剪 插入图片没什么好说的,新建Excel,【插入】->【图片】。 由于图像比较大,所以要对数据进行截取,选中图片之后,点击选项卡右端的【图片格式】…

Postgresql 的编译安装与包管理安装, 全发行版 Linux 通用

博客原文 文章目录 实验环境信息编译安装获取安装包环境依赖编译安装安装 contrib 下工具代码 创建用户创建数据目录设置开机自启动启动数据库常用运维操作 apt 安装更新源安装 postgresql开机自启修改配置修改密码 实验环境信息 Ubuntu 20.04Postgre 16.1 编译安装 获取安装…

HiveSQL——不使用union all的情况下进行列转行

参考文章: HiveSql一天一个小技巧:如何不使用union all 进行列转行_不 union all-CSDN博客文章浏览阅读881次,点赞5次,收藏10次。本文给出一种不使用传统UNION ALL方法进行 行转列的方法,其中方法一采用了concat_wsposexplode()方…

[经验] 喉咙沙哑的原因及应对方法是什么 #学习方法#其他#媒体

喉咙沙哑的原因及应对方法是什么 生活中,喉咙不舒服是很常见的情况,尤其是喉咙沙哑,让人感到特别难受,影响睡眠和生活质量。那么喉咙沙哑怎么办呢?接下来我会分享一些简单易行的方法,帮助你缓解这种不适感…

搭建yum仓库服务器

安装 1.安装linux 1.1安装依赖 yum -y install gcc zlib zlib-devel pcre-devel openssl openssl-devel 1.2下载 cd /opt/nginx wget http://nginx.org/download/nginx-1.25.3.tar.gz 1.3解压 tar -xvf nginx-1.25.3.tar.gz 1.4配置 cd nginx-1.25.3 ./configure --pre…

【Larry】英语学习笔记语法篇——从句=连词+简单句

目录 三、从句连词简单句 1、必须有连词 主从结构 疑问词的词性 2、名词性从句 同位语从句 形式主语 形式宾语 that的省略 3、形容词性从句(上) 关系代词 关系词的作用 介词前置问题 4、形容词性从句(中) 定语关系…

程序的内存模型

内存分区模型 C程序在执行时,将内存大方向分为4个区域 1.代码区:存放函数体的二进制代码,有操作系统进行管理 2.全局区:存放全局变量和静态变量以及常量 3.栈区:由编译器自动分配及释放,存放函数的参数…

开源微服务平台框架的特点是什么?

借助什么平台的力量,可以让企业实现高效率的流程化办公?低代码技术平台是近些年来较为流行的平台产品,可以帮助很多行业进入流程化办公新时代,做好数据管理工作,从而提升企业市场竞争力。流辰信息专业研发低代码技术平…

InternLM大模型实战-2.浦语大模型趣味demo

文章目录 前言笔记正文3个Demo的简要介绍InternLM模型简介Lagent介绍书生灵笔多模态大模型 Demo动手实践模型的下载更多 前言 本文是对于InternLM全链路开源体系系列课程的学习笔记。视频教程:【轻松玩转书生浦语大模型趣味Demo】 https://www.bilibili.com/video/…

appears to be hung in Auto SQL Tuning task

appears to be hung in Auto SQL Tuning task Oracle 自动定时优化任务执行失败分析 错误现象: Sat Feb 10 03:10:57 2024 Process 0x0x00007FFB81BE44A8 appears to be hung in Auto SQL Tuning task Current time 1707505857, process death time 1707505803 …

【医学大模型 尘肺病】PneumoLLM:少样本大模型诊断尘肺病新方法

PneumoLLM:少样本大模型诊断尘肺病新方法 提出背景PneumoLLM 框架效果 提出背景 论文:https://arxiv.org/pdf/2312.03490.pdf 代码:https://github.com/CodeMonsterPHD/PneumoLLM/tree/main 历史问题及其背景: 数据稀缺性问题&a…

DevOps落地笔记-13|自动化测试:提高测试效率的不二之选

上一课时主要介绍了通过 API 管理平台来管理企业内部的 API。持续集成是能够保证软件处于可工作状态的实践,但实施持续集成有一个必不可少的步骤——测试。只有尽可能全面的测试覆盖,才能降低软件出错的概率。但是,大多数企业里还是基于人工来…

第二十六回 母夜叉孟州道卖人肉 武都头十字坡遇张青-Ubuntu 防火墙ufw配置

武松到县里投案,县官看武松是个汉子,就把诉状改成:武松与嫂一时斗殴杀死,后西门庆前来,两人互殴,打死西门庆。上报东平府。东平府尹也可怜武松,从轻发落,最后判了个:脊杖…

2024 年 12 款最佳录屏软件【录屏必备指南】

在数字时代,共享屏幕就像发送短信一样常见。 无论是工作、创建教程还是流媒体游戏,找到合适的截屏软件可以决定您的在线形象。 我们测试并整理了一系列最佳的截屏和屏幕录制工具,可将您的内容提升到一个新的水平。 从功能丰富的选项到用户…

RM电控--机械入门

SW常用的快捷键: 多种视角观看: 左侧为自攻螺丝,右侧为钻尾螺丝 钻尾螺丝可以依靠自身进行钻孔操作,而自攻螺丝打之前必须先打好小孔。 螺钉; 这些螺钉大家认得全吗?你还知道哪些呢?_哔哩哔哩_bilibili …

今年春节联欢晚会中的扑克魔术到底是咋变的?

今年的刘谦给全国观众带来了俩魔术,一个是洗牌一个是撕牌,前面第一个魔术看不出来太神奇了,但是第二魔术感觉挺有趣的我可以简单分析分析。 然后我们列出这个魔术的关键步骤: 打乱四张牌 1 2 3 4 对折、撕开、面向同一个方向重…