1、前言
一般我們做在指定時間內(nèi)只允許做 n 次都用,一個 key 設(shè)置過期時間 t 秒,然后在 key 過期時間內(nèi)只需要做 n 次。然而這個思路有問題,最明顯的就是跨時間段的問題。所以這個問題很顯然用滑動窗口來做。
指定時間T內(nèi),只允許發(fā)生N次。我們可以將這個指定時間T,看成一個滑動時間窗口(定寬)。我們采用Redis的zset基本數(shù)據(jù)類型的score來圈出這個滑動時間窗口。在實(shí)際操作zset的過程中,我們只需要保留在這個滑動時間窗口以內(nèi)的數(shù)據(jù),其他的數(shù)據(jù)不處理即可。
- 每個用戶的行為采用一個zset存儲,score為毫秒時間戳,value也使用毫秒時間戳(比UUID更加節(jié)省內(nèi)存)
- 只保留滑動窗口時間內(nèi)的行為記錄,如果zset為空,則移除zset,不再占用內(nèi)存(節(jié)省內(nèi)存)
2、代碼
package com.example.demo.redis;
import redis.clients.jedis.Jedis;
import redis.clients.jedis.Pipeline;
import redis.clients.jedis.Response;
import java.io.IOException;
/**
* <p>
* 通過zset實(shí)現(xiàn)滑動窗口算法限流
* </p>
*
*/
public class SimpleSlidingWindowByZSet {
private Jedis jedis;
public SimpleSlidingWindowByZSet(Jedis jedis) {
this.jedis = jedis;
}
/**
* 判斷行為是否被允許
*
* @param userId 用戶id
* @param actionKey 行為key
* @param period 限流周期
* @param maxCount 最大請求次數(shù)(滑動窗口大小)
* @return
*/
public boolean isActionAllowed(String userId, String actionKey, int period, int maxCount) throws IOException {
String key = this.key(userId, actionKey);
long ts = System.currentTimeMillis();
Pipeline pipe = jedis.pipelined();
pipe.multi();
// 每個用戶一個 zset,這里是 user + key 組合
pipe.zadd(key, ts, String.valueOf(ts));
// 移除滑動窗口之外的數(shù)據(jù)
pipe.zremrangeByScore(key, 0, ts - (period * 1000));
// 獲取窗口內(nèi)的數(shù)量
Response<Long> count = pipe.zcard(key);
// 設(shè)置行為的過期時間,如果數(shù)據(jù)為冷數(shù)據(jù),zset將會刪除以此節(jié)省內(nèi)存空間(不是必須,算是優(yōu)化)
pipe.expire(key, period);
pipe.exec();
pipe.close();
return count.get() <= maxCount;
}
/**
* 限流key
*
* @param userId
* @param actionKey
* @return
*/
public String key(String userId, String actionKey) {
return String.format("limit:%s:%s", userId, actionKey);
}
}
public class FirstTool {
static JedisPool pool = null;
static {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxIdle(8);
config.setMaxTotal(18);
pool = new JedisPool(config, "127.0.0.1", 6379, 2000);
}
public static void main(String[] args) throws Exception {
Jedis jedis = pool.getResource();
SimpleSlidingWindowByZSet slidingWindow = new SimpleSlidingWindowByZSet(jedis);
for (int i = 1; i <= 15; i++) {
boolean actionAllowed = slidingWindow.isActionAllowed("liziba", "view", 60, 5);
System.out.println("第" + i +"次操作" + (actionAllowed ? "成功" : "失敗"));
TimeUnit.MILLISECONDS.sleep(1);
}
jedis.close();
}
}