redis實現訪問頻次限制的幾種方式


結合上一篇文章《redis在學生搶房應用中的實踐小結》中提及的用redis實現DDOS設計時遇到的expire的坑。事實上,redis官網中對incr命令的介紹中已經有關於怎樣用redis來做rate limit的探討。

這里將實現的兩種模式翻譯一下,並適當加了一些批注說明。原文可見官網

模式:Rate limiter

頻次限制器模式是一種特殊的計數器,它常被用來限制某個操作能夠被運行的頻次。

這個模式的實質事實上是限制對一個公共API運行訪問請求的次數限制。我們使用incr命令提供該模式的兩種實現。這里我們假設須要解決的問題是:對每一個IP。限制對某API的調用次數最高位10次每秒。

模式:Rate limiter 1

對該模式一個相對簡單和直接的實現,請見例如以下代碼:

FUNCTION LIMIT_API_CALL(ip)
ts = CURRENT_UNIX_TIME()
keyname = ip+":"+ts
current = GET(keyname)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    MULTI
        INCR(keyname,1)
        EXPIRE(keyname,10)
    EXEC
    PERFORM_API_CALL()
END

簡單來說。我們對每一個IP的每一秒都有一個計數器,但每一個計數器都有一個額外的設置:它們都將被設置一個10秒的過期時間。這能夠使得當時間已經不是當前秒時(此時該計數器也無效了)。能夠讓redis自己主動移除它。

須要注意的是,這里我們使用multiexec命令來確保對每一個API調用既運行了incr也同一時候能夠運行expire命令。

multi命令用於標識一個命令集被包括在一個事務塊中,exec保證該事務塊命令集運行的原子性。

模式:Rate limiter 2

另外的一種實現是採用單一的計數器,可是為了避免race condition(競態條件),它也更復雜。我們來看幾種不同的變體:

FUNCTION LIMIT_API_CALL(ip):
current = GET(ip)
IF current != NULL AND current > 10 THEN
    ERROR "too many requests per second"
ELSE
    value = INCR(ip)
    IF value == 1 THEN
        EXPIRE(value,1)
    END
    PERFORM_API_CALL()
END

該計數器在當前秒內第一次請求被運行時創建,但它僅僅能存活一秒。假設在當前秒內,發送超過10次請求。那么該計數器將超過10。

否則它將失效並從0開始又一次計數。

在上面的代碼中,存在一個race condition。假設由於某個原因。上面的代碼僅僅運行了incr命令,卻沒有運行expire命令,那么這個key將會被泄漏,直到我們再次遇到同樣的ip(備注,假設這里沒有輔助的刪除該key的措施,那么該key將永只是期,也將每次都錯誤發生,詳情可見本人之前一篇文章)。

這樣的問題也不難處理,能夠將incr命令以及另外的expire命令打包到一個lua腳本里。該腳本能夠用eval命令提交給redis運行(該方式僅僅在redis版本號大於等於2.6之后才干支持)。

local current
current = redis.call("incr",KEYS[1]) if tonumber(current) == 1 then redis.call("expire",KEYS[1],1) end

當然。也有還有一種方式來解決問題而不須要動用lua腳本。但須要用redis的list數據結構來替代計數器。

這樣的實現方式將會更復雜。並使用更高級的特性。

但它有一個優點是記住調用當前API的每一個client的IP。這樣的方式可能非常實用也可能沒用,這取決於應用需求。

FUNCTION LIMIT_API_CALL(ip)
current = LLEN(ip)
IF current > 10 THEN
    ERROR "too many requests per second"
ELSE
    IF EXISTS(ip) == FALSE
        MULTI
            RPUSH(ip,ip)
            EXPIRE(ip,1)
        EXEC
    ELSE
        RPUSHX(ip,ip)
    END
    PERFORM_API_CALL()
END

rpushx命令僅僅在key存在時才會將值增加list

仍然須要注意的是,這里也存在一個race condition(但這卻不會產生太大的影響)。問題是:exists可能返回false,但在我們運行multi/exec塊內的創建list的代碼之前,該list可能已被其它client創建。然而,在這個race condition發生時。將僅僅僅僅是丟失一個API調用,所以rate limiting仍然工作得非常好。

這里產生race condition不會有大問題的解決辦法在於,else分支使用的rpushx,它不會導致if not than init的問題。而且expire命令將在創建list的時候以原子的形式捆綁運行。

不會產生key泄漏。導致永不失效的情況產生。

很多其它內容請訪問:http://vinoyang.com


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM