redis实现的四种常见限流策略


Posted in Redis onJune 18, 2021
目录
  • 引言
  • 固定时间窗口算法
    • 实现
  • 滑动时间窗口算法
    • 实现
  • 漏桶算法
    • 实现
  • 令牌桶算法

 

引言

  • 在web开发中功能是基石,除了功能以外运维和防护就是重头菜了。因为在网站运行期间可能会因为突然的访问量导致业务异常、也有可能遭受别人恶意攻击
  • 所以我们的接口需要对流量进行限制。俗称的QPS也是对流量的一种描述
  • 针对限流现在大多应该是令牌桶算法,因为它能保证更多的吞吐量。除了令牌桶算法还有他的前身漏桶算法和简单的计数算法
  • 下面我们来看看这四种算法

 

固定时间窗口算法

  • 固定时间窗口算法也可以叫做简单计数算法。网上有很多都将计数算法单独抽离出来。但是笔者认为计数算法是一种思想,而固定时间窗口算法是他的一种实现
  • 包括下面滑动时间窗口算法也是计数算法的一种实现。因为计数如果不和时间进行绑定的话那么失去了限流的本质了。就变成了拒绝了

redis实现的四种常见限流策略

优点

  • 在固定的时间内出现流量溢出可以立即做出限流。每个时间窗口不会相互影响
  • 在时间单元内保障系统的稳定。保障的时间单元内系统的吞吐量上限

缺点

  • 正如图示一样,他的最大问题就是临界状态。在临界状态最坏情况会受到两倍流量请求
  • 除了临界的情况,还有一种是在一个单元时间窗内前期如果很快的消耗完请求阈值。那么剩下的时间将会无法请求。这样就会因为一瞬间的流量导致一段时间内系统不可用。这在互联网高可用的系统中是不能接受的。

 

 

 

实现

  • 好了,关于原理介绍及优缺点我们已经了解了。下面我们动手实现它
  • 首先我们在实现这种计数时,采用redis是非常好的选择。这里我们通过redis实现

controller

@RequestMapping(value = "/start",method = RequestMethod.GET)
    public Map<string,object> start(@RequestParam Map<string, object=""> paramMap) {
        return testService.startQps(paramMap);
    }

service

@Override
public Map<string, object=""> startQps(Map<string, object=""> paramMap) {
    //根据前端传递的qps上线
    Integer times = 100;
    if (paramMap.containsKey("times")) {
        times = Integer.valueOf(paramMap.get("times").toString());
    }
    String redisKey = "redisQps";
    RedisAtomicInteger redisAtomicInteger = new RedisAtomicInteger(redisKey, redisTemplate.getConnectionFactory());
    int no = redisAtomicInteger.getAndIncrement();
    //设置时间固定时间窗口长度 1S
    if (no == 0) {
        redisAtomicInteger.expire(1, TimeUnit.SECONDS);
    }
    //判断是否超限  time=2 表示qps=3
    if (no > times) {
        throw new RuntimeException("qps refuse request");
    }
    //返回成功告知
    Map<string, object=""> map = new HashMap<>();
    map.put("success", "success");
    return map;
}

结果测试

redis实现的四种常见限流策略

我们设置的qps=3 , 我们可以看到五个并发进来后前三个正常访问,后面两个就失败了。稍等一段时间我们在并发访问,前三个又可以正常访问。说明到了下一个时间窗口

redis实现的四种常见限流策略

redis实现的四种常见限流策略

 

滑动时间窗口算法

  • 针对固定时间窗口的缺点--临界值出现双倍流量问题。 我们的滑动时间窗口就产生了。
  • 其实很好理解,就是针对固定时间窗口,将时间窗口统计从原来的固定间隔变成更加细度化的单元了。
  • 在上面我们固定时间窗口演示中我们设置的时间单元是1S 。 针对1S我们将1S拆成时间戳。
  • 固定时间窗口是统计单元随着时间的推移不断向后进行。而滑动时间窗口是我们认为的想象出一个时间单元按照相对论的思想将时间固定,我们的抽象时间单元自己移动。抽象的时间单元比实际的时间单元更小。
  • 读者可以看下下面的动图,就可以理解了。

redis实现的四种常见限流策略

优点

  • 实质上就是固定时间窗口算法的改进。所以固定时间窗口的缺点就是他的优点。
  • 内部抽象一个滑动的时间窗,将时间更加小化。存在边界的问题更加小。客户感知更弱了。

缺点

  • 不管是固定时间窗口算法还是滑动时间窗口算法,他们都是基于计数器算法进行优化,但是他们对待限流的策略太粗暴了。
  • 为什么说粗暴呢,未限流他们正常放行。一旦达到限流后就会直接拒绝。这样我们会损失一部分请求。这对于一个产品来说不太友好

实现

  • 滑动时间窗口是将时间更加细化,上面我们是通过redis#setnx实现的。这里我们就无法通过他统一记录了。我们应该加上更小的时间单元存储到一个集合汇总。然后根据集合的总量计算限流。redis的zsett数据结构就和符合我们的需求。
  • 为什么选择zset呢,因为redis的zset中除了值以外还有一个权重。会根据这个权重进行排序。如果我们将我们的时间单元及时间戳作为我们的权重,那么我们获取统计的时候只需要按照一个时间戳范围就可以了。
  • 因为zset内元素是唯一的,所以我们的值采用uuid或者雪花算法一类的id生成器

controller

@RequestMapping(value = "/startList",method = RequestMethod.GET)
    public Map<string,object> startList(@RequestParam Map<string, object=""> paramMap) {
        return testService.startList(paramMap);
    }

service

@RequestMapping(value = "/startList",method = RequestMethod.GET)
    public Map<string,object> startList(@RequestParam Map<string, object=""> paramMap) {
        return testService.startList(paramMap);
    }

结果测试

redis实现的四种常见限流策略

  • 和固定时间窗口采用相同的并发。为什么上面也会出现临界状况呢。因为在代码里时间单元间隔比固定时间间隔采用还要大 。 上面演示固定时间窗口时间单元是1S出现了最坏情况。而滑动时间窗口设计上就应该间隔更短。而我设置成10S 也没有出现坏的情况
  • 这里就说明滑动比固定的优处了。如果我们调更小应该更加不会出现临界问题,不过说到底他还是避免不了临界出现的问题

 

漏桶算法

  • 滑动时间窗口虽然可以极大程度的规避临界值问题,但是始终还是避免不了
  • 另外时间算法还有个致命的问题,他无法面对突如其来的大量流量,因为他在达到限流后直接就拒绝了其他额外流量
  • 针对这个问题我们继续优化我们的限流算法。 漏桶算法应运而生

redis实现的四种常见限流策略

优点

  • 面对限流更加的柔性,不在粗暴的拒绝。
  • 增加了接口的接收性
  • 保证下流服务接收的稳定性。均匀下发

缺点

  • 我觉得没有缺点。非要鸡蛋里挑骨头那我只能说漏桶容量是个短板

实现

controller

@RequestMapping(value = "/startLoutong",method = RequestMethod.GET)
public Map<string,object> startLoutong(@RequestParam Map<string, object=""> paramMap) {
    return testService.startLoutong(paramMap);
}

service

在service中我们通过redis的list的功能模拟出桶的效果。这里代码是实验室性质的。在真实使用中我们还需要考虑并发的问题

@Override
public Map<string, object=""> startLoutong(Map<string, object=""> paramMap) {
    String redisKey = "qpsList";
    Integer times = 100;
    if (paramMap.containsKey("times")) {
        times = Integer.valueOf(paramMap.get("times").toString());
    }
    Long size = redisTemplate.opsForList().size(redisKey);
    if (size >= times) {
        throw new RuntimeException("qps refuse request");
    }
    Long aLong = redisTemplate.opsForList().rightPush(redisKey, paramMap);
    if (aLong > times) {
        //为了防止并发场景。这里添加完成之后也要验证。  即使这样本段代码在高并发也有问题。此处演示作用
        redisTemplate.opsForList().trim(redisKey, 0, times-1);
        throw new RuntimeException("qps refuse request");
    }
    Map<string, object=""> map = new HashMap<>();
    map.put("success", "success");
    return map;
}

下游消费

@Component
public class SchedulerTask {

    @Autowired
    RedisTemplate redisTemplate;

    private String redisKey="qpsList";

    @Scheduled(cron="*/1 * * * * ?")
    private void process(){
        //一次性消费两个
        System.out.println("正在消费。。。。。。");
        redisTemplate.opsForList().trim(redisKey, 2, -1);
    }

}

测试

  • 我们还是通过50并发循环10次访问。我们可以发现只有在一开始能达到比较高的吞吐量。在随后桶的容量满了之后。而下游水滴速率比上游请求速率慢的情况下。只能以下游恒定的速度接收访问。
  • 他的问题也暴露的很明显。针对时间窗口的不足漏桶进行的不足,但是仍是不足。无法彻底避免请求溢出的问题。
  • 请求溢出本身就是一种灾难性的问题。所有的算法目前都没有解决这个问题。只是在减缓他带来的问题

redis实现的四种常见限流策略

 

令牌桶算法

令牌桶和漏桶法是一样的。只不过将桶的作用方向改变了一下。

漏桶的出水速度是恒定的,如果流量突然增加的话我们就只能拒绝入池

但是令牌桶是将令牌放入桶中,我们知道正常情况下令牌就是一串字符当桶满了就拒绝令牌的入池,但是面对高流量的时候正常加上我们的超时时间就留下足够长的时间生产及消费令牌了。这样就尽可能的不会造成请求的拒绝

最后,不论是对于令牌桶拿不到令牌被拒绝,还是漏桶的水满了溢出,都是为了保证大部分流量的正常使用,而牺牲掉了少部分流量

public Map<string, object=""> startLingpaitong(Map<string, object=""> paramMap) {
        String redisKey = "lingpaitong";
        String token = redisTemplate.opsForList().leftPop(redisKey).toString();
        //正常情况需要验证是否合法,防止篡改
        if (StringUtils.isEmpty(token)) {
            throw new RuntimeException("令牌桶拒绝");
        }
        Map<string, object=""> map = new HashMap<>();
        map.put("success", "success");
        return map;
    }
@Scheduled(cron="*/1 * * * * ?")
    private void process(){
        //一次性生产两个
        System.out.println("正在消费。。。。。。");
        for (int i = 0; i < 2; i++) {
            redisTemplate.opsForList().rightPush(redisKey, i);
        }
    }

到此这篇关于基于redis实现的四种常见的限流策略的文章就介绍到这了,更多相关redis 限流策略内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Redis 相关文章推荐
浅谈Redis在直播场景的实践方案
Apr 27 Redis
Redis持久化与主从复制的实践
Apr 27 Redis
在K8s上部署Redis集群的方法步骤
Apr 27 Redis
Redis6.0搭建集群Redis-cluster的方法
May 08 Redis
详解Redis集群搭建的三种方式
May 31 Redis
使用Redis实现实时排行榜功能
Jul 02 Redis
基于Redis结合SpringBoot的秒杀案例详解
Oct 05 Redis
SpringBoot集成Redis的思路详解
Oct 16 Redis
redis缓存存储Session原理机制
Nov 20 Redis
浅谈Redis跟MySQL的双写问题解决方案
Feb 24 Redis
Redis集群节点通信过程/原理流程分析
Mar 18 Redis
Redis如何实现验证码发送 以及限制每日发送次数
Apr 18 Redis
Redis 哨兵集群的实现
Redis可视化客户端小结
Windows中Redis安装配置流程并实现远程访问功能
详解Redis复制原理
Windows下redis下载、redis安装及使用教程
深入理解redis中multi与pipeline
Jun 02 #Redis
SpringBoot 集成Redis 过程
Jun 02 #Redis
You might like
一个PHP的ZIP压缩类分享
2014/05/04 PHP
php中__destruct与register_shutdown_function执行的先后顺序问题
2014/10/17 PHP
php简单截取字符串代码示例
2016/10/19 PHP
浅谈PHP发送HTTP请求的几种方式
2017/07/25 PHP
php中isset与empty函数的困惑与用法分析
2019/07/05 PHP
js/ajax跨越访问-jsonp的原理和实例(javascript和jquery实现代码)
2012/12/27 Javascript
JS动态修改图片的URL(src)的方法
2015/04/01 Javascript
JavaScript对象学习小结
2015/09/02 Javascript
关于cookie的初识和运用(js和jq)
2016/04/07 Javascript
JavaScript的MVVM库Vue.js入门学习笔记
2016/05/03 Javascript
小程序开发实战:实现九宫格界面的导航的代码实现
2017/01/19 Javascript
解析Angular 2+ 样式绑定方式
2018/01/15 Javascript
react-native动态切换tab组件的方法
2018/07/07 Javascript
详解使用jest对vue项目进行单元测试
2018/09/07 Javascript
微信小程序swiper禁止用户手动滑动代码实例
2019/08/23 Javascript
JavaScript setTimeout()基本用法有哪些
2020/11/04 Javascript
跟老齐学Python之一个免费的实验室
2014/09/14 Python
python初学之用户登录的实现过程(实例讲解)
2017/12/23 Python
Python常用爬虫代码总结方便查询
2019/02/25 Python
PyQt5实现简易计算器
2020/05/30 Python
PyQt5的安装配置过程,将ui文件转为py文件后显示窗口的实例
2019/06/19 Python
django中使用事务及接入支付宝支付功能
2019/09/15 Python
Tensorflow获取张量Tensor的具体维数实例
2020/01/19 Python
Python使用Paramiko控制liunx第三方库
2020/05/20 Python
CSS3 按钮边框动画的实现
2020/11/12 HTML / CSS
美国职棒大联盟官方网上商店:MLBShop.com
2017/11/12 全球购物
Raffaello Network西班牙:意大利拉斐尔时尚购物网
2019/03/12 全球购物
装潢设计实习自我鉴定
2013/09/19 职场文书
化工专业应届生求职信
2013/11/08 职场文书
新郎新娘婚礼答谢词
2014/01/11 职场文书
CAD制图人员的自荐信
2014/02/07 职场文书
和睦家庭事迹
2014/05/14 职场文书
幼儿园小班见习报告
2014/10/31 职场文书
开场白怎么写
2015/06/01 职场文书
《巨人的花园》教学反思
2016/02/19 职场文书
小学教师教学反思
2016/02/24 职场文书