JAVA实现简单限流器(下)

163 阅读4分钟

JAVA实现简单限流器(下)

Guava 如何实现令牌桶算法

既然上篇提到采用定时任务有可能产生误差,那么Guava还能如何去实现呢?

Guava采用巧妙的记录并动态计算下一令牌发放的时间,来实现令牌桶算法,如下说明。

假设存在令牌桶的最大值1,流速为1个/秒,如果在当前T1时刻令牌桶没有令牌,那么请求正好在T1来临,下一次令牌发送时间应该是第3秒的位置

image-20220321235126307

由于在线程T1到达没有令牌,那么需要等待第3秒产生令牌后才能马上获取,那下一次产生令牌的时间就应该往后面移动一秒,也就是第4秒产生。

image-20220321235419149

假设这时T1刚刚获取令牌,T2马上请求令牌那么下一秒令牌产生的时间是多少呢?应该是第5秒

image-20220321235816514

上诉情况都是线程请求在令牌产生之前的,如果线程请求在令牌产生之后呢,如发生在线程T1请求完毕,假设是第7秒的位置,这时第5秒、第6秒和第7秒都会产生令牌,并且中间无线程获取,同时桶的最大值还是1,所以令牌桶中会保存一个令牌,线程T3可以直接获取。

image-20220322000500178

令牌桶算法实现1

从上面的分析可以得出,只要记录下一次的令牌产生时间并且动态的更新就能达到限流的效果,那么将上述分析语义化如下,注意这里实现的是令牌桶为1的情况,并不适合所有情况。

 public class SimpleLimiter1 {
     /**
      * 下一次令牌产生的时间(单位纳秒)
      */
     private long next = System.nanoTime();
 ​
     /**
      * 一秒
      * 1毫秒 = 1000微秒  1微秒 = 1000纳秒
      */
     private long speed = 1000_000_000;
 ​
     /**
      * 计算下一次令牌产生的时间(单位纳秒)
      * @param now
      * @return
      */
     private synchronized long reserve(long now){
         // 请求时间在下一次令牌产生之后 简单处理
         if (now > next){
             next = now;
         }
         // 能够获取令牌的时间
         long at = next;
         // 下一次令牌产生的时间
         next += speed;
         return at;
     }
 ​
     /**
      * 申请令牌
      */
     public void acquire(){
         long now = System.nanoTime();
         // 令牌产生时间
         long reserve = reserve(now);
 ​
         long waitTime = reserve-now;
 ​
         if (waitTime>0){
             try {
                 TimeUnit.NANOSECONDS.sleep(waitTime);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 }

测试方法

 public static void main(String[] args) {
     SimpleLimiter2 simpleLimiter2 = new SimpleLimiter2();
 ​
     ExecutorService executorService = Executors.newFixedThreadPool(10);
     AtomicLong start = new AtomicLong(System.nanoTime());
     for (int i = 0; i <20 ; i++) {
         simpleLimiter2.acquire();
         executorService.execute(()->{
             long end = System.nanoTime();
             try {
                 Thread.sleep(1000);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
             System.out.println(Thread.currentThread().getName()+"==="+(end- start.get())/1000_1000);
             start.set(end);
         });
     }
 }

令牌桶算法实现2

通过上述代码我们就能得到一个令牌桶的容量为1的限流算法,但远远不够,如果令牌桶的容量为n呢?应该如何处理。

  • 如果线程在计算下一个令牌产生的时间时,需要重新计算令牌桶的令牌数也就是resync方法。
  • reserve方法则需要修改,所有的令牌将从令牌桶中获取。
 public class SimpleLimiter2 {
     /**
      * 当前令牌数量
      */
     private long tokenNum = 0;
 ​
     /**
      * 令牌桶最大容量
      */
     private long maxTokenNum = 3;
 ​
     /**
      * 下一次令牌产生的时间(单位纳秒)
      */
     private long next = System.nanoTime();
 ​
     /**
      * 一秒
      * 1毫秒 = 1000微秒  1微秒 = 1000纳秒
      */
     private long speed = 1000_000_000;
 ​
     /**
      * 请求时间在下一令牌产生时间之后
      * 1、重新计算令牌桶中的令牌数(当前的令牌总数)
      * 2、将下一令牌产生时间重置为当前时间
      * @param now
      */
     private void resync(long now){
         if (now > next){
             // 新产生的令牌数
             long newProduceNum = (now - next) / speed;
             // 当前令牌桶中的令牌数
             tokenNum = Math.min(maxTokenNum,newProduceNum+tokenNum);
             next = now;
         }
     }
 ​
     /**
      * 计算下一次令牌产生的时间(单位纳秒)
      * @param now
      * @return
      */
     private synchronized long reserve(long now){
         // 请求时间在下一次令牌产生之后 简单处理
         resync(now);
         // 能够获取令牌的时间
         long at = next;
         // 令牌桶能够提供的令牌数 只有两种情况
         // fb = 0 说明tokenNum=0 now <= next  令牌桶中没有令牌
         // fb = 1 说明tokenNum>1 now > next   令牌桶中有令牌
         long fb = Math.min(1,tokenNum);
         // nr=0那么已经消耗了一个令牌    nr=1 令牌桶中没有令牌还没消耗
         long nr = 1- fb;
         // 下一次令牌产生的时间
         next = next + nr*speed;
         // 重新计算令牌桶中的数量
         tokenNum -= fb;
         return at;
     }
 ​
     /**
      * 申请令牌
      */
     public void acquire(){
         long now = System.nanoTime();
         // 令牌产生时间
         long reserve = reserve(now);
 ​
         long waitTime = Math.max(reserve-now,0);
 ​
         if (waitTime>0){
             try {
                 TimeUnit.NANOSECONDS.sleep(waitTime);
             } catch (InterruptedException e) {
                 e.printStackTrace();
             }
         }
     }
 }