Redis 使用滑动窗口限流
业务场景
在某一业务场景下,对访问用户进行限流处理,大约每秒响应1000个请求。
相关API
1.count(K key, double min, double max);
获取指定score区间里的元素个数
- 包括min、max
2.zCard(K key);
获取集合大小
3.removeRangeByScore(K key, double min, double max);
移除指定score区间内的值
4.add(K key, V value, double score);
向指定key中添加元素,按照score值由小到大进行排列
- 集合中对应元素已存在,会被覆盖,包括score
更多API可以参考(8条消息) RedisTemplate使用最详解(五)--- opsForZSet()_学习中啊哈哈的博客-CSDN博客_opsforzset。
代码实现
- overMaxCount方法中,获取当前时间(窗口结束值)与一秒前时间(窗口起始值),对应zset集合中的score参数的最小值和最大值。然后使用count获取指定key、指定score范围的集合大小,判断是否超过限定值,返回结果。这里只是判断大小,存在统计到窗口外过期元素的风险,所以指定了score范围。如果在数量增长的逻辑中进行了清除窗口过期元素的逻辑, 那么使用count或zCard来获取集合大小是一样的。
- canAccess方法中,使用zCard获取指定key的集合大小,如果未超过限定值,则进行数量增长。因为调用的increment方法中进行了清除窗口过期元素的逻辑,所以这里可以放心使用zCard来获取集合大小,不需要担心统计到窗口外的元素。
- increment方法中,获取当前时间(窗口结束值)与一秒前时间(窗口起始值),使用removeRangeByScore清除窗口外过期元素,即score范围在0~窗口起始值之间的元素。然后使用add添加元素,value值需要唯一,防止元素被覆盖。score为窗口结束值,用来下一次清除过期元素。最后使用expire设置key的过期时间,防止key一直存在。
@Component
public class SlidingWindowCounter {
@Resource
private RedisTemplate redisTemplate;
/**
* 判断key的value中的有效访问次数是否超过最大限定值maxCount
* 判断与数量增长分开处理
*
* @param key redis zset集合中的key
* @param windowInSecond 窗口间隔,单位:秒
* @param maxCount 最大计数
* @return boolean 是 or 否
*/
public boolean overMaxCount(String key, int windowInSecond, int maxCount) {
// 当前时间
long currentMs = System.currentTimeMillis();
// 窗口开始时间
long windowStartMs = currentMs - windowInSecond * 1000L;
// 按score统计key的value中的有效数量
Long count = redisTemplate.opsForZSet().count(key, windowStartMs, currentMs);
// 已访问次数 >= 最大可访问值
return count >= maxCount;
}
/**
* 判断key的value中的有效访问次数是否超过最大限定值maxCount,若没超过,将窗口内的访问数加一
* 判断与数量增长同步处理
*
* @param key redis zset集合中的key
* @param value redis zset集合中的value值,需唯一
* @param windowInSecond 窗口间隔,单位:秒
* @param maxCount 最大计数
* @return boolean 是 or 否
*/
public boolean canAccess(String key, String value, int windowInSecond, int maxCount) {
// 按照key统计集合中的有效数量
Long count = redisTemplate.opsForZSet().zCard(key);
if (count < maxCount) {
increment(key, value, windowInSecond);
return true;
}
return false;
}
/**
* 滑动窗口计数增长
*
* @param key redis zset集合中的key
* @param value redis zset集合中的value值,需唯一
* @param windowInSecond 窗口间隔,单位:秒
* @return void
*/
public void increment(String key, String value, int windowInSecond) {
// 当前时间
long currentMs = System.currentTimeMillis();
// 窗口开始时间
long windowStartMs = currentMs - windowInSecond * 1000L;
ZSetOperations zSetOperations = redisTemplate.opsForZSet();
// 清除窗口过期成员
zSetOperations.removeRangeByScore(key, 0, windowStartMs);
// 添加当前时间 value和score为当前时间戳
zSetOperations.add(key, value, currentMs);
// 设置key过期时间
redisTemplate.expire(key, windowInSecond, TimeUnit.SECONDS);
}
}
调用
public void buySlidingWindow() {
// 这里的value值是需要放到redis的zset集合中,如果相同元素会被覆盖,所以需要保证唯一值,推荐使用UUID
String value = "";
// 调用算法判断是否需要限流
final boolean canAccess = slidingWindowCounter.canAccess("", value, 1, 1000);
if (!canAccess) {
log.info("指定时间内请求人数超过指定值");
// 这里应该return一个返回值
return;
}
// TODO 接下来的业务逻辑
return;
}