本篇文章給大家分享一下令牌桶算法原理,并介紹一下利用redis實現(xiàn)令牌桶算法的方法,希望對大家有所幫助!
在限流算法中有一種令牌桶算法,該算法可以應對短暫的突發(fā)流量,這對于現(xiàn)實環(huán)境中流量不怎么均勻的情況特別有用,不會頻繁的觸發(fā)限流,對調(diào)用方比較友好。
例如,當前限制10qps,大多數(shù)情況下不會超過此數(shù)量,但偶爾會達到30qps,然后很快就會恢復正常,假設這種突發(fā)流量不會對系統(tǒng)穩(wěn)定性產(chǎn)生影響,我們可以在一定程度上允許這種瞬時突發(fā)流量,從而為用戶帶來更好的可用性體驗。這就是使用令牌桶算法的地方。
令牌桶算法原理
如下圖所示,該算法的基本原理是:有一個容量為X的令牌桶,每Y單位時間內(nèi)將Z個令牌放入該桶。如果桶中的令牌數(shù)量超過X,那么它將被丟棄。處理請求時,需要先從令牌桶中取出令牌,如果拿到了令牌,則繼續(xù)處理;如果拿不到令牌,則拒絕請求。
可以看出,在令牌桶算法中設置X,Y和Z的數(shù)量尤為重要。Z應該比每Y單位時間內(nèi)的請求數(shù)稍大,系統(tǒng)將長時間處于此狀態(tài);X是系統(tǒng)允許的瞬時最大請求數(shù),并且系統(tǒng)不應該長時間處于此狀態(tài),否則就會頻繁觸發(fā)限流,此時表明流量出現(xiàn)了超預期的情況,需要及時調(diào)查原因并采取相應措施。
redis實現(xiàn)令牌桶算法
之前看過有些程序?qū)崿F(xiàn)的令牌桶,其向桶中放入令牌的方法是啟動一個線程,每隔Y單位時間增加一次令牌數(shù)量,或者在Timer中定時執(zhí)行這一過程。我不太滿意這種方法, 原因有二,一是浪費線程資源,二是因為調(diào)度的問題執(zhí)行時間不精確。【相關推薦:Redis視頻教程】
這里確定令牌桶中令牌數(shù)量的方法是通過計算得出,首先算出從上次請求到這次請求經(jīng)過了多長時間,是否達到發(fā)令牌的時間閾值,然后增加的令牌數(shù)是多少,這些令牌能夠放到桶中的是多少。
Talk is cheap!
下邊就來看看Redis中怎么實現(xiàn)的,因為涉及到多次與Redis的交互,這里為了提高限流處理的吞吐量,減少程序與Redis的交互次數(shù),采用了Redis支持的Lua script,Lua script的執(zhí)行是原子的,所以也不用擔心出現(xiàn)臟數(shù)據(jù)的問題。
代碼節(jié)選自 Redis視頻教程 ,它不僅支持普通主從部署Redis,還支持集群Redis,所以吞吐量可以通過水平擴展的方式進行提升。為了方便閱讀,這里增加一些注釋,實際是沒有的。
--?定義返回值,是個數(shù)組,包含:是否觸發(fā)限流(1限流?0通過)、當前桶中的令牌數(shù) local?ret={} ret[1]=0 --?Redis集群分片Key,KEYS[1]是限流目標 local?cl_key?=?'{'?..?KEYS[1]?..?'}' --?獲取限流懲罰的當前設置,觸發(fā)限流懲罰時會寫一個有過期時間的KV --?如果存在限流懲罰,則返回結果[1,-1] local?lock_key=cl_key?..?'-lock' local?lock_val=redis.call('get',lock_key) if?lock_val?==?'1'?then ????ret[1]=1 ????ret[2]=-1 ????return?ret; end --?這里省略部分代碼 --?獲取[上次向桶中投放令牌的時間],如果沒有設置過這個投放時間,則令牌桶也不存在,此時: --?一種情況是:首次執(zhí)行,此時定義令牌桶就是滿的。 --?另一種情況是:較長時間沒有執(zhí)行過限流處理,導致承載這個時間的KV被釋放了, --?這個過期時間會超過自然投放令牌到桶中直到桶滿的時間,所以令牌桶也應該是滿的。 local?last_time=redis.call('get',st_key) if(last_time==false) then ?--?本次執(zhí)行后剩余令牌數(shù)量:桶的容量-?本次執(zhí)行消耗的令牌數(shù)量 ????bucket_amount?=?capacity?-?amount; ????--?將這個令牌數(shù)量更新到令牌桶中,同時這里有個過期時間,如果長時間不執(zhí)行這個程序,令牌桶KV會被回收 ????redis.call('set',KEYS[1],bucket_amount,'PX',key_expire_time) ????--?設置[上次向桶中放入令牌的時間],后邊計算應放入桶中的令牌數(shù)量時會用到 ????redis.call('set',st_key,start_time,'PX',key_expire_time) ????--?返回值[當前桶中的令牌數(shù)] ????ret[2]=bucket_amount ????--?無需其它處理 ????return?ret end --?令牌桶存在,獲取令牌桶中的當前令牌數(shù) local?current_value?=?redis.call('get',KEYS[1]) current_value?=?tonumber(current_value) --?判斷是不是該放入新令牌到桶中了:當前時間-上次投放的時間?>=?投放的時間間隔 last_time=tonumber(last_time) local?last_time_changed=0 local?past_time=current_time-last_time if(past_time<inflow_unit then else end ret if>0?then ????????redis.call('set',lock_key,'1','EX',lock_seconds,'NX') ????end ????ret[1]=1 ????return?ret end --?來到這里,代表可以成功扣減令牌,則需要更新令牌桶KV if?last_time_changed==1?then ????redis.call('set',KEYS[1],bucket_amount,'PX',key_expire_time) ?--?有新投放,更新[上次投放時間]為本次投放時間 ????redis.call('set',st_key,last_time,'PX',key_expire_time) else ????redis.call('set',KEYS[1],bucket_amount,'PX',key_expire_time) end return?ret</inflow_unit>
通過以上代碼,可以看出,其主要處理過程是:
1、判斷有沒有被限流懲罰,有則直接返回,無則進入下一步。
2、判斷令牌桶是否存在,不存在則先創(chuàng)建令牌桶,然后扣減令牌返回,存在則進入下一步。
3、判斷是否需要投放令牌,不需要則直接扣減令牌,需要則先投放令牌再扣減令牌。
4、判斷扣減后的令牌數(shù),如果小于0則返回限流,同時設置限流懲罰,如果大于等于0則進入下一步。
5、更新桶中的令牌數(shù)到Redis。
你可以在任何一種開發(fā)語言的Redis庫中提交并運行這段Lua script腳本,如果你使用的是.NET平臺,可以參考這篇文章:ASP.NET Core中使用令牌桶限流(https://blog.bossma.cn/dotnet/asp-net-core-token-bucket-algorithm-of-rate-limit/) 。
關于FireflySoft.RateLimit
FireflySoft.RateLimit 是一個基于 .NET Standard 的限流類庫,其內(nèi)核簡單輕巧,能夠靈活應對各種需求的限流場景。
其主要特點包括:
- 多種限流算法:內(nèi)置固定窗口、滑動窗口、漏桶、令牌桶四種算法,還可自定義擴展。
- 多種計數(shù)存儲:目前支持內(nèi)存、Redis兩種存儲方式。
- 分布式友好:通過Redis存儲支持分布式程序統(tǒng)一計數(shù)。
- 限流目標靈活:可以從請求中提取各種數(shù)據(jù)用于設置限流目標。
- 支持限流懲罰:可以在客戶端觸發(fā)限流后鎖定一段時間不允許其訪問。
- 動態(tài)更改規(guī)則:支持程序運行時動態(tài)更改限流規(guī)則。
- 自定義錯誤:可以自定義觸發(fā)限流后的錯誤碼和錯誤消息。
- 普適性:原則上可以滿足任何需要限流的場景。
Github開源地址:https://github.com/bosima/FireflySoft.RateLimit/blob/master/README.zh-CN.md
本文轉載自:https://juejin.cn/post/7039105263168651301
作者:螢火架構
更多編程相關知識,請訪問:Redis視頻教程!!