使用Redis進行簡單的限流
限流
限流的目的是當系統的處理能力有限時,阻止計划外的請求繼續對系統施壓,通過對並發/請求進行限速或者一個時間窗口內的請求進行限速來保護系統,達到限制速率則可以拒絕服務。還有一個應用目的是用於控制用戶的行為,比如在論壇中的發帖,回復等。一般是要控制某行為在規定時間內允許的次數。
redis實現的限流
常見的限流算法有:計數器,令牌桶和漏桶算法
計數器算法是最簡單粗暴的算法,系統限制在指定時間內只允許發生N次事件。使用時間窗口
使用到的redis數據結構
redis中有個很特色的數據結構:zset(有序集合)
有點兒類似於java的SortSet和HashMap的結合體,一方面是個set,保證內部的value的唯一性,另一方面可以給每個value賦予一個score,表示這個value的排序權重。
命令為:ZADD KEY SCORE VALUE

通過zset的score值,圈出一個時間窗口,將時間窗口之外的數據移除,時間窗口內的數據就是需要的數據。
因為value的值需要唯一,value中存放納秒級的時間戳,存放毫秒級的可能會出現重復在高並發下。
每個用戶使用一個zset記錄其操作記錄,因為zset為空時會自動從內存中移除,所以低活躍用戶不會有自己的zset。降低了內存消耗。
具體實現
public void userReply () throws IOException {
String actionKey = "test";
String userId = "cjl";
for(int i=0;i<20;i++) {
System.out.println(isActionAllowed(userId, actionKey, 30, 5));
}
}
/**
* 查看用戶操作是否正常
* @param userId 用戶Id
* @param actionKey 行為名
* @param period 時間范圍
* @param maxCount 最大次數
* @return 是否正常
* @throws IOException
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) throws IOException {
String key = String.format("hist:%s:%s", userId, actionKey);
long nowTs = System.nanoTime();
Pipeline pipe = jedis.pipelined();
pipe.multi();
// 記錄行為
pipe.zadd(key, nowTs, "" + nowTs);
// 移除超時行為
pipe.zremrangeByScore(key, 0, nowTs - period * 1000*1000);
// 查找時間窗口中的行為數量
Response<Long> count = pipe.zcard(key);
// 重制所有行為的過期時間
pipe.expire(key, period + 1);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
運行結果
true
true
true
true
true
false
false
false
false
false
false
false
false
false
false
false
false
false
false
false
因為操作都是針對同一個key,所以使用pipline提升redis的存取效率。
pipline是將多條命令一次性發送給redis,並在所有命令執行完后一次性返回,通過減少客戶端和redis的通行次數來實現降低往返延時時間。
缺點
如果在時間窗口中需要記錄的行為次數過多,將會有很大的消耗,比如60s內1000次操作。
