简单分析Guava中RateLimiter中的令牌桶算法的实现

为什么80%的码农都做不了架构师?>>>   hot3.png

       令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。

    大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。                                                                          

                                                                                                                         ——摘自百度百科

那么什么是令牌桶算法呢?

        简单来说就是,生产者和消费者之间的事情,生产者往一个桶(Bucket)中丢令牌(Token),消费者从里面去捡令牌,生产者以一定的速率丢令牌,直到桶装满了,令牌就溢出了,消费者持续从桶里面捡令牌,没有令牌的话,就持续等待,直到有令牌出现。

 

 这里我们看下具体令牌桶算法的实现(Guava中的RateLimiter),以及在实际生产中的应用场景(限制接口访问频次,保护后端系统)

       我们在暴露对外接口的时候,对于高频次访问的接口(例如查询接口),需要考虑到相关的保护措施,避免接口瞬时访问量过大,导致服务端不可用的场景产生。因此,我们可以使用RateLimiter,来做相关的频控。

  下面是RateLimiter的使用Demo:

   1、引入相关的依赖

        <dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>19.0</version></dependency>

 2、编写相关的Demo

public class RateLimiterTest {public static void main(String[] args) throws InterruptedException {RateLimiter limiter = RateLimiter.create(10);// 代码1Thread.currentThread().sleep(1000);//步骤1if (limiter.tryAcquire(20))//代码2System.out.println("======== Time1:" + System.currentTimeMillis() / 1000);Thread.currentThread().sleep(1001);if (limiter.tryAcquire(1))//代码3System.out.println("======== Time2:" + System.currentTimeMillis() / 1000);if (limiter.tryAcquire(5))System.out.println("======== Time3:" + System.currentTimeMillis() / 1000);}
}

3、运行结果如下

场景1:

======== Time1:1533114071
======== Time2:1533114072

场景2:修改代码:去掉步骤1,运行结果如下:

======== Time1:1533114155

场景3:修改相关代码如下:

public class RateLimiterTest {public static void main(String[] args) throws InterruptedException {RateLimiter limiter = RateLimiter.create(10);// 代码1Thread.currentThread().sleep(2000);if (limiter.tryAcquire(21))//代码2System.out.println("======== Time1:" + System.currentTimeMillis() / 1000);Thread.currentThread().sleep(1001);if (limiter.tryAcquire(1))//代码3System.out.println("======== Time2:" + System.currentTimeMillis() / 1000);if (limiter.tryAcquire(5))System.out.println("======== Time3:" + System.currentTimeMillis() / 1000);}
}

    结果如下:

======== Time1:1533114623

 下面我们来分析这三种情况产生的原因,顺便也分析下RateLimiter中的令牌桶算法是如何实现的。

在分析之前,说明一点,我之前一直以为令牌桶算法,是定时器机制,定时往桶里面放令牌,但是有些时候并不是这样的。先声明一下。

我们来分析下代码:

         代码行1:

 RateLimiter limiter = RateLimiter.create(10);

    这行代码,我们知道是创建一个每秒产生10个permit的速率器

      代码行2:
           limiter.tryAcquire(20)  //尝试从速率器中获取20个permit,获取成功 true;失败 false
      代码行3:
            limiter.tryAcquire(1) //尝试从速率器中获取1个permit,获取成功 true;失败 false

        为什么相同的代码,不同的休眠时间导致不同的结果呢?


结论:

    1、RateLimiter 速率器,通过预支将来的令牌来进行限制频控,什么意思呢?打个比方:速率器相当于工厂,获取令牌许可的线程相当于经销商,经销商过来取货,工厂每天的生产的货品是一定的(100吨/天),A经销商来取货,第一天取了200吨货,工厂没有这么多货,怎么办呢?为了留住这个经销商,厂长做了决定,给200吨,现在的100吨先给你,明天的100吨也给你,然后把200吨货品的提取清单给了A经销商,A很满意的离开了。过了一会,B来了,B要10吨货物,这个时候,厂长就没有那么好说话了(谁让大客户已经到手了呢?),说10吨货物可以,你
后天来吧,明天和今天的活已经都卖完了。这个时候通过这种方式,来限制一天只卖/生产100吨的货物。

  根据这个结论我们来看相关的代码:

RateLimiter limiter = RateLimiter.create(10);

调用的是:

@VisibleForTestingstatic RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds 注意 这里的maxBurstSeconds指定的是1s 直接影响后面的maxPermit*/);rateLimiter.setRate(permitsPerSecond);//见下文代码return rateLimiter;}

setRate(permitsPerSecond)如下:

public final void setRate(double permitsPerSecond) {checkArgument(permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");synchronized (mutex()) {doSetRate(permitsPerSecond, stopwatch.readMicros());//stopwatch.readMirco 获取的是创建以来的系统时间 这里调用SmoothRateLimiter.doSetRate()}}

 SmoothRateLimiter.doSetRate()

 @Overridefinal void doSetRate(double permitsPerSecond, long nowMicros) {resync(nowMicros);//你可以认为这边是重设相关的nextFreeTicketMicros和storedPermits 这个函数是相关计算频控的重要组成部分  ------1double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;this.stableIntervalMicros = stableIntervalMicros;doSetRate(permitsPerSecond, stableIntervalMicros);//这个函数是RateLimiter创建时候 初始化maxpermits和StorePermits的相关部分 也是一个重要的部分 ---2}

我们来看1的实现:

    

/*** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.* 基于当前的时间 计算相关的storedPermits和nextFreeTicketMicros *  storedPermits:当前存储的令牌数*  nextFreeTicketMicros:下次可以获取令牌的时间 其实这么讲不太准确 应该说是,上次令牌获取之后预支到下次可以获取令牌的最早时间*         此处再创建的时候 nextFreeTicketMicros基本就是创建时候的系统时间*/void resync(long nowMicros) {// if nextFreeTicket is in the past, resync to nowif (nowMicros > nextFreeTicketMicros) {storedPermits = min(maxPermits,storedPermits+ (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());nextFreeTicketMicros = nowMicros; }}

        我们可以看到,我们这里通过计算当前时间和下次可以获取令牌的时间,相互计算差值,然后除以一个令牌产生的时间间隔,来计算当前时段可以产生多少令牌,然后和我们的     maxPermits来取最小值,由此我们可以看到storedPermits最多只能存储maxPermits数量的令牌,这也是令牌桶大小所限制的。
    我们再来看2代码的实现:

@Overridevoid doSetRate(double permitsPerSecond, double stableIntervalMicros) {double oldMaxPermits = this.maxPermits;maxPermits = maxBurstSeconds * permitsPerSecond;//设置最大可存储的令牌数 这里的maxBurstSeconds 就是之前设置的1s 所以maxPermits数值上等于我们设置的permitsPerSecondif (oldMaxPermits == Double.POSITIVE_INFINITY) {// if we don't special-case this, we would get storedPermits == NaN, belowstoredPermits = maxPermits;} else {storedPermits = (oldMaxPermits == 0.0)? 0.0 // initial state: storedPermits * maxPermits / oldMaxPermits;}}

到这里我们的初始化RateLimiter结束了。我们来明确其中的几个概念:

    maxPermits:最大存储的令牌数,即令牌桶的大小

    storedPermits:已存储的令牌数<=maxPermits,当然这个是通过计算算出来的

    nextFreeTicketMicros:上次获取令牌时预支的最早能够再次获取令牌的时间

    nowMicros:当前系统时间

好,我们接下来看如何获取令牌:

  代码2:

limiter.tryAcquire(20)

 具体的代码实现如下:

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {//timeout = 0 unit=MICROSECONDSlong timeoutMicros = max(unit.toMicros(timeout), 0);checkPermits(permits);//校验参数long microsToWait;synchronized (mutex()) {//互斥量 long nowMicros = stopwatch.readMicros();if (!canAcquire(nowMicros, timeoutMicros)) {//此处判断当前时间是否大于等于上次预支最早时间  ----1return false;} else {microsToWait = reserveAndGetWaitLength(permits, nowMicros);//当前线程获取到permit需要等待的时间 ---2}}stopwatch.sleepMicrosUninterruptibly(microsToWait);//线程等待 获取permitreturn true;}

我们来看1的实现部分:

private boolean canAcquire(long nowMicros, long timeoutMicros) {return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;}
@Overridefinal long queryEarliestAvailable(long nowMicros) {return nextFreeTicketMicros;}

有此可见,如果当前时间+超时时间>=预支的最早时间,那么是可以获取许可的,反之则不能获取许可

再看代码2的实现:

final long reserveAndGetWaitLength(int permits, long nowMicros) {long momentAvailable = reserveEarliestAvailable(permits, nowMicros);return max(momentAvailable - nowMicros, 0);//计算需要等待的时间}

SmoothRateLimiter.reserveEarliestAvailable()

@Overridefinal long reserveEarliestAvailable(int requiredPermits, long nowMicros) {resync(nowMicros);//这里是重设相关的storedPermits和nenextFreeTicketMicros 这个在前文我们讲过 需要注意的是 这边的nextFreeTicketMicros设置的是nowMicros 可能会有人有疑问,nextFreeTicketMicros不是预支的最早获取permit的时间吗?怎么是nowMicros了呢?我们下面看long returnValue = nextFreeTicketMicros;//这里返回的其实就是nowMiscrosdouble storedPermitsToSpend = min(requiredPermits, this.storedPermits);//本次能消费的最多的permitdouble freshPermits = requiredPermits - storedPermitsToSpend;//需要预支的permitlong waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)+ (long) (freshPermits * stableIntervalMicros);//预支的生产的时间try {this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);//这里就是重设了预支下次能够获取permit的最早时间了 这边将waitMiscros加上了} catch (ArithmeticException e) {this.nextFreeTicketMicros = Long.MAX_VALUE;}this.storedPermits -= storedPermitsToSpend;//扣除本地消费的permitreturn returnValue;//返回当前时间}

      这样就完成了前后两个permit之间获取的的联动性,并不是有一个定时任务往中间放permit,而是直接预支的后面消费者的时间来进行控制的,这样有一个好处就是,第一次获取permit的时候,其实可以获取N多个permit,并不做限制,只是这么多的permit会导致后面消费者卡死在那边,当然,消费者在timeOut范围内获取不到permit也就直接返回了。

 

Q:

    思考下 前后两个线程之间的同步部分,为什么还要等待一段时间?最多能储存多少permit?令牌桶有什么弊端(或者说RateLimiter可能存在的问题)?

转载于:https://my.oschina.net/guanhe/blog/1921116

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

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

相关文章

gcc oracle mysql_Linux下C语言访问Oracle数据库Demo

前提条件1. Linux环境已经存在&#xff0c;安装好了Oracle本demo 运行环境本地环境 RedHat LINUX AS 4 ,ORACLE 10G本地数据库sid orcl,ip:127.0.0.1,用户名:kingbi&#xff0c;密码&#xff1a;kingbi,表dsd_test. 显示表dsd_test 的所有记录.步骤&#xff1a;(1) 创建表 …

炼数成金数据分析课程---16、机器学习中的分类算法(交叉内容,后面要重点看)...

炼数成金数据分析课程---16、机器学习中的分类算法&#xff08;交叉内容&#xff0c;后面要重点看&#xff09; 一、总结 一句话总结&#xff1a; 大纲实例快速学习法 主要讲解常用分类算法(如Knn、决策树、贝叶斯分类器等)的原理及python代码实现 1、什么是分类&#xff1f; 分…

NFS配置详解

1、NFS服务介绍1.1 什么是NFS&#xff1f;NFS是Network File System的缩写。中文意思是网络文件系统。它的主要功能是通过网络&#xff08;一般是局域网&#xff09;让不同的主机系统之间可以共享文件或者目录。NFS客户端&#xff08;一般为应用服务器&#xff0c;例如web&…

idea用法

更新gradle的依赖后&#xff0c;刷新项目引入jar包的方法&#xff1a; view--Tool Buttons 在右侧 Gradle 点刷新 转载于:https://www.cnblogs.com/z360519549/p/10994897.html

linux备份mysql需要暂停服务吗_【MySQL运维】线上MySQL数据库停服迁移流程

一、数据备份与恢复阶段&#xff0c;选在凌晨1点进行操作&#xff0c;暂停服务进行备份(允许停服2个小时)1、首先停止Nginx服务&#xff0c;并且修改数据库用户密码&#xff0c;防止还有新的连接进来2、杀掉某个用户所有进程for i in mysql -udba -pPASSWORD -ssse "show …

免费下载!5本阿里技术好书,带你看更大的世界

共享、开源是互联网技术发展的重要精神。在过去&#xff0c;25000多万名阿里工程师&#xff0c;撰写了一系列精品技术丛书&#xff0c;从算法、研发到职业人生随笔&#xff0c;应有尽有。目前该系列丛书已全部开放下载&#xff0c;供技术人免费阅读。 今天小编整理了其中的五本…

python3安装mysqlclient_Python3 安装mysqlclient错误处理(MAC版)

在使用django的时候需要安装mysqlclient库,很多时候会出现以下报错:running installrunning bdist_eggrunning egg_infowriting mysqlclient.egg-info/PKG-INFOwriting dependency_links to mysqlclient.egg-info/dependency_links.txtwriting top-level names to mysqlclient.…

React绑定事件处理函数this的几种方法

在以类继承的方式定义的组件中&#xff0c;为了能方便地调用当前组件的其他成员方法或属性&#xff08;如&#xff1a;this.state&#xff09;&#xff0c;通常需要将事件处理函数运行时的 this 指向当前组件实例。 绑定事件处理函数this的几种方法&#xff1a; 第一种方法&…

乌班图系统16.04安装

本例jiyu基于Ubuntu16.04 64位版本为例进行安装&#xff0c;安装的方式有多种&#xff0c;本文使用光盘进行安装安装前应准备好&#xff0c;将Ubuntu的镜像文件刻成光盘,然后将光盘放入光驱,并设置服务器从光盘启动,开机到如下界面&#xff1a;按Enter键到下一步&#xff0c;如…

python做游戏用什么软件_用Python自制谷歌小游戏

谷歌流量器中有个很有名的彩蛋&#xff1a;当你网络出现问题时&#xff0c;就会出现一个“小恐龙游戏”。(如果想要直接进行游戏&#xff0c;可以在地址栏输入&#xff1a;chrome://dino)今天我们就来给大家演示下&#xff0c;用Python来自己做一个仿制的“小恐龙游戏”&#x…

使用maven构建项目候,jar包错误的解决办法

1、删除架包&#xff0c;重新下载&#xff0c;右键项目点击"run as"中的“maven clean”,然后再maven中找到Update Project 2、可以在代码中&#xff0c;把鼠标放到报错的架包上 点击划红线部分&#xff0c;进行安装 转载于:https://www.cnblogs.com/qingqian/p/1099…

MySQL——通过EXPLAIN分析SQL的执行计划

在MySQL中&#xff0c;我们可以通过EXPLAIN命令获取MySQL如何执行SELECT语句的信息&#xff0c;包括在SELECT语句执行过程中表如何连接和连接的顺序。下面分别对EXPLAIN命令结果的每一列进行说明&#xff1a;select_type:表示SELECT的类型&#xff0c;常见的取值有&#xff1a;…

python将argv作为参数_在jupyter / ipython notebook中将命令行参数传递给argv

经过大量的环顾后,我发现了非常繁琐的自定义库,但是用几行代码解决了它,我认为这些代码很漂亮.我使用nbconvert最终得到一个html报告作为输出,包含笔记本中的所有图形和降价,但是通过最小的python包装器接受命令行参数&#xff1a;python文件test_args.py(正常执行命令行参数)&…

模拟输入(ADC-A0)

ESP8266具有内置的10位ADC&#xff0c;只有一个ADC通道(A0引脚)&#xff0c;即只有一个ADC输入引脚可读取来自外部器件的模拟电压 ESP8266上的ADC通道和芯片供电电压复用&#xff0c;也就是说我们可以将其设置为测量系统电压或者外部电压 测量外部电压&#xff1a; analogRead(…

SQL Server 连接超时案例一则

原文:SQL Server 连接超时案例一则上周六&#xff0c;一工厂系统管理员反馈一数据库连接不上&#xff0c;SSMS连接数据库报“连接超时时间已到。在尝试使用预登录握手确认时超过了此超时时间.......”, 如下截图所示&#xff1a; 另外远程连接也连接不上&#xff0c;系统管理员…

mysql 删除5天前 备份_mysql自动备份删除5天前的备份

1、查看磁盘空间情况&#xff1a;df -h2、创建备份目录&#xff1a;上面我们使用命令看出/home下空间比较充足&#xff0c;所以可以考虑在/home保存备份文件&#xff1b;cd /homemkdir backupcd backup3、创建备份Shell脚本:注意把以下命令中的DatabaseName换为实际的数据库名称…

个人作业-Alpha项目测试

这个作业属于哪个课程https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass2作业地址https://edu.cnblogs.com/campus/xnsy/SoftwareEngineeringClass2/homework/3340团队名称脑阔疼https://www.cnblogs.com/chaserFF/p/10994338.html这个作业的目标完成班级项目互评…

深入理解brew link命令

来源&#xff1a;https://newsn.net/say/brew-link-php71.html brew是mac机上面程序猿非常常用的软件包安装方式&#xff0c;其中有两组命令是需要大家知晓的。分别是&#xff1a;第一组&#xff1a;brew install和brew uninstall。第二组&#xff0c;brew link和brew unlink。…

scss2css vscode设置_VSCode下让CSS文件完美支持SCSS或SASS语法方法

VSCode下让CSS文件完美支持SCSS或SASS语法方法习惯Webpack PostCSS后, 通常PostCSS都是直接对CSS文件进行处理, 但是大部分习惯SCSS/SASS/LESS的朋友也许不适应了. 我专门研究了一下, 在Visual Studio Code编辑器下如果配置相关代码和设置达到CSS文件完美编写SCSS的办法, 其他…

第5章 初识JQuery

JQuery是对JavaScript的封装&#xff0c;简化了JS代码&#xff0c;是主流框架的基础(VUE,EasyUI,Bootstrap) 它是2006年推出的JQuery的优势&#xff1a; 体积小&#xff0c;压缩后只有100KB左右 强大的选择器 出色的DOM封装 可靠的事件处理机制 出色的浏览器兼容性 使用隐式迭代…