RateLimiter限流源码分析

455 阅读8分钟

       在设计高并发系统时候,限流是必不可少的一环。刚好最近项目中使用了限流功能,也上网看了些限流的方案,代码层有简单限流,滑动窗口限流,漏桶限流、令牌桶限流,当然限流器也分为单机版本和分布式版本。项目中使用了谷歌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有两个实现类:SmoothBurstySmoothWarmingUp,由于项目里使用的是前者,因此分析SmoothBursty

重要属性

permitsPerSecond是方法参数,可以计算出stableIntervalMicrosmaxPermits

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参数的创建方法