公司前段时间需要一个限流的功能,使用场景则是在一个滑动时间段内统计某一IP的访问数量

第一次接触窗口的概念还是在学习大数据的时候,在实时处理的时候了解到的,自然而然就想到了窗口统计的操作,不过目前业务量可能还不至于吧,嗯对就是那样

无脑上呗,就利用Redis做了一个实现(不是原子性的)


我太啰嗦了,网上百度一下Redis限流很多理论和操作实践就不罗嗦了,直接上自己魔改过的

基于Redis的ZSet实现的窗口限流操作

忘记参考哪个贴了,如果有相似部分请补充一下原帖链接,代码各位取其精华去其糟粕,嗯~很可能都没了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* 窗口限制
*
* @param key 限制key
* @param windowTimes 窗口内限制次数
* @param windowTime 窗口时间(ms)
* @param windowLimitTime 达到限制时,限制时间
* @param timeUnit 时间类型
* @param errorMsg 限制时提示的信息
*/
public void windowLimit(String key, int[] windowTimes, long[] windowTime, long[] windowLimitTime, TimeUnit timeUnit, String errorMsg) {
long currentTime = System.currentTimeMillis();
String windowKey = WINDOW + key;
if (redisUtils.exists(WINDOW_LIMIT + key)) {
String[] errors = splitError(errorMsg);
// 这里要做分割是因为错误信息里有其他的数据,但是又不是全提示用户
log.warn("当前[{}]被[{}]限制,锁定时间[{}],错误信息:[{}]", key, "窗口锁", redisUtils.getExpire(WINDOW_LIMIT + key), errors[0]);
throw new ErrorCodeException(ErrorCodeEnum.ERROR, errors[1]);
}
// 按照窗口最大值,清除历史key
OptionalLong max = Arrays.stream(windowTime).max();
if (max.isPresent()) {
long maxWindow = max.getAsLong();
long minScore = currentTime - timeUnit.toMillis(maxWindow);
// 删除最远时间之前的元素,精简大小
redisUtils.removeRangeByScore(windowKey, 0, minScore - 1);
}

// 判断窗口是否达到限制
// 这段代码感觉可以优化 但是无解~
for (int i = 0; i < windowTime.length; i++) {
// 窗口区间开始位置
long rangeBegin = currentTime - timeUnit.toMillis(windowTime[i]);
// rangeBegin,currentTime是限流的时间
Set<Object> limits = redisUtils.rangeByScore(windowKey, rangeBegin, currentTime);
// 判断是否达到窗口限制
if (Objects.nonNull(limits)) {
// 时间内次数小于窗口限制次数不处理
if (limits.size() < windowTimes[i]) {
continue;
}
// 放入redis限制key,代表下一次一定会被限制
redisUtils.set(WINDOW_LIMIT + key, errorMsg, windowLimitTime[i], timeUnit);
// 如果时间内次数大于窗口限制次数,抛出异常
if (limits.size() > windowTimes[i]) {
String[] errors = splitError(errorMsg);
log.warn("当前[{}]被[{}]限制,锁定时间[{}],错误信息:[{}]", key, "窗口锁", redisUtils.getExpire(WINDOW_LIMIT + key), errors[0]);
throw new ErrorCodeException(ErrorCodeEnum.ERROR, errors[1]);
}
}
}
// 时间戳作为zset的score 后续清数据取数据用
redisUtils.zSetAdd(windowKey, StringUtils.replaceAll(UUID.randomUUID().toString(), "-", ""), currentTime);
}

代码写的有点糙请谅解~

还是上面提到的小部分应用还是可以的,大规模的使用还是建议专业的事专业的人干比较好~