結合上一篇文章《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自己主動移除它。
須要注意的是,這里我們使用multi
和exec
命令來確保對每一個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