一、限流算法
限流算法经典的一般有四种:计数器(固定窗口)算法、滑动窗口算法、漏桶算法、令牌桶算法。
redis分布式限流来自林老师带你学编程00:0001:34
关于它们的实现网上有一堆的资料,有兴趣的可以百度、Google一下,下面是四种算法大致总结。
算法 | 确定参数 | 空间复杂度 | 时间复杂度 | 限制突发流量 | 平滑限流 | 分布式环境下实现难度 |
---|---|---|---|---|---|---|
固定窗口 | 计数周期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就可以了,很简单,但是在分布式环境下,就要求我们自己来实现了。
一般分布式限流刚开始都会使用固定窗口算法进行限流,这主要原因是借助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之前,做一层判断即可,这样一个分布式限流就完成了。
四、架构升级
4.1 Redis限流缺陷:
4.2 架构升级
第一个问题只能换限流的算法模型了,因为固定窗口的算法就是有这个问题,需要修改为滑动窗口、漏桶、令牌桶算法,具体的实现博主会再后面的文章中和大家介绍,敬请期待。
第二个问题可以结合服务内部限流+分布式限流两种分别来实现,我们可以设置一个总的阀值和服务内部的阀值,如果内部达到阀值,总的阀值还没到,就可以推送到其它实例去处理。
五、总结
今天的分享就到这边了,通过这个Bug,童靴们有没有感觉到,一个这么简单的代码逻辑,竟然隐藏着如此凶险的Bug。林老师带你学编程,今天你学到了嘛。