Redis--Zset使用场景举例(滑动窗口实现限流)

文章目录

      • 前言
      • 什么是滑动窗口
      • zset实现滑动窗口
      • 小结
      • 附录

前言

  • 在Redis–Zset的语法和使用场景举例(朋友圈点赞,排行榜)一文中,提及了redis数据结构zset的指令语法和一些使用场景,今天我们使用zset来实现滑动窗口限流,详见下文。

什么是滑动窗口

  • 滑动窗口是一种流量控制策略,用于控制一定时间内请求的访问数量。

  • 其原理是:将时间划分成规定的时间片段,每个片段有固定的时间间隔,如1s,1min,1h,然后定义一个时间窗口,比如5s,5min等,该窗口会随着时间向右移动。此外还需要计数器计算窗口内的请求数。当窗口移动时,会把已经走过的时间片段的请求数删掉。每当请求进入系统时,会检查计数器中的请求数是否已经满了,如果计数未满,则请求允许被执行;否则执行相应的拒绝方法。

    Redis--Zset使用场景举例(滑动窗口实现限流)_第1张图片

  • 滑动窗口在时间内平滑地控制流量,而非简单地固定请求数与速率,可以更加灵活地突发流量和峰值流量。

zset实现滑动窗口

  • 在redis中可以使用zset实现滑动窗口作为限流方案,假如接口A每一分钟只能访问100次,那么我们可以将这个需要限流的接口名作为key,value采用zset数据结构,zset的score设置为当前请求的时间戳,zset的member只需要保证唯一性即可。

  • 涉及到的zset指令

    向zset添加数据:zadd key score member
    删除zset某个score范围内的数据: zremrangebyscore key min max
    统计zset中数据的数量:zcard key

  • 代码实现:在代码中定义滑动窗口大小为"windowSize",收到请求后,在redis生成zset,用zremrangebyscore删除score小于当前时间戳减去"windowSize"的数据,使用zcard查询当前zset中的数据量,即请求量判断是否超出限制值,若超出则不加入zset。

    public class RedisRateLimiter {
        private Jedis jedis;
        private String key;
        //窗口大小
        private int windowsize;
        //限制访问的请求数
        private Integer limitValue;
    
        public RedisRateLimiter(Jedis jedis, String key, int windowsize, Integer limitValue) {
            this.jedis = jedis;
            this.key = key;
            this.windowsize = windowsize;
            this.limitValue = limitValue;
        }
    
        public boolean allowVisit() {
            //获取当前时间戳
            long nowTimeStamp = System.currentTimeMillis();
            //窗口开始时间为当前时间戳减去60s
            long windowStartTime = nowTimeStamp - windowsize * 1000;
            //删除score小于窗口开始时间的数据
            jedis.zremrangeByScore(key, "-inf", String.valueOf(windowStartTime));
            if (jedis.zcard(key) < limitValue) {
                jedis.zadd(key, nowTimeStamp, String.valueOf(nowTimeStamp));
                return true;
            }
            //超过limieValue 返回false
            return false;
        }
    
        /**
         * 上面的方法可以改写为使用lua脚本,以避免高并发情况下的原子性问题
         */
        public boolean allowVIsitUseLua() {
            //获取当前时间戳
            long nowTimeStamp = System.currentTimeMillis();
            String luaScript = """
                        local window_start_time = ARGV[1] -ARGV[3]*1000
                        redis.call('ZREMRANGEBYSCORE',KEYS[1],'-inf',window_start_time)
                        local now_request = redis.call('ZCARD',KEYS[1])
                        if now_request < tonumber(ARGV[2]) then
                             redis.call('ZADD',KEYS[1],ARGV[1],ARGV[1])
                             return 1
                        else
                             return 0
                        end
                    """;
            Object result = jedis.eval(luaScript, 1, key, String.valueOf(nowTimeStamp), String.valueOf(limitValue), String.valueOf(windowsize));
            return (long) result == 1;
        }
    
        public static void main(String[] args) throws InterruptedException {
            Jedis jedis = new Jedis("127.0.0.1");
            String key = "interfaceA";
            jedis.del(key);
            RedisRateLimiter interfaceA = new RedisRateLimiter(jedis, key, 60, 10);
            //调用20次接口观察结果
            for (int i = 0; i < 20; i++) {
                System.out.println("当前时间:"+ DateTimeFormatter.ofPattern("yyyyMMdd HH:mm:ss:SSS").format(LocalDateTime.now())+
               "接口访问情况: "+(interfaceA.allowVIsitUseLua()?"成功":"失败"));
                Thread.sleep(1000);
            }
        }
    }
    
  • 测试结果:我们在mian方法中,调用20次接口A,设置滑动窗口为60秒内只可以访问10次,观察接口A的访问情况:
    Redis--Zset使用场景举例(滑动窗口实现限流)_第2张图片

  • 观察运行结果,因为60秒内该接口只能调用10次,所以调用20次接口A,只有前10次成功了,与我们的期望相同。到此我们通过zset实现了滑动窗口限流的功能。

小结

本文通过Redis的有序集合Zset实现了滑动窗口限流的功能。然而这个方案也存在着缺点,因为zset要记录滑动窗口内的所有接口记录,当我们的要求是某接口在60秒内只能访问100万次,那么我们就可能得存入100万条记录,这种情况下,采用这种方案会消耗很大的存储空间,明显不适用。

附录

  • 在window系统快速使用Redis服务,只需要下载该压缩包 redis压缩包:redis.7z,解压后,找到redis-server.exe即可启动redis服务。

你可能感兴趣的:(Redis,redis,数据库)