利用Redis來限流,可以限定用戶的某個行為在指定的時間里只能允許發生N次。
場景: 某個用戶在一秒內只能回復5次,那么利用Redis如何實現呢。
思路:這個限流需求中存在一個滑動時間窗口,我們可以聯想到zset數據結構的score值,我們可以通過score來圈出這個時間窗口來。而且我們只需要維護這個時間窗口,窗口之外的數據都可以砍掉。那這個zset 的value填什么比較合適呢?它只需要保證唯一性即可,用 uuid 會比較浪費空間,改用毫秒時間戳比較好。
圖如下:

now_ts是當前的毫秒時間戳,我們只需要維護[now_s-period,now_s]這段時間里用戶的操作數即可。
具體代碼:
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.currentTimeMillis();
//毫秒時間戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();//用了multi,也就是事務,能保證一系列指令的原子順序執行
//value和score都使用毫秒時間戳
pipeline.zadd(key,nowTs,nowTs+"");
//移除時間窗口之前的行為記錄,剩下的都是時間窗口內的
pipeline.zremrangeByScore(key,0,nowTs-period*1000);
//獲得[nowTs-period*1000,nowTs]的key數量
Response<Long> count=pipeline.zcard(key);
//每次設置都能保持更新key的過期時間
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
//每個用戶在1秒內最多能做五次動作
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
由於毫秒時間戳的精度問題,1ms內可能有執行好幾次操作,有zset的去重操作,所以會看到true出現了超過5次,說明還不夠精確。
我下面用了Thread.sleep(1)來模擬了不同操作的間的時間間隔 可是這種方法並不提倡
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.currentTimeMillis();
//毫秒時間戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();
//value和score都使用毫秒時間戳
pipeline.zadd(key,nowTs,nowTs+"");
//移除時間窗口之前的行為記錄,剩下的都是時間窗口內的
pipeline.zremrangeByScore(key,0,nowTs-period*1000);
//獲得[nowTs-period*1000,nowTs]的key數量
Response<Long> count=pipeline.zcard(key);
//每次設置都能更新key的過期時間
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
while (true){
Thread.sleep(1000); //這里模擬每次經過1s限流之后 "viscu"這個用戶就可以重新進行"reply"行為的操作。
for (int i = 0; i < 20; i++) {
//模擬每個動作之間的間隔時間為1ms 具體看情況8 這里只是簡單模擬一下。
Thread.sleep(1);
//這樣就確保了該用戶在1秒內最多能做五次動作
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
}
或者我們可以使用納秒這種更加精確的數或者加上隨機數:
public class SimpleRateLimiter {
private final Jedis jedis;
public SimpleRateLimiter(Jedis jedis) {
this.jedis = jedis;
}
public boolean isActionAllow(String userId,String actionKey,int period,int maxCount) throws IOException {
String key=String.format("hist6:%s:%s",userId,actionKey);
long nowTs=System.nanoTime();
//納秒時間戳
Pipeline pipeline=jedis.pipelined();
pipeline.multi();
//value和score都使用納秒時間戳
pipeline.zadd(key,nowTs,nowTs+"");
pipeline.zremrangeByScore(key,0,nowTs-period*1000*1000*1000);
Response<Long> count=pipeline.zcard(key);
pipeline.expire(key,period);
pipeline.exec();
pipeline.close();
return count.get()<=maxCount;
}
public static void main(String[] args) throws IOException, InterruptedException {
Jedis jedis=new Jedis("localhost",6379);
jedis.auth("iostream");
SimpleRateLimiter limiter=new SimpleRateLimiter(jedis);
for (int i = 0; i < 20; i++) {
System.out.println(limiter.isActionAllow("viscu","reply",1,5));
}
}
}
- 參考: Redis 深度歷險:核心原理與應用實踐
題目和代碼都是來自這篇文章,只是加上了一些自己的思考。
