關於我
go-rate是速率限制器庫,基於 Token Bucket(令牌桶)演算法實現。 go-rate被用在LangTrend的生產中 用於遵守GitHub API速率限制。
速率限制可以完成一些特殊的功能需求,包括但不限於伺服器端垃圾郵件保護、防止api呼叫飽和等。
庫使用說明
構造限流器
我們首先構造一個限流器物件:
limiter := NewLimiter(10, 1);
這裡有兩個引數:
第一個引數是
r Limit
。代表每秒可以向 Token 桶中產生多少 token。Limit 實際上是 float64 的別名。第二個引數是
b int
。b 代表 Token 桶的容量大小。
上述的限流器的含義是:擁有一個容量為1的令牌桶,以每鈔10個的速度向桶中放令牌。
除了直接指定每秒產生的 Token 個數外,還可以用 Every 方法來指定向 Token 桶中放置 Token 的間隔,例如:
limiter := NewLimiter(Every(100 * time.Millisecond), 1);
以上就表示每 100ms 往桶中放一個 Token。本質上也就是一秒鐘產生 10 個。
消費令牌Token
Limiter 提供了三類方法供使用者消費 Token,使用者可以每次消費一個 Token,也可以一次性消費多個 Token。 而每種方法代表了當 Token 不足時,各自不同的對應手段。
Wait/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)
。
當使用 Wait 方法消費 Token 時,如果此時桶內 Token 陣列不足 (小於 N),那麼 Wait 方法將會阻塞一段時間,直至 Token 滿足條件。如果充足則直接返回。
這裡可以看到,Wait 方法有一個 context 引數。我們可以設定 context 的 Deadline 或者 Timeout,來決定此次 Wait 的最長時間。
Allow/AllowN
Allow 實際上就是 AllowN(time.Now(),1)
。
AllowN 方法表示,截止到某一時刻,目前桶中數目是否至少為 n 個,滿足則返回 true,同時從桶中消費 n 個 token。 反之返回不消費 Token,false。
通常對應這樣的線上場景,如果請求速率過快,就直接丟到某些請求。
Reserve/ReserveN
Reserve 相當於 ReserveN(time.Now(), 1)
。
ReserveN 的用法就相對來說複雜一些,當呼叫完成後,無論 Token 是否充足,都會返回一個 Reservation * 物件。
你可以呼叫該物件的 Delay() 方法,該方法返回了需要等待的時間。如果等待時間為 0,則說明不用等待。必須等到等待時間之後,才能進行接下來的工作。
或者,如果不想等待,可以呼叫 Cancel() 方法,該方法會將 Token 歸還。
使用一個虛擬碼來舉例,我們可以如何使用 Reserve 方法。
r := lim.Reserve()
//是否願意等待
f !r.OK() {
//不願意等待直接退出
return
}
//如果願意等待,將等待時間拋給使用者 time.Sleep代表使用者需要等待的時間。
time.Sleep(r.Delay())
Act() // 一段時間後生成生成新的令牌,開始執行相關邏輯
動態調整速率
Limiter 支援可以調整速率和桶大小:
SetLimit(Limit) 改變放入 Token 的速率
SetBurst(int) 改變 Token 桶大小
有了這兩個方法,可以根據現有環境和條件以及我們的需求,動態地改變 Token 桶大小和速率。
案例1-單位時間只允許一次郵件傳送操作
客戶端軟體客戶點選傳送郵件,如果客戶一秒鐘內點選10次,就會傳送10次,這明顯是不合適的。如果使用速率限制,我們就可以限制一秒內只能傳送一次,實現方法為:
(令牌桶)容量為1,速度為每一秒生成一個令牌,這樣可以保證一秒鐘只會被執行一次,虛擬碼實現如下
//初始化 limiter 每秒生成1個令牌,令牌桶容量為20
limiter := rate.NewLimiter(rate.Every(time.Second), 1)
//模擬單位時間執行多次操作
for i := 0; i < 5; i++ {
if limiter.Allow() {
fmt.Println(“傳送郵件”)
} else {
fmt.Println(“請求多次,過濾”)
}
}
if limiter.Allow() {
fmt.Println(“傳送郵件”)
}
執行結果
傳送郵件 請求多次,過濾 請求多次,過濾 請求多次,過濾 請求多次,過濾 傳送郵件
我們發現,第一次執行是可以被允許的因為第一次的令牌被允許,之後的請求失敗是因為還沒有生成新的令牌,所以需要等待1秒,之後又可以進行傳送郵件操作。
通過這樣一個案例,相信大家對令牌桶的實現場景有了一個基本的瞭解。
案例2——令牌取出單個和多個
初始化令牌桶容量為20,設定每100毫秒生成一個令牌,即1秒生產10個令牌。編碼測試功能
//初始化 limiter 每秒10個令牌,令牌桶容量為20
limiter := rate.NewLimiter(rate.Every(time.Millisecond100), 20)
for i := 0; i < 25; i++ {
if limiter.Allow() {
fmt.Println(“success”) //do something
} else {
fmt.Println(“busy”)
}
}
//阻塞直到獲取足夠的令牌或者上下文取消
ctx, _ := context.WithTimeout(context.Background(), time.Second2)
fmt.Println(“start get token”, time.Now())
err := limiter.WaitN(ctx, 20)
if err != nil {
fmt.Println(“error”, err)
return
}
fmt.Println(“success get token”, time.Now())
第二段編碼阻塞的場景在於,一次性取出20個令牌給予2秒的等待時間,如果有20個令牌可以取出列印成功訊息,如果2秒等待時間內沒有20個令牌可以取出,程式直接退出,即失敗。
參考
END
歡迎關注公眾號 程式設計師工具集 致力於分享優秀的開源專案、學習資源 、常用工具
回覆關鍵詞“關注禮包”,送你一份最全的程式設計師技能圖譜。
本作品採用《CC 協議》,轉載必須註明作者和本文連結