目录

今日说码

点滴记录中国代码进程

X

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

代码实现

  1. overMaxCount方法中,获取当前时间(窗口结束值)与一秒前时间(窗口起始值),对应zset集合中的score参数的最小值和最大值。然后使用count获取指定key、指定score范围的集合大小,判断是否超过限定值,返回结果。这里只是判断大小,存在统计到窗口外过期元素的风险,所以指定了score范围。如果在数量增长的逻辑中进行了清除窗口过期元素的逻辑, 那么使用count或zCard来获取集合大小是一样的。
  2. canAccess方法中,使用zCard获取指定key的集合大小,如果未超过限定值,则进行数量增长。因为调用的increment方法中进行了清除窗口过期元素的逻辑,所以这里可以放心使用zCard来获取集合大小,不需要担心统计到窗口外的元素。
  3. 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;
}

标题:Redis 使用滑动窗口限流
作者:96XL
地址:https://solo.96xl.top/articles/2022/09/23/1663923321139.html