引言
从实战层次来展开,主要是实战环节,以问题展开,应对面试场景作答【melo称其为"手撕面答"】,尽量简短,某些部分可能不会进行详细介绍。
emmm,但后边有些部分还是干脆整合在一起了,可观性好一点,不至于看得一头雾水
本篇脑图速览
Redis限流是怎么做的?
固定窗口计数
固定窗口计数是指,假设我们的限流规则是:1min内最多只能访问10次,那么固定窗口就是固定了【 1min-2min】这个窗口内,只能有10次访问 ,相应的我们就要给这个窗口维护一个计数器。 为了节省空间,其实我们不需要维护一个个窗口,只需要维护当前访问时间所在的窗口即可,以及对应的计数器,当新的访问到达了下一个窗口时,则计数器重置即可。
redis实现
用redis的话,由于有过期机制,其实设置1min过期,就可以实现计数器重置的效果了
-
redis设置一个名为qps的key,val用来计数,1min过期即可
//原子自增类 RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory()); //先自增 int qps = redisAtomicInteger.getAndIncrement(); //若是第一次访问 if(qps==0){ //设置1min过期 redisAtomicInteger.expire(1, TimeUnit.MINUTES); } if(qps>10){ throw new RuntimeException("qps超过阈值"); } 复制代码
存在的问题
由于是固定窗口,那其实存在窗口临界问题,比如用户可以在【1.5-2】这段区间访问10次,【2-2.5】这段区间也访问10次,这样就变成了1min内其实可以访问20次!看起来破坏了我们的限流规则,但由于我们是固定窗口计数,到达2的时候已经重置计数器了。
滑动窗口计数
假设我们的限流规则是:1min内最多只能访问10次,那么滑动窗口呢就是会根据你访问进来的时间,以访问时间作为区间末尾,当前时间-1min作为区间头部,相当于窗口一直在往右滑动,这样其实就能在一定程度上解决我们刚才提到的窗口临界问题
-
当访问时间为2.5的时候,此时对于的窗口是【1.5-2.5】,计数器都能正确计数
实现
要获得一段区间,并且按时间排序,我们可以想到用ZSet来实现,能按区间查询出【当前访问时间-1min,当前访问时间】这段区间的计数
//interMills为限流时间,也就是我们这里的1min Long count = redisTemplate.opsForZSet().count(redisKey, currentTimeMillis - interMills, currentTimeMillis); 复制代码
存在的问题
其实我们只是以更小的窗口大小去移动这个区间罢了,固定窗口计数是以1min为单位去移动,滑动窗口是以1s为单位去移动,后者出现窗口临界问题的概率更小,但依然是可能出现的,比如:
1. 一开始是【1-2min】这段区间,下一秒会移动为【1min1s - 2min1s】,如果此时有人在1min这一刻,访问了10次,然后下一秒又进入下一个区间了,计数重置,在1min1s这一刻又访问了10次,依旧会出现窗口临界问题,1min内访问次数达到了20次
经评论区的小伙伴提问过后,发现其实滑动窗口算法是能够解决临界窗口问题的,当初学习时,可能只看了片面的资料,或者那个资料的实现方式不是用ZSet,而是用其他,以起点为首的区间去计算也有可能。
不过至少可以确定的是,本文用ZSet实现,以当前访问时间为区间末尾的话,确实是不会发生临界窗口问题的,非常感谢两位掘友:[吃西瓜啊] && [kb啵]
窗口临界问题小结
其实窗口临界问题,就是在即将被移出窗口的这段区间内,可能一次性访问量达到了我们的阈值,而由于要移出窗口了,计数又将重置了,所以这些访问量就相当于不会被后续统计到,那么后续再次超过阈值,就变成双倍阈值了。