為什么限制訪問頻率
做服務接口時通常需要用到請求頻率限制 Rate limiting,例如限制一個用戶1分鍾內最多可以范圍100次
主要用來保證服務性能和保護數據安全
因為如果不進行限制,服務調用者可以隨意訪問,想調幾次就調幾次,會給服務造成很大的壓力,降低性能,再比如有的接口需要驗證調用者身份,如果不進行訪問限制,調用者可以進行暴力嘗試
redis如何解決“單位時間內只能n次操作”這樣的問題?
假定要限制每分鍾每個用戶最多只能訪問100個頁面。
方案一:string
通過為用戶使用一個名為 rate.limiting:userId 的字符串類型鍵,每次訪問都使用 INCR命令遞增該鍵的鍵值。
如果遞增后的值為 1(第一次訪問),則要為鍵設置過期時間 60秒。
這樣每次用戶訪問都讀取該鍵值,當鍵值超過100時,說明訪問頻率超過了限制,需要稍后訪問。
該鍵過期后會自動刪除,所以下一分鍾用戶訪問次數又會重新計算。
偽代碼如下: $isKeyExists = EXISTS rate.limiting:$userId // 存在返回 1,不存在返回 0 if $isKeyExists is 1 $times = INCR rate.limiting:$userId if $times > 100 // 第100次訪問會增加到101 print 訪問頻率超過限制,請稍后再試 exit else MULTI //此處,如果不加事務,競態條件可能出現
INCR rate.limiting:$userId
EXPIRE $keyName, 60
EXEC
上面為什么要用MULTI,那是因為如果在執行完INCR rate.limiting:$userId之后,如果(出現故障)沒有設置過期時間,那么該鍵將永遠存在,所以需要加上事務。
方案二:list
事實上,方案一有個問題。如果一個用戶在第一分鍾的最后一秒訪問了99次,在下一分鍾的第一秒訪問了100次,相當於在兩秒訪問了199次,
與一分鍾內最多只能訪問100次相比還是差距比較大,盡管這種情況比較極端,但是依然存在。如果要實現粒度更小的控制方式,精確的保證每分鍾最多訪問100次,就需要使用第二種方案。
第二種方案需要記錄用戶每次的訪問時間,因此對於每個用戶,用列表類型的鍵記錄他最近100次訪問的時間。
如果鍵中的元素超過100個,就判斷時間最早的元素距離現在的時間是否小於1分鍾,如果是,則表示用戶最近1分鍾的訪問次數超過100次,如果不是就將當前時間加入列表中,同時把最早的元素刪除
偽代碼如下: $limitLength = LLEN rate.limiting:$userId if $limitLength < 100 LPUSH rate.limiting:$userId, now() else $time = LINDEX rate.limiting:$userId, -1 // 取最后一個元素 if now() - $time < 60 print 訪問頻率超過限制,請稍后再試 else LPUSH rate.limiting:$userId, now() LTRIM rate.limiting:$userId, 0, 99 // 刪除[0~99]以外的元素
這種方式 now() 的功能是獲得當前的 Unix時間,由於要記錄當前訪問時間,所以當要限制 “A時間最多訪問B次” 時,如果”B”比較大,會占用較多內存,
實際使用時要去權衡。而且這種方法會出現就競態條件,可以通過腳本避免。
但是在高並發的緩存系統中,大量使用事務是非常糟糕的,可以用redis自帶的lua腳本功能實現多個操作的“原子性”
方案三:使用lua腳本實現頻率限制
思路
把限制邏輯封裝到一個Lua腳本中,調用時只需傳入:key、限制數量、過期時間,調用結果就會指明是否運行訪問
local notexists = redis.call(\"set\", KEYS[1], 1, \"NX\", \"EX\", tonumber(ARGV[2]))
if (notexists) then
return 1
end
local current = tonumber(redis.call(\"get\", KEYS[1]))
if (current == nil) then
local result = redis.call(\"incr\", KEYS[1])
redis.call(\"expire\", KEYS[1], tonumber(ARGV[2]))
return result
end
if (current >= tonumber(ARGV[1])) then
error(\"too many requests\")
end
local result = redis.call(\"incr\", KEYS[1])
return result
使用 eval 調用
eval 腳本 1 key 參數-允許的最大次數 參數-過期時間