这是几年前写的旧文,此前发布Wordpress小站上,现在又重新整理。算是温故知新,后续会继续整理。如有错误望及时指出,在此感谢。

遇到什么问题?

1.接口服务被无序调用,导致服务响应慢,出现各种异常;

2.业务资源如数据库,避免被大量请求导致服务被击穿;

3.硬件资源如cpu等面对高并发情况下无法及时响应;

解决方法有哪些?

1.增加缓存机制;

2.对业务进行限流;

这次以限流为目标,使用Google开源的Guava-RateLimiter

3.对业务进行降级;

为什么要用RateLimiter?有没有替代方案?

当然有的,JDK自带的Concurrent包中的Semaphore也可以当作限流器来使用。

那为什么要用RateLimiter呢?

因为RateLimiter使用起来更方便,更简洁。

Semaphore只能控制并发总量,而RateLimiter不光可以控制并发总量,还能控制并发的速率

RateLimiter常用的方法:

1.acquire

返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;

可以一次调用获取多个令牌;

2.tryAcquire

尝试立即获取令牌,可以尝试获取多个;

返回结果表示是否成功获取到令牌;

3.实例化

工厂方法 RateLimiter.create(double permitsPerSecond)

默认返回的是SmoothBursty子类,一种每秒按照固定(permitsPerSecond)数量生成令牌;

场景演示

JDK:1.8

Maven

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>25.1-jre</version>
</dependency>

方法被无序调用(code_01)

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

public class GuavaRateLimiterTest1 {

   static RateLimiter rateLimiter = RateLimiter.create(2);

   public static void main(String[] args) {

      //RateLimiter常用的方法有:
      //1.acquire,返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;可以一次调用获取多个令牌;
      //2.tryAcquire,立即获取令牌,可以尝试获取多个;返回结果表示是否成功获取到令牌;

      //RateLimiter的默认构造器返回的是SmoothBursty,是一种每秒按照固定速率生成令牌
      //为了测试出效果,这里使用多个线程重复模拟请求同一个方法
      for (int i = 0; i < 10; i++) {
         print(i);
      }

//    第[0]次获取,当前时间:1649777733792,waitTime:0.0
//    第[1]次获取,当前时间:1649777734292,waitTime:0.498631
//    第[2]次获取,当前时间:1649777734791,waitTime:0.498059
//    第[3]次获取,当前时间:1649777735292,waitTime:0.499828
//    第[4]次获取,当前时间:1649777735791,waitTime:0.499297
//    第[5]次获取,当前时间:1649777736291,waitTime:0.499883
//    第[6]次获取,当前时间:1649777736791,waitTime:0.499408
//    第[7]次获取,当前时间:1649777737292,waitTime:0.499867
//    第[8]次获取,当前时间:1649777737791,waitTime:0.499272
//    第[9]次获取,当前时间:1649777738291,waitTime:0.499843

   }

   public static void print(int ct) {
      //acquire的返回值是拿到令牌所等待的时长,单位为秒
      double waitTime = rateLimiter.acquire();

      System.out.println("第[" + ct + "]次获取,当前时间:" + System.currentTimeMillis() + ",waitTime:" + waitTime);
   }

}

多线程并发抢占资源(code_02)

import com.google.common.base.Preconditions;
import com.google.common.util.concurrent.RateLimiter;

import java.util.concurrent.CountDownLatch;

public class GuavaRateLimiterTest2 {

   //限速器
   static RateLimiter rateLimiter = RateLimiter.create(8);
   //并发测试控制器
   static CountDownLatch latch = new CountDownLatch(1);

   public static void main(String[] args) throws InterruptedException {

      //RateLimiter的默认构造器返回的是SmoothBursty,是一种每秒按照固定速率生成令牌
      //RateLimiter常用的方法有:
      //1.acquire,返回一个令牌,会有等待的过程,返回值是等待的时长,单位为秒;可以一次调用获取多个令牌;
      //2.tryAcquire,立即获取令牌,可以尝试获取多个;返回结果表示是否成功获取到令牌;
      System.out.println("--------------------------");
      System.out.println("let's...");
      System.out.println("--------------------------");

      //为了测试出效果,这里使用多个线程重复模拟请求同一个方法
      //设置每秒生成8个令牌,当获取令牌失败时,会抛出异常,代表请求被拒绝
      for (int i = 0; i < 10; i++) {
         new Thread(() -> {
            try {
               System.out.println("thread:" + Thread.currentThread().getName() + ",ready...");
               latch.await();

               //Guava自带的测试工具进行翻车检查
               Preconditions.checkState(rateLimiter.tryAcquire(), "thread:" + Thread.currentThread().getName() + "令牌不足访问被拒绝...");

               //获取令牌成功,执行具体业务
               System.out.println("thread:" + Thread.currentThread().getName() + ",doing...");
            } catch (InterruptedException e) {
               e.printStackTrace();
            }
         }, "t" + i).start();
      }

      Thread.sleep(3000L);
      latch.countDown();
      System.out.println("--------------------------");
      System.out.println("go!");
      System.out.println("--------------------------");

//    --------------------------
//       let's...
//    --------------------------
//    thread:t0,ready...
//    thread:t2,ready...
//    thread:t1,ready...
//    thread:t3,ready...
//    thread:t5,ready...
//    thread:t4,ready...
//    thread:t6,ready...
//    thread:t8,ready...
//    thread:t9,ready...
//    thread:t7,ready...
//    --------------------------
//       go!
//    --------------------------
//    thread:t2,doing...
//    thread:t7,doing...
//    thread:t8,doing...
//    thread:t5,doing...
//    thread:t9,doing...
//    thread:t1,doing...
//    thread:t3,doing...
//    thread:t6,doing...
//    thread:t4,doing...
//    Exception in thread "t0" java.lang.IllegalStateException: thread:t0令牌不足访问被拒绝...
//    at com.google.common.base.Preconditions.checkState(Preconditions.java:507)
//    at com.xxx.guava.GuavaRateLimiterTest2.lambda$main$0(GuavaRateLimiterTest2.java:41)
//    at java.lang.Thread.run(Thread.java:748)

   }
}

模拟限速下载器(code_03)

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

public class GuavaRateLimiterTest3 {

   static int limit = 3;
   static RateLimiter limiter = RateLimiter.create(limit);

   public static void main(String[] args) {
      //利用RateLimiter定时生成令牌的特性,做一个速率控制器(限速器),模拟某盘的下载控制功能
      //当然实际的限速器要比这个复杂的多,这里只是开阔下思维,同时与JDK自带的Semaphore进行下比较
      //Semaphore用来控制并发总量,而RateLimiter用来控制并发速率

      String inputStr = "阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。";

      System.out.println("文件内容:" + inputStr);
      System.out.println("文件长度:" + inputStr.length());
      System.out.println("--------------------------");

      long startTime = System.currentTimeMillis();
      String downloadStr = download(inputStr);
      long stopTime = System.currentTimeMillis();

      System.out.println("下载开始时间:" + startTime);
      System.out.println("下载结束时间:" + stopTime);
      System.out.println("下载耗时(毫秒):" + (stopTime - startTime));
      System.out.println("下载的文件内容:" + downloadStr);
      System.out.println("下载的文件长度:" + downloadStr.length());
      System.out.println("文件内容是否相等:" + downloadStr.equals(inputStr));

//    文件内容:阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。
//    文件长度:79
//          --------------------------
//    下载开始时间:1649750529917
//    下载结束时间:1649750555917
//    下载耗时(毫秒):26000
//    下载的文件内容:阿富汗战争,即2001年阿富汗战争,是以 美国 为首的联军在2001年10月7日起对 “基地”组织 和塔利班的一场战争,该战争是美国对 9·11事件 的报复。
//    下载的文件长度:79
//    文件内容是否相等:true
   }

   public static String download(String inputStr) {

      StringBuffer sb = new StringBuffer();
      char[] inputChars = inputStr.toCharArray();
      final int str_length = inputStr.length();

      int start_index = 0;
      int len = limit;

      while (start_index < str_length) {
         limiter.acquire(limit);

         if ((start_index + limit) >= str_length) {
            len = str_length - start_index;
         }

         String s = new String(inputChars, start_index, len);
         //System.out.println("s:" + s + ",start_index:" + start_index + ",len:" + len);
         sb.append(inputChars, start_index, len);

         start_index += limit;
      }

      return sb.toString();
   }

}

总结

RateLimiter没有Release方法,不需要手动进行令牌的回收释放;

RateLimiter默认按照设定的参数,每秒固定生成令牌数量,不光可以简单控制并发总量,更重要能控制访问的速率,粒度更细;

RateLimiter的Acquire可以一次返回多个令牌,对业务的弹性来讲丰富了很多场景;