详解4种经典的限流算法
最近,我們的業(yè)務(wù)系統(tǒng)引入了Guava的RateLimiter限流組件,它是基于令牌桶算法實(shí)現(xiàn)的,而令牌桶是非常經(jīng)典的限流算法。本文將跟大家一起學(xué)習(xí)幾種經(jīng)典的限流算法。
限流是什么?
維基百科的概念如下:
In?computer?networks,?rate?limiting?is?used?to?control?the?rate?of?requests?sent?or received?by?a?network?interface?controller.?It?can?be?used?to?prevent?DoS?attacks? and?limit?web?scraping簡單翻譯一下:在計(jì)算機(jī)網(wǎng)絡(luò)中,限流就是控制網(wǎng)絡(luò)接口發(fā)送或接收請(qǐng)求的速率,它可防止DoS攻擊和限制Web爬蟲。
限流,也稱流量控制。是指系統(tǒng)在面臨高并發(fā),或者大流量請(qǐng)求的情況下,限制新的請(qǐng)求對(duì)系統(tǒng)的訪問,從而保證系統(tǒng)的穩(wěn)定性。限流會(huì)導(dǎo)致部分用戶請(qǐng)求處理不及時(shí)或者被拒,這就影響了用戶體驗(yàn)。所以一般需要在系統(tǒng)穩(wěn)定和用戶體驗(yàn)之間平衡一下。舉個(gè)生活的例子:
★一些熱門的旅游景區(qū),一般會(huì)對(duì)每日的旅游參觀人數(shù)有限制的。每天只會(huì)賣出固定數(shù)目的門票,比如5000張。假設(shè)在五一、國慶假期,你去晚了,可能當(dāng)天的票就已經(jīng)賣完了,就無法進(jìn)去游玩了。即使你進(jìn)去了,排隊(duì)也能排到你懷疑人生。
”常見的限流算法
固定窗口限流算法
首先維護(hù)一個(gè)計(jì)數(shù)器,將單位時(shí)間段當(dāng)做一個(gè)窗口,計(jì)數(shù)器記錄這個(gè)窗口接收請(qǐng)求的次數(shù)。
當(dāng)次數(shù)少于限流閥值,就允許訪問,并且計(jì)數(shù)器+1
當(dāng)次數(shù)大于限流閥值,就拒絕訪問。
當(dāng)前的時(shí)間窗口過去之后,計(jì)數(shù)器清零。
假設(shè)單位時(shí)間是1秒,限流閥值為3。在單位時(shí)間1秒內(nèi),每來一個(gè)請(qǐng)求,計(jì)數(shù)器就加1,如果計(jì)數(shù)器累加的次數(shù)超過限流閥值3,后續(xù)的請(qǐng)求全部拒絕。等到1s結(jié)束后,計(jì)數(shù)器清0,重新開始計(jì)數(shù)。如下圖:
偽代碼如下:
????/***?固定窗口時(shí)間算法*?@return*/boolean?fixedWindowsTryAcquire()?{long?currentTime?=?System.currentTimeMillis();??//獲取系統(tǒng)當(dāng)前時(shí)間if?(currentTime?-?lastRequestTime?>?windowUnit)?{??//檢查是否在時(shí)間窗口內(nèi)counter?=?0;??//?計(jì)數(shù)器清0lastRequestTime?=?currentTime;??//開啟新的時(shí)間窗口}if?(counter?<?threshold)?{??//?小于閥值counter++;??//計(jì)數(shù)器加1return?true;}return?false;}但是,這種算法有一個(gè)很明顯的臨界問題:假設(shè)限流閥值為5個(gè)請(qǐng)求,單位時(shí)間窗口是1s,如果我們?cè)趩挝粫r(shí)間內(nèi)的前0.8-1s和1-1.2s,分別并發(fā)5個(gè)請(qǐng)求。雖然都沒有超過閥值,但是如果算0.8-1.2s,則并發(fā)數(shù)高達(dá)10,已經(jīng)超過單位時(shí)間1s不超過5閥值的定義啦。
滑動(dòng)窗口限流算法
滑動(dòng)窗口限流解決固定窗口臨界值的問題。它將單位時(shí)間周期分為n個(gè)小周期,分別記錄每個(gè)小周期內(nèi)接口的訪問次數(shù),并且根據(jù)時(shí)間滑動(dòng)刪除過期的小周期。
一張圖解釋滑動(dòng)窗口算法,如下:
假設(shè)單位時(shí)間還是1s,滑動(dòng)窗口算法把它劃分為5個(gè)小周期,也就是滑動(dòng)窗口(單位時(shí)間)被劃分為5個(gè)小格子。每格表示0.2s。每過0.2s,時(shí)間窗口就會(huì)往右滑動(dòng)一格。然后呢,每個(gè)小周期,都有自己獨(dú)立的計(jì)數(shù)器,如果請(qǐng)求是0.83s到達(dá)的,0.8~1.0s對(duì)應(yīng)的計(jì)數(shù)器就會(huì)加1。
我們來看下滑動(dòng)窗口是如何解決臨界問題的?
假設(shè)我們1s內(nèi)的限流閥值還是5個(gè)請(qǐng)求,0.8~1.0s內(nèi)(比如0.9s的時(shí)候)來了5個(gè)請(qǐng)求,落在黃色格子里。時(shí)間過了1.0s這個(gè)點(diǎn)之后,又來5個(gè)請(qǐng)求,落在紫色格子里。如果是固定窗口算法,是不會(huì)被限流的,但是滑動(dòng)窗口的話,每過一個(gè)小周期,它會(huì)右移一個(gè)小格。過了1.0s這個(gè)點(diǎn)后,會(huì)右移一小格,當(dāng)前的單位時(shí)間段是0.2~1.2s,這個(gè)區(qū)域的請(qǐng)求已經(jīng)超過限定的5了,已觸發(fā)限流啦,實(shí)際上,紫色格子的請(qǐng)求都被拒絕啦。
TIPS: 當(dāng)滑動(dòng)窗口的格子周期劃分的越多,那么滑動(dòng)窗口的滾動(dòng)就越平滑,限流的統(tǒng)計(jì)就會(huì)越精確。
滑動(dòng)窗口算法偽代碼實(shí)現(xiàn)如下:
?/***?單位時(shí)間劃分的小周期(單位時(shí)間是1分鐘,10s一個(gè)小格子窗口,一共6個(gè)格子)*/private?int?SUB_CYCLE?=?10;/***?每分鐘限流請(qǐng)求數(shù)*/private?int?thresholdPerMin?=?100;/***?計(jì)數(shù)器,?k-為當(dāng)前窗口的開始時(shí)間值秒,value為當(dāng)前窗口的計(jì)數(shù)*/private?final?TreeMap<Long,?Integer>?counters?=?new?TreeMap<>();/***?滑動(dòng)窗口時(shí)間算法實(shí)現(xiàn)*/boolean?slidingWindowsTryAcquire()?{long?currentWindowTime?=?LocalDateTime.now().toEpochSecond(ZoneOffset.UTC)?/?SUB_CYCLE?*?SUB_CYCLE;?//獲取當(dāng)前時(shí)間在哪個(gè)小周期窗口int?currentWindowNum?=?countCurrentWindow(currentWindowTime);?//當(dāng)前窗口總請(qǐng)求數(shù)//超過閥值限流if?(currentWindowNum?>=?thresholdPerMin)?{return?false;}//計(jì)數(shù)器+1counters.get(currentWindowTime)++;return?true;}/***?統(tǒng)計(jì)當(dāng)前窗口的請(qǐng)求數(shù)*/private?int?countCurrentWindow(long?currentWindowTime)?{//計(jì)算窗口開始位置long?startTime?=?currentWindowTime?-?SUB_CYCLE*?(60s/SUB_CYCLE-1);int?count?=?0;//遍歷存儲(chǔ)的計(jì)數(shù)器Iterator<Map.Entry<Long,?Integer>>?iterator?=?counters.entrySet().iterator();while?(iterator.hasNext())?{Map.Entry<Long,?Integer>?entry?=?iterator.next();//?刪除無效過期的子窗口計(jì)數(shù)器if?(entry.getKey()?<?startTime)?{iterator.remove();}?else?{//累加當(dāng)前窗口的所有計(jì)數(shù)器之和count?=count?+?entry.getValue();}}return?count;}滑動(dòng)窗口算法雖然解決了固定窗口的臨界問題,但是一旦到達(dá)限流后,請(qǐng)求都會(huì)直接暴力被拒絕。醬紫我們會(huì)損失一部分請(qǐng)求,這其實(shí)對(duì)于產(chǎn)品來說,并不太友好。
漏桶算法
漏桶算法面對(duì)限流,就更加的柔性,不存在直接的粗暴拒絕。
它的原理很簡單,可以認(rèn)為就是注水漏水的過程。往漏桶中以任意速率流入水,以固定的速率流出水。當(dāng)水超過桶的容量時(shí),會(huì)被溢出,也就是被丟棄。因?yàn)橥叭萘渴遣蛔兊?#xff0c;保證了整體的速率。
流入的水滴,可以看作是訪問系統(tǒng)的請(qǐng)求,這個(gè)流入速率是不確定的。
桶的容量一般表示系統(tǒng)所能處理的請(qǐng)求數(shù)。
如果桶的容量滿了,就達(dá)到限流的閥值,就會(huì)丟棄水滴(拒絕請(qǐng)求)
流出的水滴,是恒定過濾的,對(duì)應(yīng)服務(wù)按照固定的速率處理請(qǐng)求。
漏桶算法偽代碼實(shí)現(xiàn)如下:
/***?每秒處理數(shù)(出水率)*/private?long?rate;/***??當(dāng)前剩余水量*/private?long?currentWater;/***?最后刷新時(shí)間*/private?long?refreshTime;/***?桶容量*/private?long?capacity;/***?漏桶算法*?@return*/boolean?leakybucketLimitTryAcquire()?{long?currentTime?=?System.currentTimeMillis();??//獲取系統(tǒng)當(dāng)前時(shí)間long?outWater?=?(currentTime?-?refreshTime)?/?1000?*?rate;?//流出的水量?=(當(dāng)前時(shí)間-上次刷新時(shí)間)*?出水率long?currentWater?=?Math.max(0,?currentWater?-?outWater);?//?當(dāng)前水量?=?之前的桶內(nèi)水量-流出的水量refreshTime?=?currentTime;?//?刷新時(shí)間//?當(dāng)前剩余水量還是小于桶的容量,則請(qǐng)求放行if?(currentWater?<?capacity)?{currentWater++;return?true;}//?當(dāng)前剩余水量大于等于桶的容量,限流return?false;}在正常流量的時(shí)候,系統(tǒng)按照固定的速率處理請(qǐng)求,是我們想要的。但是面對(duì)突發(fā)流量的時(shí)候,漏桶算法還是循規(guī)蹈矩地處理請(qǐng)求,這就不是我們想看到的啦。流量變突發(fā)時(shí),我們肯定希望系統(tǒng)盡量快點(diǎn)處理請(qǐng)求,提升用戶體驗(yàn)嘛。
令牌桶算法
面對(duì)突發(fā)流量的時(shí)候,我們可以使用令牌桶算法限流。
令牌桶算法原理:
有一個(gè)令牌管理員,根據(jù)限流大小,定速往令牌桶里放令牌。
如果令牌數(shù)量滿了,超過令牌桶容量的限制,那就丟棄。
系統(tǒng)在接受到一個(gè)用戶請(qǐng)求時(shí),都會(huì)先去令牌桶要一個(gè)令牌。如果拿到令牌,那么就處理這個(gè)請(qǐng)求的業(yè)務(wù)邏輯;
如果拿不到令牌,就直接拒絕這個(gè)請(qǐng)求。
漏桶算法偽代碼實(shí)現(xiàn)如下:
????/***?每秒處理數(shù)(放入令牌數(shù)量)*/private?long?putTokenRate;/***?最后刷新時(shí)間*/private?long?refreshTime;/***?令牌桶容量*/private?long?capacity;/***?當(dāng)前桶內(nèi)令牌數(shù)*/private?long?currentToken?=?0L;/***?漏桶算法*?@return*/boolean?tokenBucketTryAcquire()?{long?currentTime?=?System.currentTimeMillis();??//獲取系統(tǒng)當(dāng)前時(shí)間long?generateToken?=?(currentTime?-?refreshTime)?/?1000?*?putTokenRate;?//生成的令牌?=(當(dāng)前時(shí)間-上次刷新時(shí)間)*?放入令牌的速率currentToken?=?Math.min(capacity,?generateToken?+?currentToken);?//?當(dāng)前令牌數(shù)量?=?之前的桶內(nèi)令牌數(shù)量+放入的令牌數(shù)量refreshTime?=?currentTime;?//?刷新時(shí)間//桶里面還有令牌,請(qǐng)求正常處理if?(currentToken?>?0)?{currentToken--;?//令牌數(shù)量-1return?true;}return?false;}如果令牌發(fā)放的策略正確,這個(gè)系統(tǒng)即不會(huì)被拖垮,也能提高機(jī)器的利用率。Guava的RateLimiter限流組件,就是基于令牌桶算法實(shí)現(xiàn)的。
參考與感謝
來,年輕人!請(qǐng)手?jǐn)]5種常見限流算法!
阿里云二面:你對(duì)限流了解多少?
總結(jié)
以上是生活随笔為你收集整理的详解4种经典的限流算法的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: scala 函数中嵌套函数_Scala中
- 下一篇: Java中那些内存泄漏的场景!