后端服務(wù)不得不了解之限流
點擊上方?好好學(xué)java?,選擇?星標(biāo)?公眾號
重磅資訊,干貨,第一時間送達(dá) 今日推薦:分享一套基于SpringBoot和Vue的企業(yè)級中后臺開源項目,這個項目有點哇塞!
個人原創(chuàng)100W +訪問量博客:點擊前往,查看更多
作者:夢朝思夕
my.oschina.net/qiangmzsx/blog/4277685
限流簡介
現(xiàn)在說到高可用系統(tǒng),都會說到高可用的保護(hù)手段:緩存、降級和限流,本博文就主要說說限流。限流是流量限速(Rate Limit)的簡稱,是指只允許指定的事件進(jìn)入系統(tǒng),超過的部分將被拒絕服務(wù)、排隊或等待、降級等處理。
對于server服務(wù)而言,限流為了保證一部分的請求流量可以得到正常的響應(yīng),總好過全部的請求都不能得到響應(yīng),甚至導(dǎo)致系統(tǒng)雪崩。限流與熔斷經(jīng)常被人弄混,博主認(rèn)為它們最大的區(qū)別在于限流主要在server實現(xiàn),而熔斷主要在client實現(xiàn),當(dāng)然了,一個服務(wù)既可以充當(dāng)server也可以充當(dāng)client,這也是讓限流與熔斷同時存在一個服務(wù)中,這兩個概念才容易被混淆。
那為什么需要限流呢?很多人第一反應(yīng)就是服務(wù)扛不住了所以需要限流。這是不全面的說法,博主認(rèn)為限流是因為資源的稀缺或出于安全防范的目的,采取的自我保護(hù)的措施。限流可以保證使用有限的資源提供最大化的服務(wù)能力,按照預(yù)期流量提供服務(wù),超過的部分將會拒絕服務(wù)、排隊或等待、降級等處理。
現(xiàn)在的系統(tǒng)對限流的支持各有不同,但是存在一些標(biāo)準(zhǔn)。在HTTP RFC 6585標(biāo)準(zhǔn)中規(guī)定了『429 Too Many Requests 』,429狀態(tài)碼表示用戶在給定時間內(nèi)發(fā)送了太多的請求,需要進(jìn)行限流(“速率限制”),同時包含一個 Retry-After 響應(yīng)頭用于告訴客戶端多長時間后可以再次請求服務(wù)。
HTTP/1.1?429?Too?Many?Requests
Content-Type:?text/html
Retry-After:?3600
??
?????Too?Many?Requests
??
??
?????Too?Many?Requests
?????I?only?allow?50?requests?per?hour?to?this?Web?site?per
????????logged?in?user.??Try?again?soon.
很多應(yīng)用框架同樣集成了,限流功能并且在返回的Header中給出明確的限流標(biāo)識。
X-Rate-Limit-Limit:同一個時間段所允許的請求的最大數(shù)目;
X-Rate-Limit-Remaining:在當(dāng)前時間段內(nèi)剩余的請求的數(shù)量;
X-Rate-Limit-Reset:為了得到最大請求數(shù)所等待的秒數(shù)。
這是通過響應(yīng)頭告訴調(diào)用方服務(wù)端的限流頻次是怎樣的,保證后端的接口訪問上限,客戶端也可以根據(jù)響應(yīng)的Header調(diào)整請求。
限流分類
限流,拆分來看,就兩個字限和流,限就是動詞限制,很好理解。但是流在不同的場景之下就是不同資源或指標(biāo),多樣性就在流中體現(xiàn)。在網(wǎng)絡(luò)流量中可以是字節(jié)流,在數(shù)據(jù)庫中可以是TPS,在API中可以是QPS亦可以是并發(fā)請求數(shù),在商品中還可以是庫存數(shù)... ...但是不管是哪一種『流』,這個流必須可以被量化,可以被度量,可以被觀察到、可以統(tǒng)計出來。我們把限流的分類基于不同的方式分為不同的類別,如下圖。

因為篇幅有限,本文只會挑選幾個常見的類型分類進(jìn)行說明。
限流粒度分類
根據(jù)限流的粒度分類:
單機(jī)限流
分布式限流
現(xiàn)狀的系統(tǒng)基本上都是分布式架構(gòu),單機(jī)的模式已經(jīng)很少了,這里說的單機(jī)限流更加準(zhǔn)確一點的說法是單服務(wù)節(jié)點限流。單機(jī)限流是指請求進(jìn)入到某一個服務(wù)節(jié)點后超過了限流閾值,服務(wù)節(jié)點采取了一種限流保護(hù)措施。

分布式限流狹義的說法是在接入層實現(xiàn)多節(jié)點合并限流,比如NGINX+redis,分布式網(wǎng)關(guān)等,廣義的分布式限流是多個節(jié)點(可以為不同服務(wù)節(jié)點)有機(jī)整合,形成整體的限流服務(wù)。

單機(jī)限流防止流量壓垮服務(wù)節(jié)點,缺乏對整體流量的感知。分布式限流適合做細(xì)粒度不同的限流控制,可以根據(jù)場景不同匹配不同的限流規(guī)則。與單機(jī)限流最大的區(qū)別,分布式限流需要中心化存儲,常見的使用redis實現(xiàn)。引入了中心化存儲,就需要解決以下問題:
數(shù)據(jù)一致性
在限流模式中理想的模式為時間點一致性。時間點一致性的定義中要求所有數(shù)據(jù)組件的數(shù)據(jù)在任意時刻都是完全一致的,但是一般來說信息傳播的速度最大是光速,其實并不能達(dá)到任意時刻一致,總有一定的時間不一致,對于我們CAP中的一致性來說只要達(dá)到讀取到最新數(shù)據(jù)即可,達(dá)到這種情況并不需要嚴(yán)格的任意時間一致。這只能是理論當(dāng)中的一致性模型,可以在限流中達(dá)到線性一致性即可。
時間一致性
這里的時間一致性與上述的時間點一致性不一樣,這里就是指各個服務(wù)節(jié)點的時間一致性。一個集群有3臺機(jī)器,但是在某一個A/B機(jī)器的時間為
Tue Dec 3 16:29:28 CST 2019,C為Tue Dec 3 16:29:28 CST 2019,那么它們的時間就不一致。那么使用ntpdate進(jìn)行同步也會存在一定的誤差,對于時間窗口敏感的算法就是誤差點。超時
在分布式系統(tǒng)中就需要網(wǎng)絡(luò)進(jìn)行通信,會存在網(wǎng)絡(luò)抖動問題,或者分布式限流中間件壓力過大導(dǎo)致響應(yīng)變慢,甚至是超時時間閾值設(shè)置不合理,導(dǎo)致應(yīng)用服務(wù)節(jié)點超時了,此時是放行流量還是拒絕流量?
性能與可靠性
分布式限流中間件的資源總是有限的,甚至可能是單點的(寫入單點),性能存在上限。如果分布式限流中間件不可用時候如何退化為單機(jī)限流模式也是一個很好的降級方案。
限流對象類型分類
按照對象類型分類:
基于請求限流
基于資源限流
基于請求限流,一般的實現(xiàn)方式有限制總量和限制QPS。限制總量就是限制某個指標(biāo)的上限,比如搶購某一個商品,放量是10w,那么最多只能賣出10w件。微信的搶紅包,群里發(fā)一個紅包拆分為10個,那么最多只能有10人可以搶到,第十一個人打開就會顯示『手慢了,紅包派完了』。

限制QPS,也是我們常說的限流方式,只要在接口層級進(jìn)行,某一個接口只允許1秒只能訪問100次,那么它的峰值QPS只能為100。限制QPS的方式最難的點就是如何預(yù)估閾值,如何定位閾值,下文中會說到。
基于資源限流是基于服務(wù)資源的使用情況進(jìn)行限制,需要定位到服務(wù)的關(guān)鍵資源有哪些,并對其進(jìn)行限制,如限制TCP連接數(shù)、線程數(shù)、內(nèi)存使用量等。限制資源更能有效地反映出服務(wù)當(dāng)前地清理,但與限制QPS類似,面臨著如何確認(rèn)資源的閾值為多少。這個閾值需要不斷地調(diào)優(yōu),不停地實踐才可以得到一個較為滿意地值。
限流算法分類
不論是按照什么維度,基于什么方式的分類,其限流的底層均是需要算法來實現(xiàn)。下面介紹實現(xiàn)常見的限流算法:
計數(shù)器
令牌桶算法
漏桶算法
計數(shù)器
固定窗口計數(shù)器
計數(shù)限流是最為簡單的限流算法,日常開發(fā)中,我們說的限流很多都是說固定窗口計數(shù)限流算法,比如某一個接口或服務(wù)1s最多只能接收1000個請求,那么我們就會設(shè)置其限流為1000QPS。該算法的實現(xiàn)思路非常簡單,維護(hù)一個固定單位時間內(nèi)的計數(shù)器,如果檢測到單位時間已經(jīng)過去就重置計數(shù)器為零。

其操作步驟:
時間線劃分為多個獨立且固定大小窗口;
落在每一個時間窗口內(nèi)的請求就將計數(shù)器加1;
如果計數(shù)器超過了限流閾值,則后續(xù)落在該窗口的請求都會被拒絕。但時間達(dá)到下一個時間窗口時,計數(shù)器會被重置為0。
下面實現(xiàn)一個簡單的代碼。
package?limit
import?(
???"sync/atomic"
???"time"
)
type?Counter?struct?{
???Count???????uint64???//?初始計數(shù)器
???Limit???????uint64??//?單位時間窗口最大請求頻次
???Interval????int64???//?單位ms
???RefreshTime?int64???//?時間窗口
}
func?NewCounter(count,?limit?uint64,?interval,?rt?int64)?*Counter?{
???return?&Counter{
??????Count:???????count,
??????Limit:???????limit,
??????Interval:????interval,
??????RefreshTime:?rt,
???}
}
func?(c?*Counter)?RateLimit()?bool?{
???now?:=?time.Now().UnixNano()?/?1e6
???if?now?<?(c.RefreshTime?+?c.Interval)?{
??????atomic.AddUint64(&c.Count,?1)
??????return?c.Count?<=?c.Limit
???}?else?{
??????c.RefreshTime?=?now
??????atomic.AddUint64(&c.Count,?-c.Count)
??????return?true
???}
}
測試代碼:
package?limit
import?(
???"fmt"
???"testing"
???"time"
)
func?Test_Counter(t?*testing.T)?{
???counter?:=?NewCounter(0,?5,?100,?time.Now().Unix())
???for?i?:=?0;?i?<?10;?i++?{
??????go?func(i?int)?{
?????????for?k?:=?0;?k?<=?10;?k++?{
????????????fmt.Println(counter.RateLimit())
????????????if?k%3?==?0?{
???????????????time.Sleep(102?*?time.Millisecond)
????????????}
?????????}
??????}(i)
???}
???time.Sleep(10?*?time.Second)
}
看了上面的邏輯,有沒有覺得固定窗口計數(shù)器很簡單,對,就是這么簡單,這就是它的一個優(yōu)點實現(xiàn)簡單。同時也存在兩個比較嚴(yán)重缺陷。試想一下,固定時間窗口1s限流閾值為100,但是前100ms,已經(jīng)請求來了99個,那么后續(xù)的900ms只能通過一個了,就是一個缺陷,基本上沒有應(yīng)對突發(fā)流量的能力。第二個缺陷,在00:00:00這個時間窗口的后500ms,請求通過了100個,在00:00:01這個時間窗口的前500ms還有100個請求通過,對于服務(wù)來說相當(dāng)于1秒內(nèi)請求量達(dá)到了限流閾值的2倍。
滑動窗口計數(shù)器
滑動時間窗口算法是對固定時間窗口算法的一種改進(jìn),這詞被大眾所知實在TCP的流量控制中。固定窗口計數(shù)器可以說是滑動窗口計數(shù)器的一種特例,滑動窗口的操作步驟:
將單位時間劃分為多個區(qū)間,一般都是均分為多個小的時間段;
每一個區(qū)間內(nèi)都有一個計數(shù)器,有一個請求落在該區(qū)間內(nèi),則該區(qū)間內(nèi)的計數(shù)器就會加一;
每過一個時間段,時間窗口就會往右滑動一格,拋棄最老的一個區(qū)間,并納入新的一個區(qū)間;
計算整個時間窗口內(nèi)的請求總數(shù)時會累加所有的時間片段內(nèi)的計數(shù)器,計數(shù)總和超過了限制數(shù)量,則本窗口內(nèi)所有的請求都被丟棄。
時間窗口劃分的越細(xì),并且按照時間"滑動",這種算法避免了固定窗口計數(shù)器出現(xiàn)的上述兩個問題。缺點是時間區(qū)間的精度越高,算法所需的空間容量就越大。
常見的實現(xiàn)方式主要有基于redis zset的方式和循環(huán)隊列實現(xiàn)?;趓edis zset可將Key為限流標(biāo)識ID,Value保持唯一,可以用UUID生成,Score 也記為同一時間戳,最好是納秒級的。使用redis提供的 ZADD、EXPIRE、ZCOUNT 和 zremrangebyscore 來實現(xiàn),并同時注意開啟 Pipeline 來盡可能提升性能。實現(xiàn)很簡單,但是缺點就是zset的數(shù)據(jù)結(jié)構(gòu)會越來越大。
漏桶算法
漏桶算法是水先進(jìn)入到漏桶里,漏桶再以一定的速率出水,當(dāng)流入水的數(shù)量大于流出水時,多余的水直接溢出。把水換成請求來看,漏桶相當(dāng)于服務(wù)器隊列,但請求量大于限流閾值時,多出來的請求就會被拒絕服務(wù)。漏桶算法使用隊列實現(xiàn),可以以固定的速率控制流量的訪問速度,可以做到流量的“平整化”處理。
大家可以通過網(wǎng)上最流行的一張圖來理解。

漏桶算法實現(xiàn)步驟:
將每個請求放入固定大小的隊列進(jìn)行存儲;
隊列以固定速率向外流出請求,如果隊列為空則停止流出;
如隊列滿了則多余的請求會被直接拒絕·
漏桶算法有一個明顯的缺陷:當(dāng)短時間內(nèi)有大量的突發(fā)請求時,即使服務(wù)器負(fù)載不高,每個請求也都得在隊列中等待一段時間才能被響應(yīng)。
令牌桶算法
令牌桶算法的原理是系統(tǒng)會以一個恒定的速率往桶里放入令牌,而如果請求需要被處理,則需要先從桶里獲取一個令牌,當(dāng)桶里沒有令牌可取時,則拒絕服務(wù)。從原理上看,令牌桶算法和漏桶算法是相反的,前者為“進(jìn)”,后者為“出”。
漏桶算法與令牌桶算法除了“方向”上的不同還有一個更加主要的區(qū)別:令牌桶算法限制的是平均流入速率(允許突發(fā)請求,只要有足夠的令牌,支持一次拿多個令牌),并允許一定程度突發(fā)流量;
令牌桶算法的實現(xiàn)步驟:
令牌以固定速率生成并放入到令牌桶中;
如果令牌桶滿了則多余的令牌會直接丟棄,當(dāng)請求到達(dá)時,會嘗試從令牌桶中取令牌,取到了令牌的請求可以執(zhí)行;
如果桶空了,則拒絕該請求。

四種策略該如何選擇?
固定窗口:實現(xiàn)簡單,但是過于粗暴,除非情況緊急,為了能快速止損眼前的問題可以作為臨時應(yīng)急的方案。
滑動窗口:限流算法簡單易實現(xiàn),可以應(yīng)對有少量突增流量場景。
漏桶:對于流量絕對均勻有很強的要求,資源的利用率上不是極致,但其寬進(jìn)嚴(yán)出模式,保護(hù)系統(tǒng)的同時還留有部分余量,是一個通用性方案。
令牌桶:系統(tǒng)經(jīng)常有突增流量,并盡可能的壓榨服務(wù)的性能。
怎么做限流?
不論使用上述的哪一種分類或者實現(xiàn)方式,系統(tǒng)都會面臨一個共同的問題:如何確認(rèn)限流閾值。有人團(tuán)隊根據(jù)經(jīng)驗先設(shè)定一個小的閾值,后續(xù)慢慢進(jìn)行調(diào)整;有的團(tuán)隊是通過進(jìn)行壓力測試后總結(jié)出來。這種方式的問題在于壓測模型與線上環(huán)境不一定一致,接口的單壓不能反饋整個系統(tǒng)的狀態(tài),全鏈路壓測又難以真實反應(yīng)實際流量場景流量比例。
再換一個思路是通過壓測+各應(yīng)用監(jiān)控數(shù)據(jù)。根據(jù)系統(tǒng)峰值的QPS與系統(tǒng)資源使用情況,進(jìn)行等水位放大預(yù)估限流閾值,問題在于系統(tǒng)性能拐點未知,單純的預(yù)測不一定準(zhǔn)確甚至極大偏離真實場景。正如《Overload Control for Scaling WeChat Microservices》所說,在具有復(fù)雜依賴關(guān)系的系統(tǒng)中,對特定服務(wù)的進(jìn)行過載控制可能對整個系統(tǒng)有害或者服務(wù)的實現(xiàn)有缺陷。
希望后續(xù)可以出現(xiàn)一個更加AI的運行反饋自動設(shè)置限流閾值的系統(tǒng),可以根據(jù)當(dāng)前QPS、資源狀態(tài)、RT情況等多種關(guān)聯(lián)數(shù)據(jù)動態(tài)地進(jìn)行過載保護(hù)。
不論是哪一種方式給出的限流閾值,系統(tǒng)都應(yīng)該關(guān)注以下幾點:
運行指標(biāo)狀態(tài),比如當(dāng)前服務(wù)的QPS、機(jī)器資源使用情況、數(shù)據(jù)庫的連接數(shù)、線程的并發(fā)數(shù)等;
資源間的調(diào)用關(guān)系,外部鏈路請求、內(nèi)部服務(wù)之間的關(guān)聯(lián)、服務(wù)之間的強弱依賴等;
控制方式,達(dá)到限流后對后續(xù)的請求直接拒絕、快速失敗、排隊等待等處理方式
go限流類庫使用
限流的類庫有很多,不同語言的有不同的類庫,如大Java的有concurrency-limits、Sentinel、Guava 等,這些類庫都有很多的分析和使用方式了,本文主要介紹Golang的限流類庫就是Golang的擴(kuò)展庫:
https://github.com/golang/time/rate
可以進(jìn)去語言類庫的代碼都值得去研讀一番,學(xué)習(xí)過Java的同學(xué)是否對AQS的設(shè)計之精妙而感嘆呢!?time/rate?也有其精妙的部分,下面開始進(jìn)入類庫學(xué)習(xí)階段。
github.com/golang/time/rate
進(jìn)行源碼分析前的,最應(yīng)該做的是了解類庫的使用方式、使用場景和API。對業(yè)務(wù)有了初步的了解,閱讀代碼就可以事半功倍。因為篇幅有限后續(xù)的博文在對多個限流類庫源碼做分析。
類庫的API文檔:
https://godoc.org/golang.org/x/time/rate%E3%80%82
time/rate類庫是基于令牌桶算法實現(xiàn)的限流功能。前面說令牌桶算法的原理是系統(tǒng)會以一個恒定的速率往桶里放入令牌,那么桶就有一個固定的大小,往桶中放入令牌的速率也是恒定的,并且允許突發(fā)流量。查看文檔發(fā)現(xiàn)一個函數(shù):
func?NewLimiter(r?Limit,?b?int)?*Limiter
newLimiter返回一個新的限制器,它允許事件的速率達(dá)到r,并允許最多突發(fā)b個令牌。也就是說Limter限制時間的發(fā)生頻率,但這個桶一開始容量就為b,并且裝滿b個令牌(令牌池中最多有b個令牌,所以一次最多只能允許b個事件發(fā)生,一個事件花費掉一個令牌),然后每一個單位時間間隔(默認(rèn)1s)往桶里放入r個令牌。
limter?:=?rate.NewLimiter(10,?5)
上面的例子表示,令牌桶的容量為5,并且每一秒中就往桶里放入10個令牌。細(xì)心的讀者都會發(fā)現(xiàn)函數(shù)NewLimiter第一個參數(shù)是Limit類型,可以看源碼就會發(fā)現(xiàn)Limit實際上就是float64的別名。
//?Limit?defines?the?maximum?frequency?of?some?events.
//?Limit?is?represented?as?number?of?events?per?second.
//?A?zero?Limit?allows?no?events.
type?Limit?float64
限流器還可以指定往桶里放入令牌的時間間隔,實現(xiàn)方式如下:
limter?:=?rate.NewLimiter(rate.Every(100*time.Millisecond),?5)
這兩個例子的效果是一樣的,使用第一種方式不會出現(xiàn)在每一秒間隔一下子放入10個令牌,也是均勻分散在100ms的間隔放入令牌。rate.Limiter提供了三類方法用來限速:
Allow/AllowN
Wait/WaitN
Reserve/ReserveN
下面對比這三類限流方式的使用方式和適用場景。先看第一類方法:
func?(lim?*Limiter)?Allow()?bool
func?(lim?*Limiter)?AllowN(now?time.Time,?n?int)?bool
Allow 是AllowN(time.Now(), 1)的簡化方法。那么重點就在方法 AllowN上了,API的解釋有點抽象,說得云里霧里的,可以看看下面的API文檔解釋:
AllowN?reports?whether?n?events?may?happen?at?time?now.?
Use?this?method?if?you?intend?to?drop?/?skip?events?that?exceed?the?rate?limit.?
Otherwise?use?Reserve?or?Wait.
實際上就是為了說,方法 AllowN在指定的時間時是否可以從令牌桶中取出N個令牌。也就意味著可以限定N個事件是否可以在指定的時間同時發(fā)生。這個兩個方法是無阻塞,也就是說一旦不滿足,就會跳過,不會等待令牌數(shù)量足夠才執(zhí)行。
也就是文檔中的第二行解釋,如果打算丟失或跳過超出速率限制的時間,那么久請使用該方法。比如使用之前實例化好的限流器,在某一個時刻,服務(wù)器同時收到超過了8個請求,如果令牌桶內(nèi)令牌小于8個,那么這8個請求就會被丟棄。一個小示例:
func?AllowDemo()?{
???limter?:=?rate.NewLimiter(rate.Every(200*time.Millisecond),?5)
???i?:=?0
???for?{
??????i++
??????if?limter.Allow()?{
?????????fmt.Println(i,?"====Allow======",?time.Now())
??????}?else?{
?????????fmt.Println(i,?"====Disallow======",?time.Now())
??????}
??????time.Sleep(80?*?time.Millisecond)
??????if?i?==?15?{
?????????return
??????}
???}
}
執(zhí)行結(jié)果:
1?====Allow======?2019-12-14?15:54:09.9852178?+0800?CST?m=+0.005998001
2?====Allow======?2019-12-14?15:54:10.1012231?+0800?CST?m=+0.122003301
3?====Allow======?2019-12-14?15:54:10.1823056?+0800?CST?m=+0.203085801
4?====Allow======?2019-12-14?15:54:10.263238?+0800?CST?m=+0.284018201
5?====Allow======?2019-12-14?15:54:10.344224?+0800?CST?m=+0.365004201
6?====Allow======?2019-12-14?15:54:10.4242458?+0800?CST?m=+0.445026001
7?====Allow======?2019-12-14?15:54:10.5043101?+0800?CST?m=+0.525090301
8?====Allow======?2019-12-14?15:54:10.5852232?+0800?CST?m=+0.606003401
9?====Disallow======?2019-12-14?15:54:10.6662181?+0800?CST?m=+0.686998301
10?====Disallow======?2019-12-14?15:54:10.7462189?+0800?CST?m=+0.766999101
11?====Allow======?2019-12-14?15:54:10.8272182?+0800?CST?m=+0.847998401
12?====Disallow======?2019-12-14?15:54:10.9072192?+0800?CST?m=+0.927999401
13?====Allow======?2019-12-14?15:54:10.9872224?+0800?CST?m=+1.008002601
14?====Disallow======?2019-12-14?15:54:11.0672253?+0800?CST?m=+1.088005501
15?====Disallow======?2019-12-14?15:54:11.1472946?+0800?CST?m=+1.168074801
第二類方法:因為ReserveN比較復(fù)雜,第二類先說WaitN。
func?(lim?*Limiter)?Wait(ctx?context.Context)?(err?error)
func?(lim?*Limiter)?WaitN(ctx?context.Context,?n?int)?(err?error)
類似Wait 是WaitN(ctx, 1)的簡化方法。與AllowN不同的是WaitN會阻塞,如果令牌桶內(nèi)的令牌數(shù)不足N個,WaitN會阻塞一段時間,阻塞時間的時長可以用第一個參數(shù)ctx進(jìn)行設(shè)置,把 context 實例為context.WithDeadline或context.WithTimeout進(jìn)行制定阻塞的時長。
func?WaitNDemo()?{
???limter?:=?rate.NewLimiter(10,?5)
???i?:=?0
???for?{
??????i++
??????ctx,?canle?:=?context.WithTimeout(context.Background(),?400*time.Millisecond)
??????if?i?==?6?{
?????????//?取消執(zhí)行
?????????canle()
??????}
??????err?:=?limter.WaitN(ctx,?4)
??????if?err?!=?nil?{
?????????fmt.Println(err)
?????????continue
??????}
??????fmt.Println(i,?",執(zhí)行:",?time.Now())
??????if?i?==?10?{
?????????return
??????}
???}
}
執(zhí)行結(jié)果:
1 ,執(zhí)行:2019-12-14 15:45:15.538539 +0800 CST m=+0.011023401
2 ,執(zhí)行:2019-12-14 15:45:15.8395195 +0800 CST m=+0.312003901
3 ,執(zhí)行:2019-12-14 15:45:16.2396051 +0800 CST m=+0.712089501
4 ,執(zhí)行:2019-12-14 15:45:16.6395169 +0800 CST m=+1.112001301
5 ,執(zhí)行:2019-12-14 15:45:17.0385893 +0800 CST m=+1.511073701
context?canceled
7 ,執(zhí)行:2019-12-14 15:45:17.440514 +0800 CST m=+1.912998401
8 ,執(zhí)行:2019-12-14 15:45:17.8405152 +0800 CST m=+2.312999601
9 ,執(zhí)行:2019-12-14 15:45:18.2405402 +0800 CST m=+2.713024601
10 ,執(zhí)行:2019-12-14 15:45:18.6405179 +0800 CST m=+3.113002301
適用于允許阻塞等待的場景,比如消費消息隊列的消息,可以限定最大的消費速率,過大了就會被限流避免消費者負(fù)載過高。
第三類方法:
func?(lim?*Limiter)?Reserve()?*Reservation
func?(lim?*Limiter)?ReserveN(now?time.Time,?n?int)?*Reservation
與之前的兩類方法不同的是Reserve/ReserveN返回了Reservation實例。Reservation在API文檔中有5個方法:
func?(r?*Reservation)?Cancel()?//?相當(dāng)于CancelAt(time.Now())
func?(r?*Reservation)?CancelAt(now?time.Time)
func?(r?*Reservation)?Delay()?time.Duration?//?相當(dāng)于DelayFrom(time.Now())
func?(r?*Reservation)?DelayFrom(now?time.Time)?time.Duration
func?(r?*Reservation)?OK()?bool
通過這5個方法可以讓開發(fā)者根據(jù)業(yè)務(wù)場景進(jìn)行操作,相比前兩類的自動化,這樣的操作顯得復(fù)雜多了。通過一個小示例來學(xué)習(xí)Reserve/ReserveN:
func?ReserveNDemo()?{
???limter?:=?rate.NewLimiter(10,?5)
???i?:=?0
???for?{
??????i++
??????reserve?:=?limter.ReserveN(time.Now(),?4)
??????//?如果為flase說明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場景
??????if?!reserve.OK()?{
?????????return
??????}
??????ts?:=?reserve.Delay()
??????time.Sleep(ts)
??????fmt.Println("執(zhí)行:",?time.Now(),ts)
??????if?i?==?10?{
?????????return
??????}
???}
}
執(zhí)行結(jié)果:
執(zhí)行:2019-12-14 16:22:26.6446468 +0800 CST m=+0.008000201 0s
執(zhí)行:2019-12-14 16:22:26.9466454 +0800 CST m=+0.309998801 247.999299ms
執(zhí)行:2019-12-14 16:22:27.3446473 +0800 CST m=+0.708000701 398.001399ms
執(zhí)行:2019-12-14 16:22:27.7456488 +0800 CST m=+1.109002201 399.999499ms
執(zhí)行:2019-12-14 16:22:28.1456465 +0800 CST m=+1.508999901 398.997999ms
執(zhí)行:2019-12-14 16:22:28.5456457 +0800 CST m=+1.908999101 399.0003ms
執(zhí)行:2019-12-14 16:22:28.9446482 +0800 CST m=+2.308001601 399.001099ms
執(zhí)行:2019-12-14 16:22:29.3446524 +0800 CST m=+2.708005801 399.998599ms
執(zhí)行:2019-12-14 16:22:29.7446514 +0800 CST m=+3.108004801 399.9944ms
執(zhí)行:2019-12-14 16:22:30.1446475 +0800 CST m=+3.508000901 399.9954ms
如果在執(zhí)行Delay()之前操作Cancel()那么返回的時間間隔就會為0,意味著可以立即執(zhí)行操作,不進(jìn)行限流。
func?ReserveNDemo2()?{
???limter?:=?rate.NewLimiter(5,?5)
???i?:=?0
???for?{
??????i++
??????reserve?:=?limter.ReserveN(time.Now(),?4)
??????//?如果為flase說明拿不到指定數(shù)量的令牌,比如需要的令牌數(shù)大于令牌桶容量的場景
??????if?!reserve.OK()?{
?????????return
??????}
??????if?i?==?6?||?i?==?5?{
?????????reserve.Cancel()
??????}
??????ts?:=?reserve.Delay()
??????time.Sleep(ts)
??????fmt.Println(i,?"執(zhí)行:",?time.Now(),?ts)
??????if?i?==?10?{
?????????return
??????}
???}
}
執(zhí)行結(jié)果:
1 執(zhí)行:2019-12-14 16:25:45.7974857 +0800 CST m=+0.007005901 0s
2 執(zhí)行:2019-12-14 16:25:46.3985135 +0800 CST m=+0.608033701 552.0048ms
3 執(zhí)行:2019-12-14 16:25:47.1984796 +0800 CST m=+1.407999801 798.9722ms
4 執(zhí)行:2019-12-14 16:25:47.9975269 +0800 CST m=+2.207047101 799.0061ms
5 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 799.9588ms
6 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
7 執(zhí)行:2019-12-14 16:25:48.7994803 +0800 CST m=+3.009000501 0s
8 執(zhí)行:2019-12-14 16:25:49.5984782 +0800 CST m=+3.807998401 798.0054ms
9 執(zhí)行:2019-12-14 16:25:50.3984779 +0800 CST m=+4.607998101 799.0075ms
10?執(zhí)行:2019-12-14 16:25:51.1995131 +0800 CST m=+5.409033301 799.0078ms
看到這里time/rate的限流方式已經(jīng)完成,除了上述的三類限流方式,time/rate還提供了動態(tài)調(diào)整限流器參數(shù)的功能。相關(guān)API如下:
func?(lim?*Limiter)?SetBurst(newBurst?int)?//?相當(dāng)于SetBurstAt(time.Now(),?newBurst).
func?(lim?*Limiter)?SetBurstAt(now?time.Time,?newBurst?int)//?重設(shè)令牌桶的容量
func?(lim?*Limiter)?SetLimit(newLimit?Limit)?//?相當(dāng)于SetLimitAt(time.Now(),?newLimit)
func?(lim?*Limiter)?SetLimitAt(now?time.Time,?newLimit?Limit)//?重設(shè)放入令牌的速率
這四個方法可以讓程序根據(jù)自身的狀態(tài)動態(tài)的調(diào)整令牌桶速率和令牌桶容量。
結(jié)尾
通過上述一系列講解,相信大家對各個限流的應(yīng)用場景和優(yōu)缺點也有了大致的掌握,希望在日常開發(fā)中有所幫助。限流僅僅是整個服務(wù)治理中的一個小環(huán)節(jié),需要與多種技術(shù)結(jié)合使用,才可以更好的提升服務(wù)的穩(wěn)定性的同時提高用戶體驗。
推薦文章
原創(chuàng)電子書
歷時整整一年總結(jié)的?Java面試+ Java入門技術(shù)學(xué)習(xí)指南,這是本人這幾年及校招的總結(jié),各種異步面試題已經(jīng)全部進(jìn)行總結(jié),按照章節(jié)復(fù)習(xí)即可,已經(jīng)拿到了了大廠提供。
原創(chuàng)思維導(dǎo)圖
掃碼或者微信搜?程序員的技術(shù)圈子?回復(fù)?面試?領(lǐng)取原創(chuàng)電子書和思維導(dǎo)圖。



