简单分析Guava中RateLimiter中的令牌桶算法的实现
為什么80%的碼農(nóng)都做不了架構(gòu)師?>>> ??
? ? ? ?令牌桶算法是網(wǎng)絡(luò)流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一種算法。典型情況下,令牌桶算法用來控制發(fā)送到網(wǎng)絡(luò)上的數(shù)據(jù)的數(shù)目,并允許突發(fā)數(shù)據(jù)的發(fā)送。
? ??大小固定的令牌桶可自行以恒定的速率源源不斷地產(chǎn)生令牌。如果令牌不被消耗,或者被消耗的速度小于產(chǎn)生的速度,令牌就會(huì)不斷地增多,直到把桶填滿。后面再產(chǎn)生的令牌就會(huì)從桶中溢出。最后桶中可以保存的最大令牌數(shù)永遠(yuǎn)不會(huì)超過桶的大小。 ?? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??
? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ??? ???——摘自百度百科
那么什么是令牌桶算法呢?
? ??? ??簡單來說就是,生產(chǎn)者和消費(fèi)者之間的事情,生產(chǎn)者往一個(gè)桶(Bucket)中丟令牌(Token),消費(fèi)者從里面去撿令牌,生產(chǎn)者以一定的速率丟令牌,直到桶裝滿了,令牌就溢出了,消費(fèi)者持續(xù)從桶里面撿令牌,沒有令牌的話,就持續(xù)等待,直到有令牌出現(xiàn)。
?
?這里我們看下具體令牌桶算法的實(shí)現(xiàn)(Guava中的RateLimiter),以及在實(shí)際生產(chǎn)中的應(yīng)用場景(限制接口訪問頻次,保護(hù)后端系統(tǒng))
? ? ? ?我們在暴露對外接口的時(shí)候,對于高頻次訪問的接口(例如查詢接口),需要考慮到相關(guān)的保護(hù)措施,避免接口瞬時(shí)訪問量過大,導(dǎo)致服務(wù)端不可用的場景產(chǎn)生。因此,我們可以使用RateLimiter,來做相關(guān)的頻控。
? 下面是RateLimiter的使用Demo:
? ?1、引入相關(guān)的依賴
<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>19.0</version></dependency>?2、編寫相關(guān)的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、運(yùn)行結(jié)果如下
場景1:
======== Time1:1533114071 ======== Time2:1533114072場景2:修改代碼:去掉步驟1,運(yùn)行結(jié)果如下:
======== Time1:1533114155場景3:修改相關(guān)代碼如下:
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);} }? ? 結(jié)果如下:
======== Time1:1533114623?下面我們來分析這三種情況產(chǎn)生的原因,順便也分析下RateLimiter中的令牌桶算法是如何實(shí)現(xiàn)的。
在分析之前,說明一點(diǎn),我之前一直以為令牌桶算法,是定時(shí)器機(jī)制,定時(shí)往桶里面放令牌,但是有些時(shí)候并不是這樣的。先聲明一下。
我們來分析下代碼:
?????????代碼行1:
?RateLimiter limiter = RateLimiter.create(10);????這行代碼,我們知道是創(chuàng)建一個(gè)每秒產(chǎn)生10個(gè)permit的速率器
? ? ??代碼行2:
? ? ? ? ? ?limiter.tryAcquire(20) ?//嘗試從速率器中獲取20個(gè)permit,獲取成功 true;失敗 false
? ????代碼行3:
? ? ? ? ? ? limiter.tryAcquire(1) //嘗試從速率器中獲取1個(gè)permit,獲取成功 true;失敗 false
????????為什么相同的代碼,不同的休眠時(shí)間導(dǎo)致不同的結(jié)果呢?
結(jié)論:
????1、RateLimiter 速率器,通過預(yù)支將來的令牌來進(jìn)行限制頻控,什么意思呢?打個(gè)比方:速率器相當(dāng)于工廠,獲取令牌許可的線程相當(dāng)于經(jīng)銷商,經(jīng)銷商過來取貨,工廠每天的生產(chǎn)的貨品是一定的(100噸/天),A經(jīng)銷商來取貨,第一天取了200噸貨,工廠沒有這么多貨,怎么辦呢?為了留住這個(gè)經(jīng)銷商,廠長做了決定,給200噸,現(xiàn)在的100噸先給你,明天的100噸也給你,然后把200噸貨品的提取清單給了A經(jīng)銷商,A很滿意的離開了。過了一會(huì),B來了,B要10噸貨物,這個(gè)時(shí)候,廠長就沒有那么好說話了(誰讓大客戶已經(jīng)到手了呢?),說10噸貨物可以,你
后天來吧,明天和今天的活已經(jīng)都賣完了。這個(gè)時(shí)候通過這種方式,來限制一天只賣/生產(chǎn)100噸的貨物。
? 根據(jù)這個(gè)結(jié)論我們來看相關(guān)的代碼:
RateLimiter limiter = RateLimiter.create(10);調(diào)用的是:
@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 獲取的是創(chuàng)建以來的系統(tǒng)時(shí)間 這里調(diào)用SmoothRateLimiter.doSetRate()}}?SmoothRateLimiter.doSetRate()
@Overridefinal void doSetRate(double permitsPerSecond, long nowMicros) {resync(nowMicros);//你可以認(rèn)為這邊是重設(shè)相關(guān)的nextFreeTicketMicros和storedPermits 這個(gè)函數(shù)是相關(guān)計(jì)算頻控的重要組成部分 ------1double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;this.stableIntervalMicros = stableIntervalMicros;doSetRate(permitsPerSecond, stableIntervalMicros);//這個(gè)函數(shù)是RateLimiter創(chuàng)建時(shí)候 初始化maxpermits和StorePermits的相關(guān)部分 也是一個(gè)重要的部分 ---2}我們來看1的實(shí)現(xiàn):
? ??
/*** Updates {@code storedPermits} and {@code nextFreeTicketMicros} based on the current time.* 基于當(dāng)前的時(shí)間 計(jì)算相關(guān)的storedPermits和nextFreeTicketMicros * storedPermits:當(dāng)前存儲(chǔ)的令牌數(shù)* nextFreeTicketMicros:下次可以獲取令牌的時(shí)間 其實(shí)這么講不太準(zhǔn)確 應(yīng)該說是,上次令牌獲取之后預(yù)支到下次可以獲取令牌的最早時(shí)間* 此處再創(chuàng)建的時(shí)候 nextFreeTicketMicros基本就是創(chuàng)建時(shí)候的系統(tǒng)時(shí)間*/void resync(long nowMicros) {// if nextFreeTicket is in the past, resync to nowif (nowMicros > nextFreeTicketMicros) {storedPermits = min(maxPermits,storedPermits+ (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros());nextFreeTicketMicros = nowMicros; }}????????我們可以看到,我們這里通過計(jì)算當(dāng)前時(shí)間和下次可以獲取令牌的時(shí)間,相互計(jì)算差值,然后除以一個(gè)令牌產(chǎn)生的時(shí)間間隔,來計(jì)算當(dāng)前時(shí)段可以產(chǎn)生多少令牌,然后和我們的 ? ? maxPermits來取最小值,由此我們可以看到storedPermits最多只能存儲(chǔ)maxPermits數(shù)量的令牌,這也是令牌桶大小所限制的。
????我們再來看2代碼的實(shí)現(xiàn):
到這里我們的初始化RateLimiter結(jié)束了。我們來明確其中的幾個(gè)概念:
????maxPermits:最大存儲(chǔ)的令牌數(shù),即令牌桶的大小
????storedPermits:已存儲(chǔ)的令牌數(shù)<=maxPermits,當(dāng)然這個(gè)是通過計(jì)算算出來的
????nextFreeTicketMicros:上次獲取令牌時(shí)預(yù)支的最早能夠再次獲取令牌的時(shí)間
????nowMicros:當(dāng)前系統(tǒng)時(shí)間
好,我們接下來看如何獲取令牌:
? 代碼2:
limiter.tryAcquire(20)?具體的代碼實(shí)現(xiàn)如下:
public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {//timeout = 0 unit=MICROSECONDSlong timeoutMicros = max(unit.toMicros(timeout), 0);checkPermits(permits);//校驗(yàn)參數(shù)long microsToWait;synchronized (mutex()) {//互斥量 long nowMicros = stopwatch.readMicros();if (!canAcquire(nowMicros, timeoutMicros)) {//此處判斷當(dāng)前時(shí)間是否大于等于上次預(yù)支最早時(shí)間 ----1return false;} else {microsToWait = reserveAndGetWaitLength(permits, nowMicros);//當(dāng)前線程獲取到permit需要等待的時(shí)間 ---2}}stopwatch.sleepMicrosUninterruptibly(microsToWait);//線程等待 獲取permitreturn true;}我們來看1的實(shí)現(xiàn)部分:
private boolean canAcquire(long nowMicros, long timeoutMicros) {return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;} @Overridefinal long queryEarliestAvailable(long nowMicros) {return nextFreeTicketMicros;}有此可見,如果當(dāng)前時(shí)間+超時(shí)時(shí)間>=預(yù)支的最早時(shí)間,那么是可以獲取許可的,反之則不能獲取許可
再看代碼2的實(shí)現(xiàn):
final long reserveAndGetWaitLength(int permits, long nowMicros) {long momentAvailable = reserveEarliestAvailable(permits, nowMicros);return max(momentAvailable - nowMicros, 0);//計(jì)算需要等待的時(shí)間}SmoothRateLimiter.reserveEarliestAvailable()
@Overridefinal long reserveEarliestAvailable(int requiredPermits, long nowMicros) {resync(nowMicros);//這里是重設(shè)相關(guān)的storedPermits和nenextFreeTicketMicros 這個(gè)在前文我們講過 需要注意的是 這邊的nextFreeTicketMicros設(shè)置的是nowMicros 可能會(huì)有人有疑問,nextFreeTicketMicros不是預(yù)支的最早獲取permit的時(shí)間嗎?怎么是nowMicros了呢?我們下面看long returnValue = nextFreeTicketMicros;//這里返回的其實(shí)就是nowMiscrosdouble storedPermitsToSpend = min(requiredPermits, this.storedPermits);//本次能消費(fèi)的最多的permitdouble freshPermits = requiredPermits - storedPermitsToSpend;//需要預(yù)支的permitlong waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)+ (long) (freshPermits * stableIntervalMicros);//預(yù)支的生產(chǎn)的時(shí)間try {this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);//這里就是重設(shè)了預(yù)支下次能夠獲取permit的最早時(shí)間了 這邊將waitMiscros加上了} catch (ArithmeticException e) {this.nextFreeTicketMicros = Long.MAX_VALUE;}this.storedPermits -= storedPermitsToSpend;//扣除本地消費(fèi)的permitreturn returnValue;//返回當(dāng)前時(shí)間}??????這樣就完成了前后兩個(gè)permit之間獲取的的聯(lián)動(dòng)性,并不是有一個(gè)定時(shí)任務(wù)往中間放permit,而是直接預(yù)支的后面消費(fèi)者的時(shí)間來進(jìn)行控制的,這樣有一個(gè)好處就是,第一次獲取permit的時(shí)候,其實(shí)可以獲取N多個(gè)permit,并不做限制,只是這么多的permit會(huì)導(dǎo)致后面消費(fèi)者卡死在那邊,當(dāng)然,消費(fèi)者在timeOut范圍內(nèi)獲取不到permit也就直接返回了。
?
Q:
????思考下 前后兩個(gè)線程之間的同步部分,為什么還要等待一段時(shí)間?最多能儲(chǔ)存多少permit?令牌桶有什么弊端(或者說RateLimiter可能存在的問題)?
轉(zhuǎn)載于:https://my.oschina.net/guanhe/blog/1921116
總結(jié)
以上是生活随笔為你收集整理的简单分析Guava中RateLimiter中的令牌桶算法的实现的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: [react] 状态管理器它精髓是什么?
- 下一篇: 易语言静态连接器提取_正确易语言链接器l