| — | — | — | — | — | — | — |
| 固定窗口 | 计数周期T、周期内最大访问数N | 低O(1)(记录周期内访问次数及周期开始时间 | 低O(1) | 否 | 否 | 低 |
| 滑动窗口 | 计数周期T、周期内最大访问数N | 高O(N)(记录每个小周期中的访问数量) | 中O(N) | 是 | 相对实现。滑动窗口的格子划分的越多,那么滑动窗口的滚动就越平滑 | 中 |
| 漏桶 | 漏桶流出速度r、漏桶容量N | 低O(1)(记录当前漏桶中容量) | 高O(N) | 是 | 是 | 高 |
| 令牌桶 | 令牌产生速度r、令牌桶容量N | 低O(1)(记录当前令牌桶中令牌数) | 高O(N) | 是 | 是 | 高 |
二、分布式限流实现
现在的服务基本都是采用分布式多实例,单例的限流一般都是采用框架实现,例如Guava就一键式集成了令牌桶限流算法,直接调用API就可以了,很简单,但是在分布式环境下,就要求我
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
们自己来实现了。
一般分布式限流刚开始都会使用固定窗口算法进行限流,这主要原因是借助Redis,可以轻轻松松几行代码搞定分布式限流。
if (redisTemplate.hasKey(key)) {
int currentNum = redisTemplate.opsForValue().get(key);
if (currentNum < maxMum) {
// 请求并发量加1
redisTemplate.opsForValue().increment(key, 1);
} else {
return;
}
} else {
redisTemplate.opsForValue().set(key, 1, 1, TimeUnit.MINUTES);
}
三、限流成绝流
如上述代码所示,非常简单,设置一个带有过期时间的key,再加一个阀值就可以了,代码看似没有问题,其实隐藏着一个巨大的Bug,也正是因为这个Bug导致限流变成绝流。
3.1 Bug场景
先给大家讲解一下Bug出现的场景,博主将限流代码发布到线上之后,刚开始确实实现了限流,达到阀值之后就不会再增加了,但是过了几分钟之后发现还处于限流状态,此时Redis早就到了过期时间。查看Redis对应的Key发现,Key的过期时间竟然是-1,也就是永不过期。这个过程就非常诡异了,博主将Key删除掉之后,过一段时间又发生同样的问题。
3.2 错误整理
我们从这个错误场景中可以得出,这个Bug并不是必然发生的,而是在某一个时间点就会突然发生,明确这一点之后,基本可以确定应该是Redis操作这个Key发生的问题。
我们再来仔细看一下代码,所有的代码都没有问题,唯一可能出现的只可能是redisTemplate.opsForValue().increment(key, 1)命令了,假如说执行increment的时候,key正好过期了呢,执行increment方法是会报错还是会生成一个新的Key然后加1呢?
通过实验我们可以得出,通过redisTemplate.opsForValue().increment(key, 1)命令也可以创建Key,而且创建的还是不过期的Key,好了罪魁祸首找到了,那我们应该如何修复这个Bug呢,别着急我们接着往下看。
不说版本就瞎逼逼,那是耍流氓。RedisTemplate的版本号为:spring.data.redis-2.1.8.RELEASE
3.3 Bug修复
public void resetExpireKey(String key) {
Long expireSeconds = redisTemplate.opsForValue().getOperations().getExpire(key);
// 永不过期或者不存在
if (expireSeconds == -1
|| expireSeconds == -2) {
redisTemplate.opsForValue().set(key, 1, 1, TimeUnit.MINUTES);
}
}
@Override
public boolean isLimiter(String key, int maxMum) {
try {
// key存在,说明1分钟之后还未过期
if (redisTemplate.hasKey(key)) {
// 重置过期时间
resetExpireKey(key);
int currentNum = redisTemplate.opsForValue().get(key);
if (currentNum < maxMum) {
// 请求并发量加1
redisTemplate.opsForValue().increment(key, 1);
} else {
return true;
}
} else {
// 已经过期,下一个周期开始(过期时间为1分钟)
redisTemplate.opsForValue().set(key, 1, 1, TimeUnit.MINUTES);
}
} catch (Exception e) {
e.printStackTrace();
}
return false;
}
解决这个Bug其实很简单,只要在increment之前,做一层判断即可,这样一个分布式限流就完成了。