簡單限流
阿新 • • 發佈:2018-12-04
參考書籍:《redis深度歷險:核心原理與應用實踐》
如果系統要限定使用者的某個行為在指定的時間裡只能允許發生N次,這裡使用Redis的資料結構來實現這個簡單限流。
首先先定義一個介面
#指定使用者user_id的某個行為action_key在特定的時間內period只允許發生的最多次數max_count def is_action_allowed(user_id,action_key,period,max_count): return True #呼叫介面 can_reply=is_action_allowed(user_id,action_key,period,max_count)if can_reply: do_reply() else: raise ActionthresholdOverflow()
在這個介面中,我們實現這樣一個解決方南。我們建立一個滑動時間視窗,我們只需要保留這個漸漸視窗,視窗之外的資料都可以砍掉。我們用zset的score值圈出這個時間視窗而zast的value只需要保證唯一性就好,使用uuid會比較浪費空間,就改用毫秒時間戳就好。
如上圖所示,我們用一個zset結構記錄使用者的行為歷史,每一個行為都會作為zset中的key儲存下來。同一個使用者的同一種行為為一個zset記錄。為了節省記憶體,我們只需要保留視窗時間內的行為記錄。如果是冷使用者,zset在是空的的時候會自動釋放記憶體這個特性可以解決。
通過統計滑動視窗內的行為數量與預支進行比較就可以得出當前的行為是否允許。
下面我們來實現這個介面
import time import redis client=redis.StrictRedis() def is_action_allowed(user_id,action_key,period,max_count): key='hist:%s%s'%(user_id,action_key) now_ts=int(time.time()*1000)#毫秒時間戳 with client.pipeline() as pipe:#pipe現在是client.pipeline得到的#記錄行為 pipe.zadd(key,now_ts,now_ts)#value和score都是用毫秒時間戳 #移除時間視窗之前的行為記錄,剩下的都是時間視窗內的 pipe.zremrangebyscore(key,0,now_ts-period*1000) #獲取視窗內的行為數量 pipe.zcard(key) #設定zset過期時間,避免冷使用者持續佔用記憶體 過期時間應該等於時間視窗的長度 pipe.expire(key,period) #批量執行 _,_,current_count,_=pipe.execute() #比較數量是否超標 return current_count<=max_count #執行這個後你會發現,其實會輸出8個True是因為會生成部分相同的毫秒時間戳,zadd會去重
#所以個數就多了,可以將時間再精確一點來改善這種情況
for i in range(20): print(is_action_allowed('hello','reply',60,5))
在這個實現程式碼裡,我們是先將行為新增進去,然後在查詢是否超過了操作次序的原因是這樣的。如果請求進來,第一步不是查詢而是新增,這樣即使最後計算結果是超過了操作次數,不繼續執行後續程式碼,看起來使用者的操作沒有起到作用,但是資料是已經進入到了redis,如果遭到惡意攻擊,資料庫執行是正常的,但是redis中有著太多垃圾資料,這時候記憶體可能會不夠用,導致redis的節點不工作,從而擴充套件到整個伺服器。