詳解go-zero如何實(shí)現(xiàn)計(jì)數(shù)器限流
上一篇文章 go-zero 是如何做路由管理的? 介紹了路由管理,這篇文章來(lái)說(shuō)說(shuō)限流,主要介紹計(jì)數(shù)器限流算法,具體的代碼實(shí)現(xiàn),我們還是來(lái)分析微服務(wù)框架 go-zero 的源碼。
在微服務(wù)架構(gòu)中,一個(gè)服務(wù)可能需要頻繁地與其他服務(wù)交互,而過(guò)多的請(qǐng)求可能導(dǎo)致性能下降或系統(tǒng)崩潰。為了確保系統(tǒng)的穩(wěn)定性和高可用性,限流算法應(yīng)運(yùn)而生。
限流算法允許在給定時(shí)間段內(nèi),對(duì)服務(wù)的請(qǐng)求流量進(jìn)行控制和調(diào)整,以防止資源耗盡和服務(wù)過(guò)載。
計(jì)數(shù)器限流算法主要有兩種實(shí)現(xiàn)方式,分別是:
- 固定窗口計(jì)數(shù)器
- 滑動(dòng)窗口計(jì)數(shù)器
下面分別來(lái)介紹。
固定窗口計(jì)數(shù)器
算法概念如下:
- 將時(shí)間劃分為多個(gè)窗口;
- 在每個(gè)窗口內(nèi)每有一次請(qǐng)求就將計(jì)數(shù)器加一;
- 如果計(jì)數(shù)器超過(guò)了限制數(shù)量,則本窗口內(nèi)所有的請(qǐng)求都被丟棄當(dāng)時(shí)間到達(dá)下一個(gè)窗口時(shí),計(jì)數(shù)器重置。

固定窗口計(jì)數(shù)器是最為簡(jiǎn)單的算法,但這個(gè)算法有時(shí)會(huì)讓通過(guò)請(qǐng)求量允許為限制的兩倍。

考慮如下情況:限制 1 秒內(nèi)最多通過(guò) 5 個(gè)請(qǐng)求,在第一個(gè)窗口的最后半秒內(nèi)通過(guò)了 5 個(gè)請(qǐng)求,第二個(gè)窗口的前半秒內(nèi)又通過(guò)了 5 個(gè)請(qǐng)求。這樣看來(lái)就是在 1 秒內(nèi)通過(guò)了 10 個(gè)請(qǐng)求。
滑動(dòng)窗口計(jì)數(shù)器
算法概念如下:
- 將時(shí)間劃分為多個(gè)區(qū)間;
- 在每個(gè)區(qū)間內(nèi)每有一次請(qǐng)求就將計(jì)數(shù)器加一維持一個(gè)時(shí)間窗口,占據(jù)多個(gè)區(qū)間;
- 每經(jīng)過(guò)一個(gè)區(qū)間的時(shí)間,則拋棄最老的一個(gè)區(qū)間,并納入最新的一個(gè)區(qū)間;
- 如果當(dāng)前窗口內(nèi)區(qū)間的請(qǐng)求計(jì)數(shù)總和超過(guò)了限制數(shù)量,則本窗口內(nèi)所有的請(qǐng)求都被丟棄。

滑動(dòng)窗口計(jì)數(shù)器是通過(guò)將窗口再細(xì)分,并且按照時(shí)間滑動(dòng),這種算法避免了固定窗口計(jì)數(shù)器帶來(lái)的雙倍突發(fā)請(qǐng)求,但時(shí)間區(qū)間的精度越高,算法所需的空間容量就越大。
go-zero 實(shí)現(xiàn)
go-zero 實(shí)現(xiàn)的是固定窗口的方式,計(jì)算一段時(shí)間內(nèi)對(duì)同一個(gè)資源的訪問(wèn)次數(shù),如果超過(guò)指定的 limit,則拒絕訪問(wèn)。當(dāng)然如果在一段時(shí)間內(nèi)訪問(wèn)不同的資源,每一個(gè)資源訪問(wèn)量都不超過(guò) limit,此種情況是不會(huì)拒絕的。
而在一個(gè)分布式系統(tǒng)中,存在多個(gè)微服務(wù)提供服務(wù)。所以當(dāng)瞬間的流量同時(shí)訪問(wèn)同一個(gè)資源,如何讓計(jì)數(shù)器在分布式系統(tǒng)中正常計(jì)數(shù)?
這里要解決的一個(gè)主要問(wèn)題就是計(jì)算的原子性,保證多個(gè)計(jì)算都能得到正確結(jié)果。
通過(guò)以下兩個(gè)方面來(lái)解決:
- 使用 redis 的
incrby做資源訪問(wèn)計(jì)數(shù) - 采用 lua script 做整個(gè)窗口計(jì)算,保證計(jì)算的原子性
接下來(lái)先看一下 lua script 的源碼:
//?core/limit/periodlimit.go
const?periodScript?=?`local?limit?=?tonumber(ARGV[1])
local?window?=?tonumber(ARGV[2])
local?current?=?redis.call("INCRBY",?KEYS[1],?1)
if?current?==?1?then
????redis.call("expire",?KEYS[1],?window)
end
if?current?<?limit?then
????return?1
elseif?current?==?limit?then
????return?2
else
????return?0
end`主要就是使用 INCRBY 命令來(lái)實(shí)現(xiàn),第一次請(qǐng)求需要給 key 加上一個(gè)過(guò)期時(shí)間,到達(dá)過(guò)期時(shí)間之后,key 過(guò)期被清楚,重新計(jì)數(shù)。
限流器初始化:
type?(
????//?PeriodOption?defines?the?method?to?customize?a?PeriodLimit.
????PeriodOption?func(l?*PeriodLimit)
????//?A?PeriodLimit?is?used?to?limit?requests?during?a?period?of?time.
????PeriodLimit?struct?{
????????period?????int??//?窗口大小,單位?s
????????quota??????int??//?請(qǐng)求上限
????????limitStore?*redis.Redis
????????keyPrefix??string???//?key?前綴
????????align??????bool
????}
)
//?NewPeriodLimit?returns?a?PeriodLimit?with?given?parameters.
func?NewPeriodLimit(period,?quota?int,?limitStore?*redis.Redis,?keyPrefix?string,
????opts?...PeriodOption)?*PeriodLimit?{
????limiter?:=?&PeriodLimit{
????????period:?????period,
????????quota:??????quota,
????????limitStore:?limitStore,
????????keyPrefix:??keyPrefix,
????}
????for?_,?opt?:=?range?opts?{
????????opt(limiter)
????}
????return?limiter
}調(diào)用限流:
//?key?就是需要被限制的資源標(biāo)識(shí)
func?(h?*PeriodLimit)?Take(key?string)?(int,?error)?{
????return?h.TakeCtx(context.Background(),?key)
}
//?TakeCtx?requests?a?permit?with?context,?it?returns?the?permit?state.
func?(h?*PeriodLimit)?TakeCtx(ctx?context.Context,?key?string)?(int,?error)?{
????resp,?err?:=?h.limitStore.EvalCtx(ctx,?periodScript,?[]string{h.keyPrefix?+?key},?[]string{
????????strconv.Itoa(h.quota),
????????strconv.Itoa(h.calcExpireSeconds()),
????})
????if?err?!=?nil?{
????????return?Unknown,?err
????}
????code,?ok?:=?resp.(int64)
????if?!ok?{
????????return?Unknown,?ErrUnknownCode
????}
????switch?code?{
????case?internalOverQuota:?//?超過(guò)上限
????????return?OverQuota,?nil
????case?internalAllowed:???//?未超過(guò),允許訪問(wèn)
????????return?Allowed,?nil
????case?internalHitQuota:??//?正好達(dá)到限流上限
????????return?HitQuota,?nil
????default:
????????return?Unknown,?ErrUnknownCode
????}
}到此這篇關(guān)于詳解go-zero如何實(shí)現(xiàn)計(jì)數(shù)器限流的文章就介紹到這了,更多相關(guān)go-zero計(jì)數(shù)器限流內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Go+Lua解決Redis秒殺中庫(kù)存與超賣問(wèn)題
本文主要介紹了Go+Lua解決Redis秒殺中庫(kù)存與超賣問(wèn)題,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-03-03
golang開(kāi)發(fā)微框架Gin的安裝測(cè)試及簡(jiǎn)介
這篇文章主要為大家介紹了golang微框架Gin的安裝測(cè)試及簡(jiǎn)介,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步早日升職加薪2021-11-11
golang將多路復(fù)異步io轉(zhuǎn)成阻塞io的方法詳解
常見(jiàn)的IO模型有阻塞、非阻塞、IO多路復(fù)用,異,下面這篇文章主要給大家介紹了關(guān)于golang將多路復(fù)異步io轉(zhuǎn)成阻塞io的方法,文中給出了詳細(xì)的示例代碼,需要的朋友可以參考借鑒,下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09
Golang實(shí)現(xiàn)字符串倒序的幾種解決方案
給定一個(gè)字符串,按單詞將該字符串逆序是我們大家在開(kāi)發(fā)中可能會(huì)遇到的一個(gè)需求,所以下面這篇文章主要給大家介紹了關(guān)于Golang如何實(shí)現(xiàn)字符串倒序的幾種解決方案,文中通過(guò)示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來(lái)一起看看吧。2017-10-10
深入理解Golang中的Protocol Buffers及其應(yīng)用
本篇文章將深入探討 Go 語(yǔ)言中使用 Protobuf 的基礎(chǔ)知識(shí)、常見(jiàn)應(yīng)用以及最佳實(shí)踐,希望能幫大家了解如何在項(xiàng)目中高效利用 Protobuf2024-11-11
如何通過(guò)Golang的container/list實(shí)現(xiàn)LRU緩存算法
文章介紹了Go語(yǔ)言中container/list包實(shí)現(xiàn)的雙向鏈表,并探討了如何使用鏈表實(shí)現(xiàn)LRU緩存,LRU緩存通過(guò)維護(hù)一個(gè)雙向鏈表來(lái)管理數(shù)據(jù),確保在插入和刪除操作時(shí)能夠以O(shè)(1)的平均時(shí)間復(fù)雜度運(yùn)行,提供了鏈表的操作和使用場(chǎng)景,并附帶了實(shí)現(xiàn)LRU緩存的代碼示例,感興趣的朋友一起看看吧2025-03-03
golang 實(shí)現(xiàn)比特幣內(nèi)核之處理橢圓曲線中的天文數(shù)字
比特幣密碼學(xué)中涉及到的大數(shù)運(yùn)算超出常規(guī)整數(shù)范圍,需使用golang的big包進(jìn)行處理,通過(guò)使用big.Int類型,能有效避免整數(shù)溢出,并保持邏輯正確性,測(cè)試展示了在不同質(zhì)數(shù)模下的運(yùn)算結(jié)果,驗(yàn)證了邏輯的準(zhǔn)確性,此外,探討了費(fèi)馬小定理在有限字段除法運(yùn)算中的應(yīng)用2024-11-11

