限流算法在分布式領(lǐng)域是一個(gè)經(jīng)常被提起的話題,當(dāng)系統(tǒng)的處理能力有限時(shí),如何阻止計(jì)劃外的請(qǐng)求繼續(xù)對(duì)系統(tǒng)施壓,這是一個(gè)需要重視的問題。
除了控制流量,限流還有一個(gè)應(yīng)用目的是用于控制用戶行為,避免垃圾請(qǐng)求。比如在UGC 社區(qū),用戶的發(fā)帖、回復(fù)、點(diǎn)贊等行為都要嚴(yán)格受控,一般要嚴(yán)格限定某行為在規(guī)定時(shí)間內(nèi)允許的次數(shù),超過了次數(shù)那就是非法行為。對(duì)非法行為,業(yè)務(wù)必須規(guī)定適當(dāng)?shù)膽吞幉呗浴?/p>
如何使用 Redis 來實(shí)現(xiàn)簡(jiǎn)單限流策略?
首先我們來看一個(gè)常見 的簡(jiǎn)單的限流策略。系統(tǒng)要限定用戶的某個(gè)行為在指定的時(shí)間里只能允許發(fā)生 N 次,如何使用 Redis 的數(shù)據(jù)結(jié)構(gòu)來實(shí)現(xiàn)這個(gè)限流的功能?
這個(gè)限流需求中存在一個(gè)滑動(dòng)時(shí)間窗口,想想 zset 數(shù)據(jù)結(jié)構(gòu)的 score 值,是不是可以通過 score 來圈出這個(gè)時(shí)間窗口來。而且我們只需要保留這個(gè)時(shí)間窗口,窗口之外的數(shù)據(jù)都可以砍掉。那這個(gè) zset 的 value 填什么比較合適呢?它只需要保證唯一性即可,用 uuid 會(huì)比較浪費(fèi)空間,那就改用毫秒時(shí)間戳吧。

如圖所示,用一個(gè) zset 結(jié)構(gòu)記錄用戶的行為歷史,每一個(gè)行為都會(huì)作為 zset 中的一個(gè)key 保存下來。同一個(gè)用戶同一種行為用一個(gè) zset 記錄。
為節(jié)省內(nèi)存,我們只需要保留時(shí)間窗口內(nèi)的行為記錄,同時(shí)如果用戶是冷用戶,滑動(dòng)時(shí)間窗口內(nèi)的行為是空記錄,那么這個(gè) zset 就可以從內(nèi)存中移除,不再占用空間。
通過統(tǒng)計(jì)滑動(dòng)窗口內(nèi)的行為數(shù)量與閾值 max_count 進(jìn)行比較就可以得出當(dāng)前的行為是否允許。用代碼表示如下:
/**
* @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區(qū)間內(nèi)的所有成員
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));
}
}
}
整體思路就是:每一個(gè)行為到來時(shí),都維護(hù)一次時(shí)間窗口。將時(shí)間窗口外的記錄全部清理掉,只保留窗口內(nèi)的記錄。zset 集合中只有 score 值非常重要,value 值沒有特別的意義,只需要保證它是唯一的就可以了。
因?yàn)檫@幾個(gè)連續(xù)的 Redis 操作都是針對(duì)同一個(gè) key 的,使用 pipeline 可以顯著提升Redis 存取效率。但這種方案也有缺點(diǎn),因?yàn)樗涗洉r(shí)間窗口內(nèi)所有的行為記錄,如果這個(gè)量很大,比如限定 60s 內(nèi)操作不得超過 100w 次這樣的參數(shù),它是不適合做這樣的限流的,因?yàn)闀?huì)消耗大量的存儲(chǔ)空間。
下一節(jié)我們將引入高級(jí)限流算法——漏斗限流。
本文基于《Redis深度歷險(xiǎn):核心原理和應(yīng)用實(shí)踐》一文的JAVA實(shí)踐。更多文章請(qǐng)參考:高性能緩存中間件Redis應(yīng)用實(shí)戰(zhàn)(JAVA)