Redis實現訪問控制頻率


 

為什么限制訪問頻率

做服務接口時通常需要用到請求頻率限制 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 參數-允許的最大次數 參數-過期時間

 


免責聲明!

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



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