Day857.高性能限流器Guava RateLimiter -Java 并发编程实战

news2024/11/17 6:02:27

高性能限流器Guava RateLimiter

Hi,我是阿昌,今天学习记录的是关于高性能限流器Guava RateLimiter的内容。

Guava RateLimiter 是如何解决高并发场景下的限流问题的。

Guava 是 Google 开源的 Java 类库,提供了一个工具类 RateLimiter。先来看看 RateLimiter 的使用,对限流有个感官的印象。

假设有一个线程池,它每秒只能处理两个任务,如果提交的任务过快,可能导致系统不稳定,这个时候就需要用到限流。

在下面的示例代码中,创建了一个流速为 2 个请求 / 秒的限流器,这里的流速该怎么理解呢?

直观地看,2 个请求 / 秒指的是每秒最多允许 2 个请求通过限流器,其实在 Guava 中,流速还有更深一层的意思:是一种匀速的概念,2 个请求 / 秒等价于 1 个请求 /500 毫秒。

在向线程池提交任务之前,调用 acquire() 方法就能起到限流的作用。

通过示例代码的执行结果,任务提交到线程池的时间间隔基本上稳定在 500 毫秒。

//限流器流速: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


一、经典限流算法:令牌桶算法

Guava 的限流器使用上还是很简单的,那它是如何实现的呢?

Guava 采用的是令牌桶算法,其核心是要想通过限流器,必须拿到令牌

也就是说,只要能够限制发放令牌的速率,那么就能控制流速了。

令牌桶算法的详细描述如下:

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

这个算法中,限流的速率 r 还是比较容易理解的,但令牌桶的容量 b 该怎么理解呢?b 其实是 burst 的简写,意义是限流器允许的最大突发流量。

比如 b=10,而且令牌桶中的令牌已满,此时限流器允许 10 个请求同时通过限流器,当然只是突发流量而已,这 10 个请求会带走 10 个令牌,所以后续的流量只能按照速率 r 通过限流器。

令牌桶这个算法,如何用 Java 实现呢?很可能直觉会告诉你生产者 - 消费者模式

一个生产者线程定时向阻塞队列中添加令牌,而试图通过限流器的线程则作为消费者线程,只有从阻塞队列中获取到令牌,才允许通过限流器。这个算法看上去非常完美,而且实现起来非常简单,如果并发量不大,这个实现并没有什么问题。可实际情况却是使用限流的场景大部分都是高并发场景,而且系统压力已经临近极限了,此时这个实现就有问题了。

问题就出在定时器上,在高并发场景下,当系统压力已经临近极限的时候,定时器的精度误差会非常大,同时定时器本身会创建调度线程,也会对系统的性能产生影响。

那还有什么好的实现方式呢?当然有,Guava 的实现就没有使用定时器,下面就来看看它是如何实现的。


二、Guava 如何实现令牌桶算法

Guava 实现令牌桶算法,用了一个很简单的办法,其关键是记录并动态计算下一令牌发放的时间。下面以一个最简单的场景来介绍该算法的执行过程。

假设令牌桶的容量为 b=1,限流速率 r = 1 个请求 / 秒,如下图所示,如果当前令牌桶中没有令牌,下一个令牌的发放时间是在第 3 秒,而在第 2 秒的时候有一个线程 T1 请求令牌,此时该如何处理呢?

线程 T1 请求令牌示意图
对于这个请求令牌的线程而言,很显然需要等待 1 秒,因为 1 秒以后(第 3 秒)它就能拿到令牌了。

此时需要注意的是,下一个令牌发放的时间也要增加 1 秒,为什么呢?

因为第 3 秒发放的令牌已经被线程 T1 预占了。处理之后如下图所示。

线程 T1 请求结束示意图

假设 T1 在预占了第 3 秒的令牌之后,马上又有一个线程 T2 请求令牌,如下图所示。

线程 T2 请求令牌示意图
很显然,由于下一个令牌产生的时间是第 4 秒,所以线程 T2 要等待两秒的时间,才能获取到令牌,同时由于 T2 预占了第 4 秒的令牌,所以下一令牌产生时间还要增加 1 秒,完全处理之后,如下图所示。

线程 T2 请求结束示意图
上面线程 T1、T2 都是在下一令牌产生时间之前请求令牌,如果线程在下一令牌产生时间之后请求令牌会如何呢?

假设在线程 T1 请求令牌之后的 5 秒,也就是第 7 秒,线程 T3 请求令牌,如下图所示。
线程 T3 请求令牌示意图
由于在第 5 秒已经产生了一个令牌,所以此时线程 T3 可以直接拿到令牌,而无需等待。

在第 7 秒,实际上限流器能够产生 3 个令牌,第 5、6、7 秒各产生一个令牌。

由于假设令牌桶的容量是 1,所以第 6、7 秒产生的令牌就丢弃了,其实等价地也可以认为是保留的第 7 秒的令牌,丢弃的第 5、6 秒的令牌,也就是说第 7 秒的令牌被线程 T3 占有了,于是下一令牌的的产生时间应该是第 8 秒,如下图所示。

线程 T3 请求结束示意图

通过上面简要地分析,会发现,只需要记录一个下一令牌产生的时间,并动态更新它,就能够轻松完成限流功能。可以将上面的这个算法代码化,示例代码如下所示,依然假设令牌桶的容量是 1。

关键是 reserve() 方法,这个方法会为请求令牌的线程预分配令牌,同时返回该线程能够获取令牌的时间。其实现逻辑就是上面提到的:

如果线程请求令牌的时间在下一令牌产生时间之后,那么该线程立刻就能够获取令牌;
反之,如果请求时间在下一令牌产生时间之前,那么该线程是在下一令牌产生的时间获取令牌。

由于此时下一令牌已经被该线程预占,所以下一令牌产生的时间需要加上 1 秒。

class SimpleLimiter {
  //下一令牌产生时间
  long next = System.nanoTime();
  //发放令牌间隔:纳秒
  long interval = 1000_000_000;
  //预占令牌,返回能够获取令牌的时间
  synchronized long reserve(long now){
    //请求时间在下一令牌产生时间之后
    //重新计算下一令牌产生时间
    if (now > next){
      //将下一令牌产生时间重置为当前时间
      next = now;
    }
    //能够获取令牌的时间
    long at=next;
    //设置下一令牌产生时间
    next += interval;
    //返回线程需要等待的时间
    return Math.max(at, 0L);
  }
  //申请令牌
  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();
      }
    }
  }
}

如果令牌桶的容量大于 1,又该如何处理呢?

按照令牌桶算法,令牌要首先从令牌桶中出,所以需要按需计算令牌桶中的数量,当有线程请求令牌时,先从令牌桶中出。具体的代码实现如下所示。

增加了一个 resync() 方法,在这个方法中,如果线程请求令牌的时间在下一令牌产生时间之后,会重新计算令牌桶中的令牌数,新产生的令牌的计算公式是:(now-next)/interval,可对照上面的示意图来理解。

reserve() 方法中,则增加了先从令牌桶中出令牌的逻辑,不过需要注意的是,如果令牌是从令牌桶中出的,那么 next 就无需增加一个 interval 了。

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();
      }
    }
  }
}

三、总结

经典的限流算法有两个:

  • 一个是令牌桶算法(Token Bucket)
  • 另一个是漏桶算法(Leaky Bucket)。

令牌桶算法是定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,然后才能通过限流器;

漏桶算法里,请求就像水一样注入漏桶,漏桶会按照一定的速率自动将水漏掉,只有漏桶里还能注入水的时候,请求才能通过限流器。

令牌桶算法和漏桶算法很像一个硬币的正反面,所以你可以参考令牌桶算法的实现来实现漏桶算法。

Guava 是如何实现令牌桶算法的,示例代码是对 Guava RateLimiter 的简化,Guava RateLimiter 扩展了标准的令牌桶算法,比如还能支持预热功能

对于按需加载的缓存来说,预热后缓存能支持 5 万 TPS 的并发,但是在预热前 5 万 TPS 的并发直接就把缓存击垮了,所以如果需要给该缓存限流,限流器也需要支持预热功能,在初始阶段,限制的流速 r 很小,但是动态增长的。

预热功能的实现非常复杂,Guava 构建了一个积分函数来解决这个问题,可以继续深入研究。


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/160508.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

2022智能制造世界与中国十大科技进展内容小结

2022智能制造世界与中国十大科技进展内容小结1. 全断面隧道掘进装备行业工业互联网平台2. 大型复杂构件机器人原位高效高质量铣削加工技术及装备3. 西门子SNC—原生数字化工厂4. 亚马逊 数字孪生服务IoT TwinMaker5. Nvidia 新一代智能移动机器人仿真平台6. 北京奔驰的基于大数…

Excel特殊符号及用法

一、公式中常用符号 &#xff1a; 表示一个单元格区域&#xff0c;如A1:B10 - * / 加减乘除运算符> 大于号> 大于等于号< 小于号< 小于等于号<> 不等于号&#xff0c;如IF(A1<>"销售部",,)^数字 乘方运算&#xff0c;如…

第二章Mybatis进阶操作学习

文章目录特殊SQL的执行模糊查询批量操作动态设置表名添加功能获取自增的主键自定义映射resultMap处理字段和属性的映射关系在SQL语句中使用别名使用核心配置文件中的驼峰对应方法使用resultMap自定义映射处理一对一映射处理级联方式处理association标签分步查询分步查询的优点&…

【网安神器篇】——Sqlmap详解

作者名&#xff1a;白昼安全主页面链接&#xff1a;主页传送门创作初心&#xff1a;舞台再大&#xff0c;你不上台&#xff0c;永远是观众&#xff0c;没人会关心你努不努力&#xff0c;摔的痛不痛&#xff0c;他们只会看你最后站在什么位置&#xff0c;然后羡慕或鄙夷座右铭&a…

C语言 自定义类型 之 【联合体(共用体)】

文章目录前言联合体(UNION)类型的定义联合体的特点联合体大小的计算联合体使用的典型例题 — 判断机器的大小端写在最后前言 联合体&#xff08;union&#xff09;在C语言中是不常用的&#xff0c;不过存在就必然有其有用之处&#xff0c;这里不深入探讨联合体用在何处有什么用…

R语言实现牛顿插值

文章目录4 差商与牛顿插值差商牛顿插值4 差商与牛顿插值 差商 如果采取间隔不等的采样&#xff0c;差商会变得稍显复杂&#xff0c;对于x0,x1,…,xnx_0,x_1,\ldots,x_nx0​,x1​,…,xn​&#xff0c;若与y0,y1,…,yny_0,y_1,\ldots,y_ny0​,y1​,…,yn​通过映射fff一一对应&…

ASEMI整流桥KBP310电路设计注意事项

编辑-Z 型号&#xff1a;KBP310 最大重复峰值反向电压&#xff08;VRRM&#xff09;&#xff1a;1000V 最大平均正向整流输出电流&#xff08;IF&#xff09;&#xff1a;3.0A 峰值正向浪涌电流&#xff08;IFSM&#xff09;&#xff1a;60A 每个元件的典型热阻&#xff0…

MySQL LOAD VS DM8 dmfldr

MySQL LOAD VS DM8 dmfldr 背景 某业务系统从MySQL迁移至达梦后&#xff0c;有导入业务文件的功能使用MySQL的LOAD方式将csv文件导入到指定的表中。迁移到达梦后&#xff0c;该功能需要进行对应的调整&#xff08;因为达梦没有LOAD功能&#xff09;&#xff0c;但达梦提供了dm…

【自定义类型】-结构体,枚举,联合

&#x1f387;作者&#xff1a;小树苗渴望变成参天大树 &#x1f496;作者宣言&#xff1a;认真写好每一篇博客 &#x1f9e8;作者gitee:link 如 果 你 喜 欢 作 者 的 文 章 &#xff0c;就 给 作 者 点 点 关 注 吧&#xff01; &#x1f38a;自定义类型&#x1f389;前言&a…

osg fbo(二),将颜色缓冲区图片铺在全屏

其实&#xff0c;需要两个摄像机&#xff0c;一个是采样摄像机&#xff0c;一个是最终显示的摄像机。 osg fbo(一&#xff09;中&#xff0c;passRoot直接加的面片&#xff0c;现在通过最终显示摄像机来代替&#xff0c;passRoot加上这两个摄像机即可。 //passRoot->addCh…

jmeter编写压测脚本规范

一、压测时长 压测时长&#xff0c;一般为10分钟或者15分钟。 设置时长&#xff1a; 勾选 永远--持续时间&#xff08;秒&#xff09; 二、脚本编写规范 脚本越简单越好&#xff0c;多余的监听会影响jmeter的性能&#xff0c;继而影响到压测结果。 一个基础的脚本&#xf…

Web前端axios从入门

1 axios是什么 前端最流行的 ajax请求库&#xff0c; https://gitcode.net/mirrors/axios/axios?utm_sourcecsdn_github_accelerator 基于promise的异步ajax请求库浏览器端/node端都可以使用支持请求/响应拦截器支持请求取消请求/响应数据转换批量发送多个请求 2 json-serv…

论文解读OTA: Optimal Transport Assignment for Object Detection

CSDN优秀解读&#xff1a;https://blog.csdn.net/jiaoyangwm/article/details/126638775 2021 https://arxiv.org/pdf/2103.14259.pdf 关键解读 在目标检测中标签分配的最新进展主要寻求为每个GT对象独立定义正/负训练样本。在本文中&#xff0c;我们创新性地从全局的角度重…

SpringCloudAlibaba商城实战项目(day01)

前言 闲来无事在B站找了一个项目&#xff0c;是谷粒商城的项目&#xff0c;于是乎照着在敲这个项目&#xff0c;特此记录一下。会持续更新到这个项目敲完。这个记录偏向小白向&#xff0c;确保你照着敲也可以完成所有项目的搭建。 一、简介 1.1、项目架构图 1.2、服务列表 …

CAN Bus cable simulation

REF&#xff1a;CAN总线标准接口与布线规范 https://zhuanlan.zhihu.com/p/34333969高速CAN总线物理层对线束的要求 https://www.suncve.com/the-requirement-of-physical-layer-of-can-bus-for-wiring-harness/利用LTSPICE 做仿真&#xff0c; 选用的是 ADI的 CAN transceiver…

RabbitMQ快速入门之进阶

RabbitMQ快速入门之进阶 进阶RabbitMQ快速入门之进阶1、confirm 模式的设置2、return 退回模式的处理3、消费者 Ack&#xff0c;手动确认4、消费端限流 (流量削缝)5、TTL存活时间过期时间6、死信队列DLX7、延迟队列 &#xff08;TTL DLX&#xff09;1、confirm 模式的设置 *c…

VSCode使用Clangd

前言 在使用微软的C/C插件时&#xff0c;遇到较大项目时&#xff0c;代码提示速度非常的慢&#xff0c;这时可以使用clangd 1、系统安装clangd 版本选择&#xff1a;Linux github仓库: https://github.com/clangd/clangd/releases 解压下载好的安装包&#xff1a; unzip cla…

Python实现小米蓝牙温湿度计2 Home Assistant 自定义组件源码

小米 米家蓝牙温湿度计2 这是一个Home Assistant自定义组件&#xff0c;用于 Home Assistant 通过 蓝牙适配器 直接集成 小米 米家蓝牙温湿度计 (LYWSDCGQ/01ZM) 和 米家蓝牙温湿度计2 (LYWSD03MMC)。 v0.2.0-dev版本以后&#xff0c;已经支持自动发现功能&#xff0c;不需要…

Leetcode:501. 二叉搜索树中的众数(C++)

目录 问题描述&#xff1a; 实现代码与解析&#xff1a; 通用写法&#xff08;递归&#xff09;&#xff1a; 原理思路&#xff1a; 依据二叉搜索树特性写法&#xff08;递归&#xff09;&#xff1a; 原理思路&#xff1a; 迭代&#xff1a; 原理思路&#xff1a; 问题…

Android Compose——一个简单的新闻APP

Owl简述效果视频导航导航结点路线图底部导航栏使用标签页状态切换FeaturePage构建CoursePage实现搜索ViewModelView详情页DetailDescribeLesson尾Gitte简述 此Demo是参考Google Github其中一个Demo而完成&#xff0c;涉及的内容并不复杂&#xff0c;主要是为了熟悉Compose编码…