文章目录
- 前言
- 什么是滑动窗口
- zset实现滑动窗口
- 小结
- 附录
前言
- 在Redis–Zset的语法和使用场景举例(朋友圈点赞,排行榜)一文中,提及了redis数据结构zset的指令语法和一些使用场景,今天我们使用zset来实现滑动窗口限流,详见下文。
什么是滑动窗口
-
滑动窗口是一种流量控制策略,用于控制一定时间内请求的访问数量。
-
其原理是:将时间划分成规定的时间片段,每个片段有固定的时间间隔,如1s,1min,1h,然后定义一个时间窗口,比如5s,5min等,该窗口会随着时间向右移动。此外还需要计数器计算窗口内的请求数。当窗口移动时,会把已经走过的时间片段的请求数删掉。每当请求进入系统时,会检查计数器中的请求数是否已经满了,如果计数未满,则请求允许被执行;否则执行相应的拒绝方法。
-
滑动窗口在时间内平滑地控制流量,而非简单地固定请求数与速率,可以更加灵活地突发流量和峰值流量。
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的访问情况:
-
观察运行结果,因为60秒内该接口只能调用10次,所以调用20次接口A,只有前10次成功了,与我们的期望相同。到此我们通过zset实现了滑动窗口限流的功能。
小结
本文通过Redis的有序集合Zset实现了滑动窗口限流的功能。然而这个方案也存在着缺点,因为zset要记录滑动窗口内的所有接口记录,当我们的要求是某接口在60秒内只能访问100万次,那么我们就可能得存入100万条记录,这种情况下,采用这种方案会消耗很大的存储空间,明显不适用。
附录
- 在window系统快速使用Redis服务,只需要下载该压缩包 redis压缩包:redis.7z,解压后,找到redis-server.exe即可启动redis服务。