限流算法在分佈式領域是一個常常被提起的話題,當系統的處理能力有限時,如何阻止計劃外的請求繼續對系統施壓,這是一個須要重視的問題。算法
除了控制流量,限流還有一個應用目的是用於控制用戶行爲,避免垃圾請求。好比在UGC 社區,用戶的發帖、回覆、點贊等行爲都要嚴格受控,通常要嚴格限定某行爲在規定時間內容許的次數,超過了次數那就是非法行爲。對非法行爲,業務必須規定適當的懲處策略。緩存
首先咱們來看一個常見 的簡單的限流策略。系統要限定用戶的某個行爲在指定的時間裏只能容許發生 N 次,如何使用 Redis 的數據結構來實現這個限流的功能?數據結構
這個限流需求中存在一個滑動時間窗口,想一想 zset 數據結構的 score 值,是否是能夠經過 score 來圈出這個時間窗口來。並且咱們只須要保留這個時間窗口,窗口以外的數據均可以砍掉。那這個 zset 的 value 填什麼比較合適呢?它只須要保證惟一性便可,用 uuid 會比較浪費空間,那就改用毫秒時間戳吧。分佈式
如圖所示,用一個 zset 結構記錄用戶的行爲歷史,每個行爲都會做爲 zset 中的一個key 保存下來。同一個用戶同一種行爲用一個 zset 記錄。性能
爲節省內存,咱們只須要保留時間窗口內的行爲記錄,同時若是用戶是冷用戶,滑動時間窗口內的行爲是空記錄,那麼這個 zset 就能夠從內存中移除,再也不佔用空間。ui
經過統計滑動窗口內的行爲數量與閾值 max_count 進行比較就能夠得出當前的行爲是否容許。用代碼表示以下:code
/** * @Auther: majx2 * @Date: 2019-3-22 14:21 * @Description: */ public class SimpleRateLimiter { private Jedis jedis = RedisDS.create().getJedis(); public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) { String key = String.format("hist:%s:%s", userId, actionKey); long nowTs = System.currentTimeMillis(); Pipeline pipe = jedis.pipelined(); pipe.multi(); pipe.zadd(key, nowTs, "" + nowTs); // 移除有序集中,指定score區間內的全部成員 pipe.zremrangeByScore(key, 0, nowTs - period * 1000); Response<Long> count = pipe.zcard(key); pipe.expire(key, period + 1); pipe.exec(); pipe.close(); return count.get() <= maxCount; } public static void main(String[] args) throws InterruptedException { final SimpleRateLimiter limiter = new SimpleRateLimiter(); for(int i=0;i<20;i++) { System.out.println(limiter.isActionAllowed("majx2", "reply", 60, 5)); } } }
總體思路就是:每個行爲到來時,都維護一次時間窗口。將時間窗口外的記錄所有清理掉,只保留窗口內的記錄。zset 集合中只有 score 值很是重要,value 值沒有特別的意義,只須要保證它是惟一的就能夠了。orm
由於這幾個連續的 Redis 操做都是針對同一個 key 的,使用 pipeline 能夠顯著提高Redis 存取效率。但這種方案也有缺點,由於它要記錄時間窗口內全部的行爲記錄,若是這個量很大,好比限定 60s 內操做不得超過 100w 次這樣的參數,它是不適合作這樣的限流的,由於會消耗大量的存儲空間。中間件
下一節咱們將引入高級限流算法——漏斗限流。ip
本文基於《Redis深度歷險:核心原理和應用實踐》一文的JAVA實踐。更多文章請參考:高性能緩存中間件Redis應用實戰(JAVA)