令牌桶算法详解

令牌桶算法详解_第1张图片

RateLimiter 有两个实现类:SmoothBursty 和 SmoothWarmingUp,其都是令牌桶算法的变种实现,区别在于 SmoothBursty 加令牌的速度是恒定的,而 SmoothWarmingUp 会有个预热期,在预热期内加令牌的速度是慢慢增加的,直到达到固定速度为止

RateLimiter 是用来控制访问资源的速率(rate)的,它强调的是控制速率。比如控制每秒只能有 100 个请求通过,比如允许每秒发送 1MB 的数据。 它的构造方法指定一个 permitsPerSecond 参数,代表每秒钟产生多少个 permits,这就是我们的速率。

RateLimiter 允许预占未来的令牌,比如,每秒产生 5 个 permits,我们可以单次请求 100 个,这样,紧接着的下一个请求需要等待大概 20 秒才能获取到 permits。

主要是根据 synchronized mutex()双锁 实现访问的限制, synchronized(this)和 synchronized(mutex) 都是对象锁, 同一时间每个实例都保证只能有一个实例能访问块中资源.


迸发平滑式


  private static final int THREAD_COUNT = 30;
    private static ExecutorService threadPool = Executors
            .newFixedThreadPool(THREAD_COUNT);

 public static void main(String[] args) {  RateLimiter limiter = RateLimiter.create(5);  for (int i = 0; i < 10000; i++) {  threadPool.execute(() -> {  try {  double acquire = limiter.acquire();  System.out.println(acquire);  Thread.sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }   });  }  }   

所谓的 ratelimter 真正的操作都是在 SmoothRateLimiter 抽象类中,又以下几个参数:

  • double storedPermits; 当前存储的许可证
  • double maxPermits; 最大允许缓存的 permits 数量,也就是 storedPermits 能达到的最大值
  • double stableIntervalMicros; 每隔多少秒产生一个 permit 例如例子中的 RateLimiter.create(5),代表每秒产生 5 个,200ms 产生一个
  • private long nextFreeTicketMicros = 0L; 下一次获取请求的时间(时间戳)

从上面就可以简单想到一些逻辑,根据请求,判断存储的 storedpermit 是否足够,如果不够,预消费。把下一次请求时间增加,也就是说要变大 nextFreeTicketMicros 时间。

先说创建:Ratelimter.create:

//创建默认为1s ,每秒产生的令牌数自定义
public static RateLimiter create(double permitsPerSecond) {
  create(permitsPerSecond, 1.0);
  }
//创建 最大迸发秒数自定义 产生令牌自定义
 public static RateLimiter create(double permitsPerSecond, double maxBurstSeconds) {  return create(SleepingStopwatch.createFromSystemTimer(), permitsPerSecond, maxBurstSeconds);  }  //真正的创建方法 SleepingStopwatch 代表休眠时间  static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond, double maxBurstSeconds) {  //初始化生成SmoothBursty 平滑突发式的  RateLimiter rateLimiter = new SmoothBursty(stopwatch, maxBurstSeconds);  rateLimiter.setRate(permitsPerSecond);  return rateLimiter;  } 

看一下 rateLimiter.setRate(permitsPerSecond)实现,主要是做一个同步。设置下一次请求的时间,以及设置最大许可和剩余许可,具体代码如下:


 public final void setRate(double permitsPerSecond) {
 //判断每秒许可是否符规范
    checkArgument(
        permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
 //设定互斥  synchronized (mutex()) {  doSetRate(permitsPerSecond, stopwatch.readMicros());  }  }   @Override  final void doSetRate(double permitsPerSecond, long nowMicros) {  //同步  resync(nowMicros);  // 每隔多少秒产生一个permit  double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;  this.stableIntervalMicros = stableIntervalMicros;  //设置速率  doSetRate(permitsPerSecond, stableIntervalMicros);  }   //同步 void resync(long nowMicros) {  // 判断当前时间 和下一次执行秒数时间戳 比nextFreeTicketMicros大,说明上一个请求欠的令牌已经补充好了,本次请求不用等待  if (nowMicros > nextFreeTicketMicros) {  //更新桶中的令牌,不能超过maxPermits  storedPermits = min(maxPermits,  storedPermits  + (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());  //当前时间赋值给本次时间  nextFreeTicketMicros = nowMicros;  }  }   //设置最大许可以及剩余许可  @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 {  //初始化时候 为0  storedPermits = (oldMaxPermits == 0.0)  ? 0.0 // initial state  : storedPermits * maxPermits / oldMaxPermits;  }  } 

说完了创建,下面将要说我们最重要的使用了,acquire(),acquire 有预消费能力,前一个请求需求的令牌数超过了当前持有的令牌数,那么这个请求可以预消费,直接取走自己所需要的的量,而这个量会由后一个请求来偿还(通过等待时间来偿还)

//默认每次获取一个许可
public double acquire() {
    return 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());  }  }   final long reserveAndGetWaitLength(int permits, long nowMicros) {  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);   //判断是否有预消费的时间  return max(momentAvailable - nowMicros, 0);  }   //设置下一次执行时间 以及剩余许可 返回下一次获取资源时间  @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;  return returnValue;  }  

还有一个 tryacquire(),这个的主要功能就是,去尝试获取下一次许可,如果拿不到,则返回 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 {  //调用acquire 的方法。获取下一次时间 以及减少剩余许可 返回时间  microsToWait = reserveAndGetWaitLength(permits, nowMicros);  }  }  stopwatch.sleepMicrosUninterruptibly(microsToWait);  return true;  }  //判断下一次请求时间减去等待时间 是否比当前时间小  private boolean canAcquire(long nowMicros, long timeoutMicros) {  return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;  }  //acquire 也会调用  final long reserveAndGetWaitLength(int permits, long nowMicros) {  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);  return max(momentAvailable - nowMicros, 0);  } 

大体上的用法就是这样了。总结一下,

  • 一个请求过来,如果当前令牌桶中有令牌,那么直接获取执行,不需要等待。具体操作就式更新下一次请求时间,更新剩余的令牌
  • 如果一个请求过来,当前桶里令牌不够了,但是会依旧放行通过 在下一次请求来偿还,具体操作就式更新下一次请求时间,更新剩余的令牌
  • 如果令牌桶中已经没有,则会进行等待

SmoothWarmingUp 预热平滑类型

  private static final int THREAD_COUNT = 20;

    private static ExecutorService threadPool = Executors
            .newFixedThreadPool(THREAD_COUNT);

 public static void main(String[] args) {  RateLimiter limiter = RateLimiter.create(5, 5,TimeUnit.SECONDS);  long l = System.currentTimeMillis();  for (int i = 0; i < 10000; i++) {  threadPool.execute(() -> {  try {  double acquire = limiter.acquire();  System.out.println(acquire + "test:{}" + (System.currentTimeMillis() - l));  Thread.sleep(1000);  } catch (InterruptedException e) {  e.printStackTrace();  }   });  }  } 

主要有几个参数:

  • private final long warmupPeriodMicros; //冷静持续时间
  • private double slope; //斜率
  • private double thresholdPermits; //门槛
  • private double coldFactor; //冷启动因子 默认 3.0

说一下创建:创建同 smoothup 类似,唯一不同的就是下面这个方法:

 @Override
    void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
    //最大许可
      double oldMaxPermits = maxPermits;
//冷启动间隔 通过生成每个许可的时间乘以冷启动因子3.0
 double coldIntervalMicros = stableIntervalMicros * coldFactor;  //门槛许可数 0.5乘以冷启动时间秒数 再除以每秒产生许可的时间秒数 例如例子 冷启动时间 5000ms 每秒产生许可时间200ms 门槛许可数12.5  thresholdPermits = 0.5 * warmupPeriodMicros / stableIntervalMicros;  //最大许可数 门槛许可数加上 2.0乘以冷启动时间秒数除以每产生一个许可的时间加上冷启动时间间隔 例如例子 12.5 + 2.0*5000/(200+60)=51  maxPermits = thresholdPermits  + 2.0 * warmupPeriodMicros / (stableIntervalMicros + coldIntervalMicros);   //斜率计算 冷启动间隔减去每个许可的生成时间 除以 最大许可数减去门槛许可数 例如例子 (60-200)/(51-12.5)= -3.7  slope = (coldIntervalMicros - stableIntervalMicros) / (maxPermits - thresholdPermits);   //初始化剩余许可数  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 // initial state is cold  : storedPermits * maxPermits / oldMaxPermits;  }  }  

acquire 与 tryacquire 也是类似,唯一不同的是以下这个方法:

@Override
    long storedPermitsToWaitTime(double storedPermits, double permitsToTake) {
    //获取高于阀值的许可数量 剩余许可 减去 门槛许可
      double availablePermitsAboveThreshold = storedPermits - thresholdPermits;
      long micros = 0;
 // 差值大于0 计算大于门槛许可的时间  if (availablePermitsAboveThreshold > 0.0) {  //判断可以消费的许可 和 高于门槛的许可数量 为了获取 允许超过门槛的数量  double permitsAboveThresholdToTake = min(availablePermitsAboveThreshold, permitsToTake);   //获取等待时间  micros = (long) (permitsAboveThresholdToTake  * (permitsToTime(availablePermitsAboveThreshold)  + permitsToTime(availablePermitsAboveThreshold - permitsAboveThresholdToTake)) / 2.0);  permitsToTake -= permitsAboveThresholdToTake;  }   // 计算平滑期等待时间 存储的消费许可 乘以 每产生一个许可的时间  micros += (stableIntervalMicros * permitsToTake);  return micros;  }   //根据许可 和斜率计算 加上 每产生一个许可的秒数  private double permitsToTime(double permits) {  return stableIntervalMicros + permits * slope;  }  

基本上以上就是全部的代码解释。感谢

我等采石之人,当心怀大教堂之愿景! 欢迎关注我的公众号!!搬砖小金

你可能感兴趣的:(java)