RateLimiter 平滑限流之非突发、非预热

平滑限流

什么是平滑限流? 平就是平稳、滑是没有折线(好像也不太准确),没有曲线?(曲线其实也可以有Smooth的意思)丝滑?总之是比较Smooth就对了。guava中RateLimiter 的实现只有平滑限流的实现,即SmoothRateLimiter。 而SmoothRateLimiter 也是抽象的,它有两个实现,一个是突发实现即SmoothBursty,一个是预热实现即SmoothWarmingUp。

突发限流的方法是一个参数,而预热限流的方法是三个参数的方法,具体可见源码:

static RateLimiter create(
      SleepingStopwatch stopwatch, double permitsPerSecond, long warmupPeriod, TimeUnit unit) {
    RateLimiter rateLimiter = new SmoothWarmingUp(stopwatch, warmupPeriod, unit);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }

可见预热限流是SmoothWarmingUp

预热时间为0 的预热限流

预热时间为0 的预热限流 是一个什么情况?

注意到上方法的第一个参数是和突发限流的方法是一样的,都是每秒的许可数。上方法的第二个参数是预热时间。最后一个参数是时间单位。 第一个参数自然是不能小于等于0的, 不能失去了意义。 第二个参数当然不能小于0,否则无法解释;那它是否可以等于0? 测试发现是可以的。

当预热限流的方法的第二个参数是0的时候,预热的效果就消失,也就是没有了预热。这就变成了一个特殊的情况, 也就是也非突发、既非预热。SmoothWarmingUp的图形变成了一个点! 尽管如此,虽然此时预热时间为0,那么它是否就没有了任何的缓存许可的功能? 非也!

通过上文的分析,加上测试,发现SmoothWarmingUp 并不会积累permits。 就是说,使用的时候立即达到最高速度,不使用的时候立即冷却,此时速度呢? 其实冷却的时候速度都也不重要,实际上是无穷大,但是没有意义,因为加速的过程为0, 所以也可以把冷却的速度理解为0(更好理解)。就是说,基本上SmoothWarmingUp 只会使用2个速度:使用时max,不使用时0;

RateLimiter r = RateLimiter.create(2, 0, TimeUnit.MILLISECONDS);
漏桶,因为它本质上是令牌桶上做了修改。SmoothWarmingUp

SmoothWarmingUp 到底是漏桶还是令牌桶

其实上文说错了。答案是明确的,SmoothWarmingUp 应该算是漏桶,一般情况下,也就是当预热时间不为0的时候。我们知道SmoothWarmingUp有一个冷却的过程,而这个冷却的过程,就是可以理解为漏桶的令牌漏出的过程!但是呢, 正常理解的漏桶,如果桶内令牌足够,应该是可以直接获取而不用等待的(不管是否已经开始了冷却)。而我们知道SmoothWarmingUp不是的,SmoothWarmingUp一旦开始了冷却,它就必须要至少需要等待一些些的预热时间,也就是说需要比稳定状态时更久的时间!!

而且,我们知道,SmoothWarmingUp的整个加热过程(包括预热和准稳定过程) 并不是一个直线,而是折线,这和我们想象中的理想的漏桶恐怕还是不一样。

测试观察

@org.junit.Test
    public void testSmoothWarmingWith0() {
        RateLimiter r = RateLimiter.create(2, 0, TimeUnit.MILLISECONDS);
        while (true)
        {
            System.out.println("get 2 tokens: " + r.acquire(2) + "s");// 2个需要1s,但许可获取时间由下一次获取承担; 循环的首次,此行等待时间为0,第二次以后的等待时间为⑤ 行的许可获取时间,即0.5s
            try {
                Thread.sleep(1500);// 休息1.5s,可以完全消耗上行的2个许可,并剩出来0.5s,但是因为 预热/冷却时间为0,所以这个0.5s 其实是完全白白的流逝了..
            } catch (Exception e) {
            }
            System.out.println("get 3 tokens: " + r.acquire(3) + "s");// 因为 预热/冷却时间为0,前面的许可已经完全被补偿,所以此处3个需要1.5s,但时间地点时间为0,许可获取时间由下一次获取承担
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");// 上一个方法的实际等待时间为0,因为它由此方法承担—— 此方法实际等待时间为上一个方法的3个许可获取时间,即1.5s
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");// 此方法实际等待时间为上一个方法的1个许可获取时间,即0.5s
            System.out.println("get 1 tokens: " + r.acquire(1) + "s");// ⑤ 此方法实际等待时间为上一个方法的1个许可获取时间,即0.5s
            System.out.println("end");
        }
    }

观察日志打印,发现符合预期。

get 2 tokens: 0.0s
get 3 tokens: 0.0s
get 1 tokens: 1.499681s
get 1 tokens: 0.494564s
get 1 tokens: 0.498945s
end
get 2 tokens: 0.499013s
get 3 tokens: 0.0s
get 1 tokens: 1.499839s
get 1 tokens: 0.499698s
get 1 tokens: 0.499081s
end
get 2 tokens: 0.499659s
get 3 tokens: 0.0s
get 1 tokens: 1.499842s
get 1 tokens: 0.498895s
get 1 tokens: 0.499479s

SmoothWarmingUp 整个加热过程是不是可以为一条直线?

理论上来讲,SmoothWarmingUp 的斜率变成0,那么整个加热过程的就是一天直线。但是其实这个在SmoothWarmingUp 中是不可能发生的。 因为SmoothWarmingUp 的一个假设是 预热时间 = 冷却时间。如果斜率变成了0,那么预热时间 = 冷却时间 就不可能满足,除非预热时间变成了0,这就又回到了我们开始讨论的情况,也就是变成了一个点。

上一篇:Aop结合Guava实现的接口的限流保护(单机版)


下一篇:Laravel RateLimiter的使用