时间滑动窗口限流

限流算法——时间滑动窗口

背景:

在当今的微服务架构中,会存在流量剧增的情况,需要适当的限流才能保证我们服务不会被打崩,因此一些限流组件就随之诞生,主流的接口限流组件,如 spring cloud alibaba sentinel等,开源限流工具包,如 guava 等。

本篇文章,主要通过手撕代码对时间滑动窗口限流算法实现。

常见限流算法:

限流算法是一种用于控制数据流速率的方法,以防止系统过载或保证服务质量。常见的限流算法包括固定窗口算法、滑动窗口算法、漏桶算法、漏桶算法和令牌桶算法

  1. 固定窗口算法。这是一种简单的计数器算法,它在固定的时间窗口内累加访问次数。当访问次数达到设定的阈值时,触发限流策略。这种方法在每个新的时间窗口开始时进行计数器的清零。
  2. 滑动窗口算法。滑动窗口算法是对固定窗口算法的改进,它将时间窗口分为多个小周期,每个小周期都有自己的计数器。随着时间的滑动,过期的小周期数据被删除,这样可以更精确地控制流量。
  3. 漏桶算法。漏桶算法则是一种更加平滑的限流方式,它以固定的速率处理请求,就像漏桶以一定的速率释放水滴一样。如果请求速率超过漏桶的释放速率,则超出部分的请求会被丢弃。
  4. 令牌桶算法。令牌桶算法中,系统以恒定的速率向桶中添加令牌,每个请求在处理前都需要从桶中获取一个令牌。如果桶中没有足够的令牌,则请求会被限流。

这些算法各有优缺点,适用于不同的场景和需求。例如,固定窗口和滑动窗口算法适合于QPS限流和统计总访问量,而漏桶和令牌桶算法则更适合于保证请求处理的平滑性和速率限制。

业务场景:

需求:需要针对单个设备,限制设备在固定时间内上报消息的频率,所以需要在设备发送消息前判断当前设备是否超出了限流标准,如果超过了标准就需要限制发送。

时间滑动窗口算法:

例如需求是十秒钟限制请求数100,

格子(时间):

我们可以将时间转为秒,看作一个个格子,将一秒钟看作一个格子,当同一秒钟新来的请求就将当前格子中的计数加一,随着时间的推移,到下一秒钟就创建出新的格子,这样我们就可以得到一个长链表,里面是随着时间推移,一个个记录请求数的格子。

窗口:

我们需求是计算十秒钟的请求是否超限,最新的十个格子,也就是10秒钟就是我们当前的窗口大小。我们只需要计算当前窗口内的请求总数是否超过了限制即可。

滑动窗口:

随之时间推移,会不断的创建新的格式,我们需要计算的窗口也会随着时间不断向后滑动,只包含当前最新的十个格子。

如下图图示,会建立一个长链表,其中包括当前的时间窗口以及过期的时间格子。判断是否超限只需计算当前滑动窗口内的所有请求数之和即可。

image-20240330164433004

缺陷:

随着时间推移,会不断创建新的格子,此时链表就会越来越长,并且过期的时间格子永远也用不到了,所以需要将过期的时间格子进行清除,并且最新的格子也需要一直创建,此时就需要一个异步任务一直来创建和清除格子,无论是在时间上还是空间上都是比较消耗性能。

有没有优化方法?

答案是有的,环形数组。

我们可以根据当前的窗口来创建一个环形的数组,随着时间的推移,新的格子会将原来个格子覆盖掉,也不需要开启异步线程专门用来创建和清除格子。

image-20240330165351457

环形数组

所谓环形数组,我们可以使用数组 + 取余 的方式来实现。

可以通过对当前时间取余的方式,确定当前时间格子的下标。

此时我们还需要维护一个变量,上次请求时间lastTime,需要与当前时间now进行比较,来决定我们计数的策略。

会有以下三种情况:

(1)情况一:lastTime 和 now 在同一秒钟,该情况,只需要继续沿用上次请求创建的格子,格子中请求数+1即可。

环形数组情况1

(2)情况二:当前时间和上次请求记录时间不在同一格子,但是在同一个周期中,没有超过一个周期。

环形数组情况2

该情况,从当前时间now计算时间窗口,保留在同一个周期内的格子, (now -> lastTime] 之间的格子,即绿色的格子,里面存储的数据是同一个周期内的;而从 (lastTime -> now] 之间的格子已经是上一个周期,已经淘汰,需要将其重置为0;并且将now 当前的格子数置为1,重新开始计数当前格子。

(3)情况三:当前时间和上次请求记录时间已经超过了一个周期中,已经超过了环形数组中的一圈。

环形数组情况-3

此时就需要将当前窗口内的所有格子进行清空,重新开始计数。

(4)情况四:异常情况,时钟回拨,可能是服务器时间被人修改,往前修改了,导致时间出错,lastTime > now,需要重新统计。

java代码实现:

package com.company.limit;import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicIntegerArray;
import java.util.concurrent.atomic.AtomicLong;/*** 时间滑动窗口算法* 限流算法*/
public class TimeSlidingWindow {/*** 限流上线次数*/private Integer limitMaxNum;/*** 滑动窗口格子数* 根据时间来,60秒分成60格*/private Integer windowNum;/*** 上次统计时间,单位秒*/private ConcurrentHashMap<String, AtomicLong> lastTimeMap;/*** 记录key值与时间窗口映射*/private ConcurrentHashMap<String, AtomicIntegerArray> timeWindowsMap;/*** 记录key值与窗口内请求总数映射*/private ConcurrentHashMap<String, AtomicInteger> timeCountMap;public TimeSlidingWindow(int limitMaxNum, int windowNum) {this.timeWindowsMap = new ConcurrentHashMap<>();this.timeCountMap = new ConcurrentHashMap<>();this.windowNum = windowNum;this.limitMaxNum = limitMaxNum;this.lastTimeMap = new ConcurrentHashMap<>();}/*** 限流方法入口* @param key* @return*/public Boolean limit(String key) {// 获取当前窗口AtomicIntegerArray windows = this.timeWindowsMap.computeIfAbsent(key, k -> new AtomicIntegerArray(this.windowNum));// 获取当前窗口请求总和AtomicInteger count = timeCountMap.computeIfAbsent(key, k -> new AtomicInteger(0));AtomicLong lastTime = lastTimeMap.computeIfAbsent(key, k -> new AtomicLong(System.currentTimeMillis() / 1000));// 计算当前时间所处格子Long now = System.currentTimeMillis() / 1000;int temp = (int) (now % this.windowNum);// 计算当前时间与上次请求时间差,用于刷新窗口Long diffTime = now - lastTime.get();//        System.out.println("now:" + now);
//        System.out.println("lastTime:" + lastTime.get());// 将锁的粒度缩小单个value节点synchronized (windows) {if (diffTime >= 0 && now.equals(lastTime.get())) {/*(1)当前时间所属格子与上次请求记录在同一个格子中该情况,只需要继续沿用上次请求创建的格子,格子中请求数++*/windows.getAndAdd(temp, 1);count.addAndGet(1);} else if (diffTime >= 0 && diffTime < windowNum) {/*(2)当前时间和上次请求记录时间在同一个周期中,环形数组的同一个周期中,没有超过一个周期。该情况意味着,从当前时间now计算时间窗口内请求数,只需要保留并计算 (now -> last) 之间的格子;而从(last -> now) 之间的格子已经淘汰,需要将其重置为0;并且将now 当前的格子数置为1,重新开始计数当前格子。*/count = clearExpireWindows(windows, (int) (lastTime.get() % this.windowNum), temp, count);windows.set(temp, 1);count.addAndGet(1);} else if (diffTime >= 0 && diffTime >= this.windowNum) {/*(3)当前时间和上次请求记录时间不在同一个周期中,已经超过了环形数组中的一圈。意味着,之前统计的*/windows = new AtomicIntegerArray(this.windowNum);windows.set(temp, 1);count.set(1);} else {/*(4)异常情况,时钟回拨,可能是服务器时间被人修改,往前修改了,导致时间出错,需要重新统计*/System.out.println("时钟回拨,时间异常,重新开启限流统计");this.timeWindowsMap = new ConcurrentHashMap<>();this.timeCountMap = new ConcurrentHashMap<>();return true;}lastTime.set(now);// 如果限流了,这次计数需要回退if (count.get() > this.limitMaxNum) {windows.getAndAdd(temp, -1);count.addAndGet(-1);return false;}}return true;}/*** 清除过期数据* @param windows   需要清除的窗口* @param from      开始位置* @param to        结束位置* @param count     当前周期计算总和* @return*/private AtomicInteger clearExpireWindows(AtomicIntegerArray windows, int from, int to, AtomicInteger count) {if (to == from) {count.addAndGet(1);return count;}// 调整下标值,若结束位置小于开始位置,则说明当前格子位于下一个周期中。if (to < from) {to = this.windowNum + to;}while (++from <= to) {int window = windows.get(from % this.windowNum);count.addAndGet(-window);windows.set(from % this.windowNum, 0);}return count;}public static void main(String[] args) {TimeSlidingWindow timeSlidingWindow = new TimeSlidingWindow(5, 10);new Thread(() -> {int i = 0;while (i < 600) {Boolean limit = timeSlidingWindow.limit("/hello");System.out.println("/hello" + i + ":" + limit + ", 时间:" + (i * 300.0) / 1000.0);try {Thread.sleep(300);} catch (InterruptedException e) {e.printStackTrace();}i++;}}).start();new Thread(() -> {int i = 0;while (i < 600) {Boolean limit1 = timeSlidingWindow.limit("/world");System.out.println("/world" + i + ":" + limit1 + ", 时间:" + (i * 500.0) / 1000.0);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}i++;}}).start();}
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/781437.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

图的基础和图的遍历(--蓝桥云)

图的基础概念 度数&#xff1a;出边入边的条数 有向边&#xff1a;有箭头 图的存储方式 //邻接表 List<int []> list[N] list<x>//存放x的所有出点的信息 list[i][j]{first,second}//其中first表示从i出发的某个出点的编号&#xff08;这个出点是i的第j个出点&…

记录一个写自定义Flume拦截器遇到的错误

先说结论&#xff1a; 【结论1】配置文件中包名要写正确 vim flume1.conf ... a1.sources.r1.interceptors.i1.type com.atguigu.flume.interceptor.MyInterceptor2$MyBuilder ... 标红的是包名&#xff0c;表黄的是类名&#xff0c;标蓝的是自己加的内部类名。这三个都…

百度蜘蛛池平台在线发外链-原理以及搭建教程

蜘蛛池平台是一款非常实用的SEO优化工具&#xff0c;它可以帮助网站管理员提高网站的排名和流量。百度蜘蛛池原理是基于百度搜索引擎的搜索算法&#xff0c;通过对网页的内容、结构、链接等方面进行分析和评估&#xff0c;从而判断网页的质量和重要性&#xff0c;从而对网页进行…

前端学习--品优购项目

文章目录 前端学习--品优购项目1.案例铺垫文件建立与命名必备文件网站favicon图标网站TDK三大标签SEO优化常用命名 2.LOGO SEO优化3.实际代码4.申请免费域名 前端学习–品优购项目 1.案例铺垫 文件建立与命名 一个项目中为了方便实用和查找内容会有多个文件夹&#xff0c;比如…

windows10搭建reactnative,运行android全过程

环境描述 win10,react-native-cli是0.73&#xff0c;nodeJS是20&#xff0c;jdk17。这都是完全根据官网文档配置的。react-native环境搭建windows。当然官网文档会更新&#xff0c;得完全按照配置来安装&#xff0c;避免遇到环境不兼容情况。 安装nodeJS并配置 这里文档有详…

如何制作Word模板并用Java导出自定义的内容

1前言 在做项目时会按照指定模板导出word文档,本文讲解分析需求后,制作word模板、修改模板内容,最终通过Java代码实现按照模板自定义内容的导出。 2制作word模板 2.1 新建word文档 新建word文档,根据需求进行编写模板内容,调整行间距和段落格式后将指定替换位置留空。…

文件操作(随机读写篇)

1. 铺垫 建议先看&#xff1a; 文件操作&#xff08;基础知识篇&#xff09;-CSDN博客 文件操作&#xff08;顺序读写篇&#xff09;-CSDN博客 首先要指出的是&#xff0c;本篇文章中的“文件指针”并不是指FILE*类型的指针&#xff0c;而是类似于打字时的光标的东西。 打…

竞赛 python+深度学习+opencv实现植物识别算法系统

0 前言 &#x1f525; 优质竞赛项目系列&#xff0c;今天要分享的是 &#x1f6a9; 基于深度学习的植物识别算法研究与实现 &#x1f947;学长这里给一个题目综合评分(每项满分5分) 难度系数&#xff1a;4分工作量&#xff1a;4分创新点&#xff1a;4分 &#x1f9ff; 更多…

154 Linux C++ 通讯架构实战9 ,信号功能添加,信号使用sa_sigaction 回调,子进程添加,文件IO详谈,守护进程添加

初始化信号 使用neg_init_signals(); 在nginx.cxx中的位置如下 //(3)一些必须事先准备好的资源&#xff0c;先初始化ngx_log_init(); //日志初始化(创建/打开日志文件)&#xff0c;这个需要配置项&#xff0c;所以必须放配置文件载入的后边&#xff1b;//(4)一些初…

Web应用安全攻防战:识别十大威胁,掌握防护要点

OWASP&#xff08;Open Worldwide Application Security Project&#xff09;是一家致力于应用安全威胁研究的非盈利机构。通过对超过20万个组织进行调研分析&#xff0c;该机构每三年左右就会发布一次《Web应用安全风险Top10》报告&#xff0c;这个报告已经成为全球企业开展We…

Redis入门三(主从复制、Redis哨兵、Redis集群、缓存更新策略、缓存穿透、缓存击穿、缓存雪崩)

文章目录 一、主从复制1.单例redis存在的问题2.主从复制是什么&#xff1f;3.主从复制的原理4.主从搭建1&#xff09;准备工作2&#xff09;方式一3&#xff09;方式二 5.python中操作1&#xff09;原生操作2&#xff09;Django的缓存操作 二、Redis哨兵&#xff08;Redis-Sent…

Lilishop商城(windows)本地部署【docker版】

Lilishop商城&#xff08;windows&#xff09;本地部署【docker版】 部署官方文档&#xff1a;LILISHOP-开发者中心 https://gitee.com/beijing_hongye_huicheng/lilishop 本地安装docker https://docs.pickmall.cn/deploy/win/deploy.html 命令端页面 启动后docker界面 注…

第十四章 MySQL

一、MySQL 1.1 MySql 体系结构 MySQL 架构总共四层&#xff0c;在上图中以虚线作为划分。 1. 最上层的服务并不是 MySQL 独有的&#xff0c;大多数给予网络的客户端/服务器的工具或者服务都有类似的架构。比如&#xff1a;连接处理、授权认证、安全等。 2. 第二层的架构包括…

三个表的联合查询的场景分析-场景4:c表维护a和b表的id关联关系(一对多)

基础SQL演练&#xff0c;带详细分析&#xff0c;笔记和备忘。 目录 背景介绍 表数据 需求1&#xff1a;查询g表所有记录&#xff0c;以及关联的h的id 需求2&#xff1a;在需求1基础上&#xff0c;查出关联的h的其它字段&#xff08;name&#xff09; 需求3&#xff1a;在需…

AR智能眼镜解决方案_MTK平台安卓主板硬件芯片方案开发

AR智能眼镜&#xff0c;是一个可以让现场作业更智能的综合管控设备。采用移动互联网、大数据和云计算等技术&#xff0c;现场数据的采集与分析&#xff1b;同时实现前端现场作业和后端管理的实时连动、信息的同步传输与存储。让前端现场作业更加智能&#xff0c;后端管理更加高…

项目安全性与权限管理实践与探讨

✨✨谢谢大家捧场&#xff0c;祝屏幕前的小伙伴们每天都有好运相伴左右&#xff0c;一定要天天开心哦&#xff01;✨✨ &#x1f388;&#x1f388;作者主页&#xff1a; 喔的嘛呀&#x1f388;&#x1f388; 目录 引言 一. 身份验证和授权 二. 输入验证和过滤 2.1. 添加O…

C语言-printf和scanf的区别详解

fprintf&#xff08;指定的格式写到文件里面。适用于所有的输出流&#xff0c;可以打印在屏幕上面&#xff09;fscanf&#xff08;指定的格式读取出来&#xff0c;适用于所有的输入流&#xff09; fprintf&#xff08;指定的格式写到文件里面&#xff09; 两个函数是一样的 打开…

广和通发布基于高通高算力芯片的具身智能机器人开发平台Fibot

3月29日&#xff0c;为助力机器人厂商客户快速复现及验证斯坦福Mobile ALOHA机器人的相关算法&#xff0c;广和通发布具身智能机器人开发平台Fibot。作为首款国产Mobile ALOHA机器人的升级配置版本&#xff0c;开发平台采用全向轮底盘设计、可拆卸式训练臂结构&#xff0c;赋予…

黑马鸿蒙笔记2

1.图片设置&#xff1a; 1 加载网络图片&#xff0c;申请权限。 申请权限&#xff1a;entry - src - resources - module.json5 2 加载本地图片 ,两种加载方式 API 鼠标悬停在Image&#xff0c; 点击show in API Reference interpolation&#xff1a;看起来更加清晰 resou…

【JavaSE】java刷题--数组练习

前言 本篇讲解了一些数组相关题目&#xff08;主要以代码的形式呈现&#xff09;&#xff0c;主要目的在于巩固数组相关知识。 上一篇 数组 讲解了一维数组和二维数组的基础知识~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎…