簡單分析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.pswp.cn/news/277551.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/277551.shtml
英文地址,請注明出處:http://en.pswp.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封裝 可靠的事件處理機制 出色的瀏覽器兼容性 使用隱式迭代…