令牌桶算法谷歌实现

令牌桶算法


小序


The token bucketis an algorithm used in packet-switched and telecommunications networks. It can be used to check that data transmissions, in the form of packets, conform to defined limits on bandwidth) and burstiness (a measure of the unevenness or variations in the traffic flow). 
令牌桶是一种用于分组交换和电信网络的算法。它可用于检查数据包形式的数据传输是否符合定义的带宽和突发性限制(流量不均匀或变化的度量)

谷歌实现


添加maven依赖

<dependency>
    <groupId>com.google.guavagroupId>
    <artifactId>guavaartifactId>
    <version>30.1-jreversion>
dependency>

测试用例

import com.google.common.util.concurrent.ListeningExecutorService;
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.RateLimiter;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.Executors;

public class LimitTest {

	public static void main(String[] args) {
		ListeningExecutorService executorService = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(100));
         
		RateLimiter limiter = RateLimiter.create(1);
		for (int i = 1; i < 50; i++) {
			Double acquire = limiter.acquire(i);
			executorService.submit(new PrintTask(String.format("get token,token:%s consumer:%s, taskId:%s",acquire, acquire, i)));
		}
	}
	static class PrintTask implements Runnable {
		String out;
		public PrintTask(String out) {
			this.out = out;
		}
		@Override
		public void run() {
			SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
			System.out.println(sdf.format(new Date()) + " -> " + Thread.currentThread().getName() + out);
		}
	}
}

核心方法

//实例化处理器 每秒允许一个请求通过
RateLimiter limiter = RateLimiter.create(1); 
//获取令牌
Double acquire = limiter.acquire(i);

源码

/**
创建具有指定稳定吞吐量的RateLimiter ,以“每秒允许数”(通常称为QPS ,每秒查询数)的形式给出。
返回的RateLimiter确保在任何给定的秒内平均发出不超过permitsPerSecond ,持续的请求在每一秒内顺利传播。 当传入请求速率超过permitsPerSecond时,速率限制器将每(1.0 / permitsPerSecond)秒释放一个许可。 当速率限制器未使用时,将允许最多permitsPerSecond许可的突发,随后的请求以permitsPerSecond的稳定速率平滑限制。

参数:
permitPerSecond – 返回的RateLimiter的速率,以每秒可用的许可数量衡量
抛出:
IllegalArgumentException – 如果permitsPerSecond为负数或零
推断注释:
@org.jetbrains.annotations.NotNull @org.jetbrains.annotations.Contract("_->new")

*/
public static RateLimiter create(double permitsPerSecond) {
    /*
     * The default RateLimiter configuration can save the unused permits of up to one second. This
     * is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, and 4 threads,
     * all calling acquire() at these moments:
     *
     * T0 at 0 seconds
     * T1 at 1.05 seconds
     * T2 at 2 seconds
     * T3 at 3 seconds
     *
     * Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also
     * have to sleep till 3.05 seconds.
    默认的 RateLimiter 配置最多可以保存一秒钟的未使用许可。这是为了避免在以下情况下出现不必要的停顿: 1qps 的 RateLimiter 和 4 个线程,在这些时刻都调用 acquire(): T0 在 0 秒 T1 在 1.05 秒 T2 在 2 秒 T3 在 3 秒 由于轻微延迟在 T1 中,T2 必须睡到 2.05 秒,T3 也必须睡到 3.05 秒。
     */
    return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
  }

从源码中我们可以看到,除了传入的速率值,还需要初始化一个SleepingStopwatch.createFromSystemTimer()定时处理器,我们看看这个里面到底有什么。

SleepingStopwatch

 public static SleepingStopwatch createFromSystemTimer() {
      return new SleepingStopwatch() {
		//初始化Stopwatch 并启动
        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);
          }
        }
      };
    }
//stopwatch.elapsed(MICROSECONDS) 获取当前时间 
 public long elapsed(TimeUnit desiredUnit) {
    //获取时间并转换为纳秒
    return desiredUnit.convert(elapsedNanos(), NANOSECONDS);
 }
//elapsedNanos() 获取时间
private long elapsedNanos() {
    // 如果当前处于运行状态 则从ticker中读取时间-开始计时的时间+elapsedNanos时间, 否则返回elapsedNanos 过去时间
    return isRunning ? ticker.read() - startTick + elapsedNanos : elapsedNanos;
}
//追踪ticker 初始化对象中Stopwatch.createStarted();
  Stopwatch() {
    this.ticker = Ticker.systemTicker();
  }
// Ticker 源码 其实就是去读取系统的纳秒时间
public abstract class Ticker {
  protected Ticker() {}
  public abstract long read();
  public static Ticker systemTicker() {
    return SYSTEM_TICKER;
  }
  private static final Ticker SYSTEM_TICKER =
      new Ticker() {
        @Override
        public long read() {
          return Platform.systemNanoTime();
        }
      };
}
//Platform.systemNanoTime 读取系统时间
 static long systemNanoTime() {
    return System.nanoTime();
  }

可以看到createFromSystemTimer 方法返回了一个只有读取秒表时间和睡眠的方法的实例。可以看到这个方法主要是获取系统的纳秒时间

我们继续跟踪主线代码create方法

  @VisibleForTesting
  static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
    RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
    rateLimiter.setRate(permitsPerSecond);
    return rateLimiter;
  }
----->创建对象
  SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
      super(stopwatch);
      this.maxBurstSeconds = maxBurstSeconds;
    }

到这里我们可以看到,ReteLimiter真正的实现是SmoothBursty,然后设置下流速,就返回了。到此对象的实例算是完成了,我们这条线暂时到这。接下来看看acquire获取令牌的方法。

acquire 获取令牌

/**
从此RateLimiter获取给定数量的许可,阻塞直到可以授予请求。 告知睡眠时间(如果有)
*/
  @CanIgnoreReturnValue
  public double acquire(int permits) {
    //获取需要等待的时间
    long microsToWait = reserve(permits);
    //阻塞等待
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
	//返回一个值,其实这个值是多少没什么太大作用
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

可以看到,申请token的方法中,主要实现三个功能:

  1. 计算等待时间
  2. 阻塞等待
  3. 返回一个值

可以看到核心是reserve计算等待时间这个方法,我们可以继续跟进去。

/**
   * Reserves the given number of permits from this {@code RateLimiter} for future use, returning
   * the number of microseconds until the reservation can be consumed.
   * 从此RateLimiter保留给定数量的许可以供将来使用,返回微秒数,直到可以使用保留
   * @return time in microseconds to wait until the resource can be acquired, never negative
   */
  final long reserve(int permits) {
	//校验permits是否大于0,负责抛出异常
    checkPermits(permits);
     // mutex 设置上锁点
    synchronized (mutex()) {
      //核心 保留并计算等待时间长度
      return reserveAndGetWaitLength(permits, stopwatch.readMicros());
    }
  }

可以看到reserveAndGetWaitLength才是我们算法的核心所在,这个方法需要的参数,除了我们传入的速率之外,还有一个从stopwatch中读取的秒表,我们继续跟踪查看:

/**
   * Reserves next ticket and returns the wait time that the caller must wait for.
   * 保留下一张票并返回调用者必须等待的等待时间
   * @return the required wait time, never negative
   */
  final long reserveAndGetWaitLength(int permits, long nowMicros) {
    //获取最早可用的时间
    long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
    //返回需要等待时间值
    return max(momentAvailable - nowMicros, 0);
  }

这里reserveEarliestAvailable 就是我们的主要目标了,怼吧。

  @Override
  final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
    //1.重新同步时间
    resync(nowMicros);
    //2.将下次可以获取获取令牌的时间作为返回值
    long returnValue = nextFreeTicketMicros;
    //3.存储消费许可, 取要求token个数和已有token个数的最小值
    /*
    3.存储消费许可, 取要求token个数和已有token个数的最小值
    4.去除要求的token之外,剩余token个数
    这两步要合起来看
    需求toke个数: requiredPermits
    已有token: storedPermits
    如果requiredPermits > storedPermits,则需要新令牌的个数为
     requiredPermits - storedPermits
    如果requiredPermits <= storedPermits,则需要新令牌的个数为
     requiredPermits - storedPermits
     这两行代码写的真是厉害
    */  
    double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
    // 4.去除要求的token之外,需要重新刷的token个数
    double freshPermits = requiredPermits - storedPermitsToSpend;
    //5.令牌消费之后,剩余令牌可以使用的时间
    long waitMicros =
        storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
            + (long) (freshPermits * stableIntervalMicros);
	//6.刷新下个允许获取令牌的时间  
    this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
    //7.更新消费许可
    this.storedPermits -= storedPermitsToSpend;
    return returnValue;
  }

//-----------------------------resync 代码------------------------------
void resync(long nowMicros) {
    // if nextFreeTicket is in the past, resync to now
    //如果允许获取下一个令牌的时间小于当前时间,则更新nextFreeTicketMicros为当前时间
    if (nowMicros > nextFreeTicketMicros) {
      //计算当前时间和next的时间差内可以产生的token个数
      double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
      //更新存储的可用token的个数
      storedPermits = min(maxPermits, storedPermits + newPermits);
      //更新允许可以获取下一个token的时间
      nextFreeTicketMicros = nowMicros;
    }
  }

可以看到这里就是我们的索要追踪的核心所在了,

  1. 重新同步时间的逻辑

    1.2.1. 计算当前时间和上一个next的时间差内可以产生的token个数

    1.2.2. 更新当前可存储的token个数

    1.2.3. 更新允许获取下一个token的时间next

  2. 将下次可以获取获取令牌的时间作为返回值

  3. 存储消费许可, 取要求token个数和已有token个数的最小值,具体原因可以看代码注释

  4. 去除要求的token之外,需要重新刷的token个数

  5. 令牌消费之后,剩余令牌可以使用的时间

  6. 计算下个允许获取令牌的时间

  7. 更新剩下的消费许可

获取到需要等待的时间之后,我们就可以返回方法处了

/**
从此RateLimiter获取给定数量的许可,阻塞直到可以授予请求。 告知睡眠时间(如果有)
*/
  @CanIgnoreReturnValue
  public double acquire(int permits) {
    //获取需要等待的时间
    long microsToWait = reserve(permits);
    //阻塞等待
    stopwatch.sleepMicrosUninterruptibly(microsToWait);
	//返回一个值,其实这个值是多少没什么太大作用
    return 1.0 * microsToWait / SECONDS.toMicros(1L);
  }

这个拿到需要阻塞的时间之后,直接阻塞,时间到了,返回值。 到此算是整个过程处理完毕。

总结

从源码看这个令牌桶算法是先拿的人是无限制,拿到之后,下次可以获取令牌的时间,就要等到这些令牌的制造所需时间过后才能重新获取。

你可能感兴趣的:(算法,java,guava,令牌桶,限流)