基于Redis的限流系統(tǒng)的設(shè)計(jì)

本文講述基于Redis的限流系統(tǒng)的設(shè)計(jì),主要會(huì)談及限流系統(tǒng)中限流策略這個(gè)功能的設(shè)計(jì);在實(shí)現(xiàn)方面,算法使用的是令牌桶算法來,訪問Redis使用lua腳本。

1、概念

In computer networks,?rate limiting?is used to control the rate of traffic sent or received by a network interface controller and is used to prevent DoS attacks

用我的理解翻譯一下:限流是對(duì)系統(tǒng)的出入流量進(jìn)行控制,防止大流量出入,導(dǎo)致資源不足,系統(tǒng)不穩(wěn)定。

限流系統(tǒng)是對(duì)資源訪問的控制組件,控制主要的兩個(gè)功能:限流策略熔斷策略,對(duì)于熔斷策略,不同的系統(tǒng)有不同的熔斷策略訴求,有的系統(tǒng)希望直接拒絕、有的系統(tǒng)希望排隊(duì)等待、有的系統(tǒng)希望服務(wù)降級(jí)、有的系統(tǒng)會(huì)定制自己的熔斷策略,很難一一列舉,所以本文只針對(duì)限流策略這個(gè)功能做詳細(xì)的設(shè)計(jì)。

針對(duì)限流策略這個(gè)功能,限流系統(tǒng)中有兩個(gè)基礎(chǔ)概念:資源和策略。

資源?:或者叫稀缺資源,被流量控制的對(duì)象;比如寫接口、外部商戶接口、大流量下的讀接口

策略?:限流策略由限流算法和可調(diào)節(jié)的參數(shù)兩部分組成

熔斷策略:超出速率閾值的請(qǐng)求處理策略,是我自己理解的一個(gè)叫法,不是業(yè)界主流的說法。

2、限流算法

限制瞬時(shí)并發(fā)數(shù)

限制時(shí)間窗最大請(qǐng)求數(shù)

令牌桶

2.1、限制瞬時(shí)并發(fā)數(shù)

定義:瞬時(shí)并發(fā)數(shù),系統(tǒng)同時(shí)處理的請(qǐng)求/事務(wù)數(shù)量

優(yōu)點(diǎn):這個(gè)算法能夠?qū)崿F(xiàn)控制并發(fā)數(shù)的效果

缺點(diǎn):使用場(chǎng)景比較單一,一般用來對(duì)入流量進(jìn)行控制

java偽代碼實(shí)現(xiàn)

AtomicInteger atomic = new AtomicInteger(1)

try {

if(atomic.incrementAndGet() > 限流數(shù)) {

//熔斷邏輯

} else {

//處理邏輯

}

} finally {

atomic.decrementAndGet();

}

2.2、限制時(shí)間窗最大請(qǐng)求數(shù)

定義:時(shí)間窗最大請(qǐng)求數(shù),指定的時(shí)間范圍內(nèi)允許的最大請(qǐng)求數(shù)

優(yōu)點(diǎn):這個(gè)算法能夠滿足絕大多數(shù)的流控需求,通過時(shí)間窗最大請(qǐng)求數(shù)可以直接換算出最大的QPS(QPS = 請(qǐng)求數(shù)/時(shí)間窗)

缺點(diǎn):這種方式可能會(huì)出現(xiàn)流量不平滑的情況,時(shí)間窗內(nèi)一小段流量占比特別大

lua代碼實(shí)現(xiàn)

--- 資源唯一標(biāo)識(shí)

local key = KEYS[1]

--- 時(shí)間窗最大并發(fā)數(shù)

local max_window_concurrency = tonumber(ARGV[1])

--- 時(shí)間窗

local window = tonumber(ARGV[2])

--- 時(shí)間窗內(nèi)當(dāng)前并發(fā)數(shù)

local curr_window_concurrency = tonumber(redis.call('get', key) or 0)

if current + 1 > limit then

return false

else

redis.call("INCRBY", key,1)

if window > -1 then

redis.call("expire", key,window)

end

return true

end

2.3、令牌桶

算法描述

假如用戶配置的平均發(fā)送速率為r,則每隔1/r秒一個(gè)令牌被加入到桶中

假設(shè)桶中最多可以存放b個(gè)令牌。如果令牌到達(dá)時(shí)令牌桶已經(jīng)滿了,那么這個(gè)令牌會(huì)被丟棄

當(dāng)流量以速率v進(jìn)入,從桶中以速率v取令牌,拿到令牌的流量通過,拿不到令牌流量不通過,執(zhí)行熔斷邏輯

屬性

長(zhǎng)期來看,符合流量的速率是受到令牌添加速率的影響,被穩(wěn)定為:r

因?yàn)榱钆仆坝幸欢ǖ拇鎯?chǔ)量,可以抵擋一定的流量突發(fā)情況

M是以字節(jié)/秒為單位的最大可能傳輸速率:M>r

T max = b/(M-r) 承受最大傳輸速率的時(shí)間

B max = T max * M 承受最大傳輸速率的時(shí)間內(nèi)傳輸?shù)牧髁?/p>

優(yōu)點(diǎn):流量比較平滑,并且可以抵擋一定的流量突發(fā)情況

因?yàn)槲覀兿蘖飨到y(tǒng)的實(shí)現(xiàn)就是基于令牌桶這個(gè)算法,具體的代碼實(shí)現(xiàn)參考下文。

3、工程實(shí)現(xiàn)

3.1、技術(shù)選型

mysql:存儲(chǔ)限流策略的參數(shù)等元數(shù)據(jù)

redis+lua:令牌桶算法實(shí)現(xiàn)

說明:因?yàn)槲覀儼裷edis 定位為:緩存、計(jì)算媒介,所以元數(shù)據(jù)都是存在db中

3.2、架構(gòu)圖

3.3、 數(shù)據(jù)結(jié)構(gòu)

字段描述name令牌桶的唯一標(biāo)示apps能夠使用令牌桶的應(yīng)用列表max_permits令牌桶的最大令牌數(shù)rate向令牌桶中添加令牌的速率created_by創(chuàng)建人updated_by更新人

限流系統(tǒng)的實(shí)現(xiàn)是基于redis的,本可以和應(yīng)用無關(guān),但是為了做限流元數(shù)據(jù)配置的統(tǒng)一管理,按應(yīng)用維度管理和使用,在數(shù)據(jù)結(jié)構(gòu)中加入了apps這個(gè)字段,出現(xiàn)問題,排查起來也比較方便。

3.4、代碼實(shí)現(xiàn)

3.4.1、代碼實(shí)現(xiàn)遇到的問題

參考令牌桶的算法描述,一般思路是在RateLimiter-client放一個(gè)重復(fù)執(zhí)行的線程,線程根據(jù)配置往令牌桶里添加令牌,這樣的實(shí)現(xiàn)由如下缺點(diǎn):

需要為每個(gè)令牌桶配置添加一個(gè)重復(fù)執(zhí)行的線程

重復(fù)的間隔精度不夠精確:線程需要每1/r秒向桶里添加一個(gè)令牌,當(dāng)r>1000 時(shí)間線程執(zhí)行的時(shí)間間隔根本沒辦法設(shè)置(從后面性能測(cè)試的變現(xiàn)來看RateLimiter-client 是可以承擔(dān) QPS > 5000 的請(qǐng)求速率)

3.4.2、解決方案

基于上面的缺點(diǎn),參考了google的guava中RateLimiter中的實(shí)現(xiàn),我們使用了觸發(fā)式添加令牌的方式。

算法描述

基于上述的令牌桶算法

將添加令牌改成觸發(fā)式的方式,取令牌的是做添加令牌的動(dòng)作

在去令牌的時(shí)候,通過計(jì)算上一次添加令牌和當(dāng)前的時(shí)間差,計(jì)算出這段間應(yīng)該添加的令牌數(shù),然后往桶里添加

curr_mill_second = 當(dāng)前毫秒數(shù)

last_mill_second = 上一次添加令牌的毫秒數(shù)

r = 添加令牌的速率

reserve_permits = (curr_mill_second-last_mill_second)/1000 * r

添加完令牌之后再執(zhí)行取令牌邏輯

3.4.3、 lua代碼實(shí)現(xiàn)

--- 獲取令牌

--- 返回碼

--- 0 沒有令牌桶配置

--- -1 表示取令牌失敗,也就是桶里沒有令牌

--- 1 表示取令牌成功

--- @param key 令牌(資源)的唯一標(biāo)識(shí)

--- @param permits 請(qǐng)求令牌數(shù)量

--- @param curr_mill_second 當(dāng)前毫秒數(shù)

--- @param context 使用令牌的應(yīng)用標(biāo)識(shí)

local function acquire(key, permits, curr_mill_second, context)

local rate_limit_info = redis.pcall("HMGET", key, "last_mill_second", "curr_permits", "max_permits", "rate", "apps")

local last_mill_second = rate_limit_info[1]

local curr_permits = tonumber(rate_limit_info[2])

local max_permits = tonumber(rate_limit_info[3])

local rate = rate_limit_info[4]

local apps = rate_limit_info[5]

--- 標(biāo)識(shí)沒有配置令牌桶

if type(apps) == 'boolean' or apps == nil or not contains(apps, context) then

return 0

end

local local_curr_permits = max_permits;

--- 令牌桶剛剛創(chuàng)建,上一次獲取令牌的毫秒數(shù)為空

--- 根據(jù)和上一次向桶里添加令牌的時(shí)間和當(dāng)前時(shí)間差,觸發(fā)式往桶里添加令牌

--- 并且更新上一次向桶里添加令牌的時(shí)間

--- 如果向桶里添加的令牌數(shù)不足一個(gè),則不更新上一次向桶里添加令牌的時(shí)間

if (type(last_mill_second) ~= 'boolean' and last_mill_second ~= false and last_mill_second ~= nil) then

local reverse_permits = math.floor(((curr_mill_second - last_mill_second) / 1000) * rate)

local expect_curr_permits = reverse_permits + curr_permits;

local_curr_permits = math.min(expect_curr_permits, max_permits);

--- 大于0表示不是第一次獲取令牌,也沒有向桶里添加令牌

if (reverse_permits > 0) then

redis.pcall("HSET", key, "last_mill_second", curr_mill_second)

end

else

redis.pcall("HSET", key, "last_mill_second", curr_mill_second)

end

local result = -1

if (local_curr_permits - permits >= 0) then

result = 1

redis.pcall("HSET", key, "curr_permits", local_curr_permits - permits)

else

redis.pcall("HSET", key, "curr_permits", local_curr_permits)

end

return result

end

關(guān)于限流系統(tǒng)的所有實(shí)現(xiàn)細(xì)節(jié),我都已經(jīng)放到github上,gitbub地址:https://github.com/wukq/rate-limiter,有興趣的同學(xué)可以前往查看,由于筆者經(jīng)驗(yàn)與知識(shí)有限,代碼中如有錯(cuò)誤或偏頗,歡迎探討和指正。

3.4.4、管理界面

前面的設(shè)計(jì)中,限流的配置是和應(yīng)用關(guān)聯(lián)的,為了更夠更好的管理配置,需要一個(gè)統(tǒng)一的管理頁面去對(duì)配置進(jìn)行管控:

按應(yīng)用對(duì)限流配置進(jìn)行管理

不同的人分配不同的權(quán)限;相關(guān)人員有查看配置的權(quán)限,負(fù)責(zé)人有修改和刪除配置的權(quán)限

3.5、性能測(cè)試

配置:aws-elasticcache-redis 2核4g

因?yàn)镽atelimiter-client的功能比較簡(jiǎn)單,基本上是redis的性能打個(gè)折扣。

單線程取令牌:Ratelimiter-client的 QPS = 250/s

10個(gè)線程取令牌:Ratelimiter-client的 QPS = 2000/s

100個(gè)線程取令牌:Ratelimiter-client的 QPS = 5000/s

4、總結(jié)

限流系統(tǒng)從設(shè)計(jì)到實(shí)現(xiàn)都比較簡(jiǎn)單,但是確實(shí)很實(shí)用,用四個(gè)字來形容就是:短小強(qiáng)悍,其中比較重要的是結(jié)合公司的權(quán)限體系和系統(tǒng)結(jié)構(gòu),設(shè)計(jì)出符合自己公司規(guī)范的限流系統(tǒng)。

不足

redis 我們用的是單點(diǎn)redis,只做了主從,沒有使用redis高可用集群(可能使用redis高可用集群,會(huì)帶來新的問題)

限流系統(tǒng)目前只做了應(yīng)用層面的實(shí)現(xiàn),沒有做接口網(wǎng)關(guān)上的實(shí)現(xiàn)

熔斷策略需要自己定制,如果實(shí)現(xiàn)的好一點(diǎn),可以給一些常用的熔斷策略模板

參考書籍:

1.《Redis?設(shè)計(jì)與實(shí)現(xiàn)》

2.《Lua編程指南》

參考文章:

1. redis官網(wǎng)

2. lua編碼規(guī)范

3. 聊聊高并發(fā)系統(tǒng)之限流特技

4. guava Ratelimiter 實(shí)現(xiàn)

5. Token_bucket wiki 詞條

那么我們?cè)趺磥韺W(xué)習(xí)Redis 或者說需要掌握哪些Redis的技術(shù)呢,這里為大家準(zhǔn)備了一個(gè)Redis的學(xué)習(xí)腦圖,思路如下

當(dāng)然在Java架構(gòu)師的路上,僅僅學(xué)完Redis是不夠的,我這邊還為大家準(zhǔn)備好了Java架構(gòu)師的學(xué)習(xí)腦圖,加群即可獲取Java工程化、高性能及分布式、高性能、高架構(gòu)、zookeeper、性能調(diào)優(yōu)、Spring、MyBatis、Netty源碼分析和大數(shù)據(jù)等多個(gè)知識(shí)點(diǎn)高級(jí)進(jìn)階干貨的直播免費(fèi)學(xué)習(xí)權(quán)限及相關(guān)視頻資料,群號(hào):835638062 點(diǎn)擊鏈接加入群聊【Java高級(jí)架構(gòu)學(xué)習(xí)交流】:https://jq.qq.com/?_wv=1027&k=5S3kL3v

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容