限流器是提高服務穩定性的很是重要的組件,能夠用來限制請求速率,保護服務,以避免服務過載。 限流器的實現方法有不少種,常見的限流算法有固定窗口、滑動窗口、漏桶、令牌桶,我在前面的文章**「經常使用限流算法的應用場景和實現原理」** 中給你們講解了這幾種限流方法自身的特色和應用場景,其中令牌桶在限流的同時還能夠應對必定的突發流量,與互聯網應用容易由於熱點事件出現突發流量高峯的特色更契合。golang
簡單來講,令牌桶就是想象有一個固定大小的桶,系統會以恆定速率向桶中放 Token,桶滿則暫時不放。在請求比較的少的時候桶能夠先"攢"一些Token,應對突發的流量,若是桶中有剩餘 Token 就能夠一直取。若是沒有剩餘 Token,則須要等到桶中被放置了 Token 才行。算法
關於令牌桶限流更詳細的解釋請參考文章:經常使用限流算法的應用場景和實現原理數組
有的同窗在看明白令牌桶的原理後就很是想去本身實現一個限流器應用到本身的項目裏,em... 怎麼說呢,造個輪子確實有利於本身水平提升,不過要是應用到商用項目裏的話其實大可沒必要本身去造輪子,Golang官方已經替咱們造好輪子啦 ......~!微信
Golang 官方提供的擴展庫裏就自帶了限流算法的實現,即 golang.org/x/time/rate
。該限流器也是基於 Token Bucket(令牌桶) 實現的。markdown
time/rate
包的Limiter
類型對限流器進行了定義,全部限流功能都是經過基於Limiter
類型實現的,其內部結構以下:函數
type Limiter struct {
mu sync.Mutex
limit Limit
burst int // 令牌桶的大小
tokens float64
last time.Time // 上次更新tokens的時間
lastEvent time.Time // 上次發生限速器事件的時間(經過或者限制都是限速器事件)
}
複製代碼
其主要字段的做用是:oop
limit
字段表示往桶裏放Token的速率,它的類型是Limit,是int64的類型別名。設置limit
時既能夠用數字指定每秒向桶中放多少個Token,也能夠指定向桶中放Token的時間間隔,其實指定了每秒放Token的個數後就能計算出放每一個Token的時間間隔了。能夠看到在 timer/rate
的限流器實現中,並無單獨維護一個 Timer 和隊列去真的每隔一段時間向桶中放令牌,而是僅僅經過計數的方式表示桶中剩餘的令牌。每次消費取 Token 以前會先根據上次更新令牌數的時間差更新桶中Token數。spa
大概瞭解了time/rate
限流器的內部實現後,下面的內容咱們會集中介紹下該組件的具體使用方法:code
咱們可使用如下方法構造一個限流器對象:orm
limiter := rate.NewLimiter(10, 100);
複製代碼
這裏有兩個參數:
r Limit
,設置的是限流器Limiter的limit
字段,表明每秒能夠向 Token 桶中產生多少 token。Limit 其實是 float64 的別名。b int
,b 表明 Token 桶的容量大小,也就是設置的限流器 Limiter 的burst
字段。那麼,對於以上例子來講,其構造出的限流器的令牌桶大小爲 100, 以每秒 10 個 Token 的速率向桶中放置 Token。
除了給r Limit
參數直接指定每秒產生的 Token 個數外,還能夠用 Every 方法來指定向桶中放置 Token 的間隔,例如:
limit := rate.Every(100 * time.Millisecond);
limiter := rate.NewLimiter(limit, 100);
複製代碼
以上就表示每 100ms 往桶中放一個 Token。本質上也是一秒鐘往桶裏放 10 個。
Limiter 提供了三類方法供程序消費 Token,能夠每次消費一個 Token,也能夠一次性消費多個 Token。 每種方法表明了當 Token 不足時,各自不一樣的對應手段,能夠阻塞等待桶中Token補充,也能夠直接返回取Token失敗。
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)
複製代碼
Wait 實際上就是 WaitN(ctx,1)
。
當使用 Wait 方法消費 Token 時,若是此時桶內 Token 數組不足 (小於 N),那麼 Wait 方法將會阻塞一段時間,直至 Token 知足條件。若是充足則直接返回。
這裏能夠看到,Wait 方法有一個 context 參數。 咱們能夠設置 context 的 Deadline 或者 Timeout,來決定這次 Wait 的最長時間。
// 一直等到獲取到桶中的令牌
err := limiter.Wait(context.Background())
if err != nil {
fmt.Println("Error: ", err)
}
// 設置一秒的等待超時時間
ctx, _ := context.WithTimeout(context.Background(), time.Second * 1)
err := limiter.Wait(ctx)
if err != nil {
fmt.Println("Error: ", err)
}
複製代碼
func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool
複製代碼
Allow 實際上就是對 AllowN(time.Now(),1)
進行簡化的函數。
AllowN 方法表示,截止到某一時刻,目前桶中數目是否至少爲 n 個,知足則返回 true,同時從桶中消費 n 個 token。反之不消費桶中的Token,返回false。
對應線上的使用場景是,若是請求速率超過限制,就直接丟棄超頻後的請求。
if limiter.AllowN(time.Now(), 2) {
fmt.Println("event allowed")
} else {
fmt.Println("event not allowed")
}
複製代碼
func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation
複製代碼
Reserve 至關於 ReserveN(time.Now(), 1)
。
ReserveN 的用法就相對來講複雜一些,當調用完成後,不管 Token 是否充足,都會返回一個 *Reservation
對象。你能夠調用該對象的Delay()
方法,該方法返回的參數類型爲time.Duration
,反映了須要等待的時間,必須等到等待時間以後,才能進行接下來的工做。若是不想等待,能夠調用Cancel()
方法,該方法會將 Token 歸還。
舉一個簡單的例子,咱們能夠這麼使用 Reserve 方法。
r := limiter.Reserve()
f !r.OK() {
// Not allowed to act! Did you remember to set lim.burst to be > 0 ?
return
}
time.Sleep(r.Delay())
Act() // 執行相關邏輯
複製代碼
Limiter 支持建立後動態調整速率和桶大小:
有了這兩個方法,能夠根據現有環境和條件以及咱們的需求,動態地改變 Token 桶大小和速率。
今天咱們總結了 Golang 官方限流器的使用方法,它是一種令牌桶算實現的限流器。其中 Wait/WaitN,Allow/AllowN 這兩組方法在平時用的比較多,前者是消費Token時若是桶中Token不足可讓程序等待桶中新Token的放入(最好設置上等待時長)後者則是在桶中的Token不足時選擇直接丟棄請求。
除了Golang官方提供的限流器實現,Uber公司開源的限流器uber-go/ratelimit
也是一個很好的選擇,與Golang官方限流器不一樣的是Uber的限流器是經過漏桶算法實現的,不過對傳統的漏桶算法進行了改良,有興趣的同窗能夠自行去體驗一下。
今天的文章就到這裏啦,若是喜歡個人文章就幫我點個贊吧,我會每週經過技術文章分享個人所學所見和第一手實踐經驗,感謝你的支持。微信搜索關注公衆號「網管叨bi叨」 每週教會你一個進階知識。