前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Redis限流正确姿势指导指南,结尾附带Bug小惊喜,不要错过哦~

Redis限流正确姿势指导指南,结尾附带Bug小惊喜,不要错过哦~

作者头像
林老师带你学编程
发布2021-03-02 16:26:16
1.4K0
发布2021-03-02 16:26:16
举报
文章被收录于专栏:强仔仔强仔仔

一、限流算法

限流算法经典的一般有四种:计数器(固定窗口)算法、滑动窗口算法、漏桶算法、令牌桶算法。

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,可以轻轻松松几行代码搞定分布式限流。

代码语言:javascript
复制
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修复

代码语言:javascript
复制
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限流缺陷:

  1. 因为结构简单,它的缺陷也是非常明显的,如果我们设置的一分钟内上限是1w,但还是有可能导致高并发而不被限流,比如0.1s用完1w是额度,这个每秒的并发量就非常高了。
  2. 还有另外一个问题就是,如果多个实例,可能会导致某一个实例负载过高,导致限流不均匀。

4.2 架构升级

第一个问题只能换限流的算法模型了,因为固定窗口的算法就是有这个问题,需要修改为滑动窗口、漏桶、令牌桶算法,具体的实现博主会再后面的文章中和大家介绍,敬请期待。

第二个问题可以结合服务内部限流+分布式限流两种分别来实现,我们可以设置一个总的阀值和服务内部的阀值,如果内部达到阀值,总的阀值还没到,就可以推送到其它实例去处理。

图片
图片

五、总结

今天的分享就到这边了,通过这个Bug,童靴们有没有感觉到,一个这么简单的代码逻辑,竟然隐藏着如此凶险的Bug。林老师带你学编程,今天你学到了嘛。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-02-09 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档