第四部分-并发编程案例分析1:限流Guava RateLimiter

1.并发案例,限流Guava RateLimiter

Guava RateLimiter 如何解决高并发的限流问题

guava中的工具类RateLimiter

2.简单实用RateLimiter

假设已有线程池,每秒只能处理两个任务,提交任务过快,可能导致系统不稳定,用到限流

创建一个2个请求/秒的限流器。每秒最多允许2个请求通过限流器,也就是1个请求500ms

伪代码


//限流器流速:2个请求/秒
RateLimiter limiter = 
  RateLimiter.create(2.0);
//执行任务的线程池
ExecutorService es = Executors
  .newFixedThreadPool(1);
//记录上一次执行时间
prev = System.nanoTime();
//测试执行20次
for (int i=0; i<20; i++){
  //限流器限流
  limiter.acquire();
  //提交任务异步执行
  es.execute(()->{
    long cur=System.nanoTime();
    //打印时间间隔:毫秒
    System.out.println(
      (cur-prev)/1000_000);
    prev = cur;
  });
}

输出结果:
...
500
499
499
500
499

3.限流算法:令牌桶

Guava的RateLimiter使用的是令牌桶算法,核心就是通过限流器的话,必须拿到令牌。
我们限制发放令牌的速率,就能控制流速。

令牌桶算法说明:
1.令牌以固定的速率添加到令牌桶中,假设速率是2/秒,则令牌每1/2秒会添加一个。
2.假设令牌桶容量是b,如果令牌桶已满,新的令牌会被丢弃。
3.请求能通过限流的前提是令牌桶中有令牌

令牌桶容量b是burst的简写,允许的最大突发流量。比如b=10,令牌桶中令牌已满,则限流器允许10个请求同时通过限流器,当然只是突发流量,这10个请求带走10个令牌,后续流量只能按照速率2/秒通过限流器

原理知道了?如何实现呢?
最简单的生产者,消费者模式就可以支持。生产者线程定时向阻塞队列中添加令牌,试图通过限流器的线程则作为消费者线程,只有从阻塞队列中取到令牌,才会运行通过限流器

有什么问题没?
并发量不大时,没什么问题。但高并发场景,系统压力到极限,定时器的精度会误差非常大。定时器本身会创建调度线程,对系统性能产生影响

4.Guava的Limiter是如何基于令牌桶算法实现限流器呢

核心:记录并动态计算下一令牌发放的时间

假设桶容量b=1,限流速率r=1/秒,如果桶中没有令牌,下一个令牌发送时间第三妙,第二秒有一个线程T1请求令牌,如何处理?
image

显然需要等待1秒,因未等1秒后就能拿到令牌。此时令牌产生时间往后顺延1秒,第三秒发的令牌被线程T1预占了
image

在T1预占了第三秒的令牌后,又有线程T2请求令牌
image
下一个令牌产生时间是第4秒,T2需要等待2秒才能获取令牌,T2预占了第4秒的令牌,令牌产生时间顺延1秒,需要第5秒再产生令牌
image

T1和T2都是在下一令牌产生时间之前请求令牌

如果线程T3在下一令牌产生时间(第5秒)之后,请求令牌如何呢
image
线程T3可以直接拿到令牌,无需等待。因为brust=1,所以第6,7秒产生的令牌就直接丢弃了。
image

5.guava的限流逻辑伪代码化



class SimpleLimiter {
  //当前令牌桶中的令牌数量
  long storedPermits = 0;
  //令牌桶的容量
  long maxPermits = 3;
  //下一令牌产生时间
  long next = System.nanoTime();
  //发放令牌间隔:纳秒
  long interval = 1000_000_000;
  
  //请求时间在下一令牌产生时间之后,则
  // 1.重新计算令牌桶中的令牌数
  // 2.将下一个令牌发放时间重置为当前时间
  void resync(long now) {
    if (now > next) {
      //新产生的令牌数
      long newPermits=(now-next)/interval;
      //新令牌增加到令牌桶
      storedPermits=min(maxPermits, 
        storedPermits + newPermits);
      //将下一个令牌发放时间重置为当前时间
      next = now;
    }
  }
  //预占令牌,返回能够获取令牌的时间
  synchronized long reserve(long now){
    resync(now);
    //能够获取令牌的时间
    long at = next;
    //令牌桶中能提供的令牌
    long fb=min(1, storedPermits);
    //令牌净需求:首先减掉令牌桶中的令牌
    long nr = 1 - fb;
    //重新计算下一令牌产生时间
    next = next + nr*interval;
    //重新计算令牌桶中的令牌
    this.storedPermits -= fb;
    return at;
  }
  //申请令牌
  void acquire() {
    //申请令牌时的时间
    long now = System.nanoTime();
    //预占令牌
    long at=reserve(now);
    long waitTime=max(at-now, 0);
    //按照条件等待
    if(waitTime > 0) {
      try {
        TimeUnit.NANOSECONDS
          .sleep(waitTime);
      }catch(InterruptedException e){
        e.printStackTrace();
      }
    }
  }
}

6.总结

令牌桶:定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,才能通过限流
漏桶算法:请求像水一样注入漏桶,漏桶会按照一定的速率自动将水滤掉,只有漏桶留能注水的时候,请求才能通过限流器

posted @ 2021-06-17 16:12  SpecialSpeculator  阅读(214)  评论(0编辑  收藏  举报