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过期事件实现订单超时取消
May 08 Redis
为Java项目添加Redis缓存的方法
May 18 Redis
Java Socket实现Redis客户端的详细说明
May 26 Redis
Django使用redis配置缓存的方法
Jun 01 Redis
redis客户端实现高可用读写分离的方式详解
Jul 04 Redis
解析redis hash应用场景和常用命令
Aug 04 Redis
Redis入门教程详解
Aug 30 Redis
Redis集群新增、删除节点以及动态增加内存的方法
Sep 04 Redis
Redis高并发防止秒杀超卖实战源码解决方案
Nov 01 Redis
基于Redis6.2.6版本部署Redis Cluster集群的问题
Apr 01 Redis
浅谈Redis变慢的原因及排查方法
Jun 21 Redis
Redis主从复制操作和配置详情
Sep 23 Redis
Redis 哨兵集群的实现
Redis可视化客户端小结
Windows中Redis安装配置流程并实现远程访问功能
详解Redis复制原理
Windows下redis下载、redis安装及使用教程
深入理解redis中multi与pipeline
Jun 02 #Redis
SpringBoot 集成Redis 过程
Jun 02 #Redis
You might like
也谈 PHP 和 MYSQL
2006/10/09 PHP
php array_map array_multisort 高效处理多维数组排序
2009/06/11 PHP
php递归方法实现无限分类实例代码
2014/02/28 PHP
php set_include_path函数设置 include_path 配置选项
2016/10/30 PHP
PHP如何防止XSS攻击与XSS攻击原理的讲解
2019/03/22 PHP
Javascript JSQL,SQL无处不在,
2010/05/05 Javascript
解析javascript 实用函数的使用详解
2013/05/10 Javascript
JavaScript控制各种浏览器全屏模式的方法、属性和事件介绍
2014/04/03 Javascript
javascript中parseInt()函数的定义和用法分析
2014/12/20 Javascript
node.js中的forEach()是同步还是异步呢
2015/01/29 Javascript
JavaScript中的函数声明和函数表达式区别浅析
2015/03/27 Javascript
Jquery实现顶部弹出框特效
2015/08/08 Javascript
jQuery实现的多级下拉菜单效果代码
2015/08/24 Javascript
jQuery实现淡入淡出二级下拉导航菜单的方法
2015/08/28 Javascript
javascript基础语法——全面理解变量和标识符
2016/06/02 Javascript
AngularJS 模型详细介绍及实例代码
2016/07/27 Javascript
jQuery实现邮箱下拉列表自动补全功能
2016/09/08 Javascript
jQuery实现获取元素索引值index的方法
2016/09/18 Javascript
Vue.js实现文章评论和回复评论功能
2020/05/30 Javascript
使用jquery DataTable和ajax向页面显示数据列表的方法
2018/08/09 jQuery
JavaScript 斐波那契数列 倒序输出 输出100以内的质数代码实例
2019/09/11 Javascript
js实现消灭星星(web简易版)
2020/03/24 Javascript
python网络编程学习笔记(10):webpy框架
2014/06/09 Python
Python命令行参数解析模块optparse使用实例
2015/04/13 Python
python实现自动登录人人网并采集信息的方法
2015/06/28 Python
Python根据已知邻接矩阵绘制无向图操作示例
2018/06/23 Python
python 通过 socket 发送文件的实例代码
2018/08/14 Python
Python中垃圾回收和del语句详解
2018/11/15 Python
Python实现在线批量美颜功能过程解析
2020/06/10 Python
香港太阳眼镜网上商店:SmartBuyGlasses香港
2016/07/22 全球购物
马来西亚网上购物:Youbeli
2018/03/30 全球购物
德国家具购物网站:Möbel Höffner
2019/08/26 全球购物
《小草和大树》教学反思
2014/02/16 职场文书
2015年会计年终工作总结
2015/05/26 职场文书
车辆安全隐患排查制度
2015/08/05 职场文书
SpringDataJPA实体类关系映射配置方式
2021/12/06 Java/Android