常见限流算法详细解析

常见限流算法详细解析

分布式系统中,由于接口API无法控制上游调用方的行为,因此当瞬时请求量突增时,会导致服务器占用过多资源,发生响应速度降低、超时、乃至宕机,甚至引发雪崩造成整个系统不可用。
限流,Rate Limiter,就是对API的请求量进行限制,对于超出限制部分的请求作出快速拒绝、快速失败、丢弃处理,以保证本服务以及下游资源系统的稳定。

常见的限流算法有:固定时间窗、滑动时间窗算法、漏桶算法、令牌桶算法,以下分别详解介绍。

固定时间窗口算法

生活示例

一个游戏规则:

  • 每小时只能玩5次游戏
  • 时间一到整点,计数器就会重置

举个例子:

  • 上午9:55,你已经玩了3次游戏
  • 上午10:05,你又可以玩完整的5次

这就像是一个"准点重置"的游戏规则。优点是简单直接!

算法原理

  1. 将时间划分为固定的窗口大小,例如10s
  2. 在窗口时间段内,每来一个请求,对计数器加1。
  3. 当计数器达到设定限制后,该窗口时间内的之后的请求都被丢弃处理。
  4. 该窗口时间结束后,计数器清零,从新开始计数。如上图所示,10s内限制100个请求,在第11s的时候计数器会从0重新开始计数。

如下图所示:

在这里插入图片描述

代码示例

public class FixedWindowRateLimiter {// 窗口最大请求数private final int maxRequests;// 窗口时间大小(毫秒)private final long windowSizeMillis;// 当前窗口的请求计数private int currentRequests = 0;// 窗口开始时间private long windowStartTime;public FixedWindowRateLimiter(int maxRequests, long windowSizeMillis) {this.maxRequests = maxRequests;this.windowSizeMillis = windowSizeMillis;this.windowStartTime = System.currentTimeMillis();}public synchronized boolean tryAcquire() {long currentTime = System.currentTimeMillis();// 如果当前时间已超过窗口,重置窗口if (currentTime - windowStartTime >= windowSizeMillis) {currentRequests = 0;windowStartTime = currentTime;}// 判断是否允许新请求if (currentRequests < maxRequests) {currentRequests++;return true;}return false;}
}// 短信发送限制器:每小时最多发送5条短信
FixedWindowRateLimiter smsSender = new FixedWindowRateLimiter(5, 60 * 60 * 1000);public boolean sendSMS(String phoneNumber, String message) {if (smsSender.tryAcquire()) {// 执行短信发送逻辑return true;} else {System.out.println("超过短信发送限制");return false;}
}

存在问题

边界问题

  • 在窗口切换时可能会出现流量突发
  • 例如,在一个小时的最后一秒和下一秒分别允许100个请求,实际上可能瞬间允许200个请求,所谓的2N问题,如下图所示:
    在这里插入图片描述

不够平滑

  • 流量控制不够精细
  • 容易在窗口边界产生流量不均匀的情况

​ 跨两个固定时间窗统计不准确:比如6t到16t之间也是10t大小的一个时间窗,但跨了两个固定的时间窗,问题是请求总数为110,超过阈值,这种固定时间窗无法处理这部分超出的请求,所以解决办法就是使用滑动时间窗

滑动时间窗算法

生活示例

计数器滑动时间窗口算法是计数器固定窗口算法的改进,解决了固定窗口切换时可能会产生两倍于阈值流量请求的缺点。TCP协议中数据包的传输,同样也是采用滑动窗口来进行流量控制。

想象你有一个会"移动"的时间窗口。

  • 不再是整点重置
  • 而是始终保持最近1小时内只能玩5次游戏
  • 举个栗子:
    • 9:30玩了2次
    • 9:45玩了2次
    • 10:15想玩,系统会检查9:15到10:15的记录

再举个例子:

​ 再想象你是一个售票员,需要控制游乐场的人流量。你的规则是"任意10分钟内最多允许50人进入"。如果你每2分钟清点一次人数(这就是滑动步长),就比每1秒钟都清点要轻松得多,而且准确性也足够。

原理

​ 滑动窗口算法在固定窗口的基础上,将一个计时窗口分成了若干个小窗口,然后每个小窗口维护一个独立的计数器。

当请求的时间大于当前窗口的最大时间时,则将计时窗口向前平移一个小窗口。平移时,将第一个小窗口的数据丢弃,然后将第二个小窗口设置为第一个小窗口,同时在最后面新增一个小窗口,将新的请求放在新增的小窗口中。同时要保证整个窗口中所有小窗口的请求数目之后不能超过设定的阈值。

滑动时间窗限流算法解决了固定时间窗限流算法的问题。其没有划分固定的时间窗起点与终点,而是将每一次请求的到来时间点作为统计时间窗的终点,起点则是终点向前推时间窗长度的时间点。这种时间窗称为“滑动时间窗”

在这里插入图片描述

图解

假设: 
- 时间窗口 = 10秒
- 滑动步长 = 2秒
- 最大请求 = 5个时间轴划分:
0s    2s    4s    6s    8s    10s   12s   14s
|-----|-----|-----|-----|-----|-----|-----|↑     ↑     ↑     ↑     ↑     ↑     ↑
小窗口1 窗口2  窗口3  窗口4  窗口5  窗口6  窗口7完整时间窗口(10秒):
|----------------------------------|包含5个小窗口滑动过程:
第0秒:  [窗口1|窗口2|窗口3|窗口4|窗口5]
第2秒:    [窗口2|窗口3|窗口4|窗口5|窗口6]
第4秒:      [窗口3|窗口4|窗口5|窗口6|窗口7]

代码示例

public class SlidingWindowLimiter {private final int maxRequestsAllowed;     // 允许的最大请求数private final int windowSizeInSeconds;    // 时间窗口大小(秒)private final int slideTimeInSeconds;     // 滑动步长(秒),最小时间窗格// 使用TreeMap记录每个小窗口中的请求数// Key: 窗口的开始时间戳(毫秒)// Value: 该窗口内的请求数private final TreeMap<Long, Integer> windowCounts = new TreeMap<>();public SlidingWindowLimiter(int maxRequests, int windowSize, int slideTime) {this.maxRequestsAllowed = maxRequests;this.windowSizeInSeconds = windowSize;this.slideTimeInSeconds = slideTime;}public synchronized boolean isAllowed() {long currentTime = System.currentTimeMillis();long windowStart = currentTime - (windowSizeInSeconds * 1000L);// 计算当前请求所属的小窗口的开始时间long currentSlideStart = currentTime - (currentTime % (slideTimeInSeconds * 1000L));// 清理过期的小窗口windowCounts.headMap(windowStart).clear();// 统计当前时间窗口内的总请求数int totalRequests = windowCounts.values().stream().mapToInt(Integer::intValue).sum();if (totalRequests < maxRequestsAllowed) {// 更新当前小窗口的计数windowCounts.merge(currentSlideStart, 1, Integer::sum);return true;}return false;}// 用于展示当前所有小窗口的状态public void printWindowStatus() {System.out.println("当前时间窗口状态:");windowCounts.forEach((timestamp, count) -> {String time = new SimpleDateFormat("HH:mm:ss.SSS").format(new Date(timestamp));System.out.printf("窗口开始时间:%s, 请求数:%d\n", time, count);});}
}public class Example {public static void main(String[] args) {// 创建限流器:// - 10秒的时间窗口// - 每个窗口最多允许5个请求// - 每2秒滑动一次SlidingWindowLimiter limiter = new SlidingWindowLimiter(5, 10, 2);// 模拟请求for (int i = 0; i < 8; i++) {boolean allowed = limiter.isAllowed();System.out.printf("请求 %d: %s\n", i + 1, allowed ? "通过" : "拒绝");limiter.printWindowStatus();try {Thread.sleep(1000); // 等待1秒} catch (InterruptedException e) {e.printStackTrace();}}}
}

漏桶算法

生活示例

​ 想象你在一个水桶(桶装水、净水机)底部有个小洞的水龙头下,这个小洞代表了水流出的恒定速率,无论上面的水有多少,出水速度都是固定的。即使你快速地往桶里倒水,水也只会以固定的速度从底部的小洞流出。这就是漏桶算法的生活化比喻。

算法原理

![在这里插入图片描述![在这里插入图片描述](https://i-blog.csdnimg.cn/direct/21ea20c5462d455aaeeee1a67eb82054.png)
漏桶算法的核心思想是:

  1. 请求(水)进入一个固定容量的"桶"
  2. 桶内请求以恒定速率被处理
  3. 如果桶已满,新的请求将被拒绝,桶的容量代表系统能够处理的最大并发请求数

代码示例

public class LeakyBucketRateLimiter {private final int capacity;private final int rate;private int currentWaterLevel = 0;private long lastRequestTime;// 添加拒绝策略枚举public enum LimitPolicy {REJECT,       // 直接拒绝WAIT,         // 等待DEGRADE       // 降级服务}public synchronized LimitResult tryAcquire(LimitPolicy policy) {long currentTime = System.currentTimeMillis();long timePassed = currentTime - lastRequestTime;// 计算流出的请求数int outflow = (int) (timePassed / (1000 / rate));currentWaterLevel = Math.max(0, currentWaterLevel - outflow);lastRequestTime = currentTime;// 根据不同策略处理限流 当桶已满时,拒绝新请求if (currentWaterLevel < capacity) {currentWaterLevel++;return new LimitResult(true, "请求通过");}// 根据不同策略返回结果switch (policy) {case REJECT:return new LimitResult(false, "请求被拒绝");case WAIT:// 模拟等待逻辑return new LimitResult(false, "请求需要等待");case DEGRADE:// 降级服务逻辑return new LimitResult(false, "服务降级");default:return new LimitResult(false, "未知策略");}}// 结果封装类public static class LimitResult {public final boolean allowed;public final String message;public LimitResult(boolean allowed, String message) {this.allowed = allowed;this.message = message;}}public static void main(String[] args) throws InterruptedException {LeakyBucketRateLimiter limiter = new LeakyBucketRateLimiter(10, 2);// 模拟请求for (int i = 0; i < 15; i++) {if (limiter.tryAcquire()) {System.out.println("请求" + i + ": 通过");} else {System.out.println("请求" + i + ": 被限流");}TimeUnit.MILLISECONDS.sleep(200);}}
}

优点:

  1. 算法简单,实现容易
  2. 可以平滑处理突发流量
  3. 能够有效防止系统过载

缺点:

  1. 不能充分利用系统资源
  2. 处理速率固定,不够灵活
  3. 无法应对短时间的高并发请求

令牌桶算法

生活示例

​ 取号看演出或办理业务,取到号以后才能进场,取不到号就要等待

​ 想象你有一个自动售货机,它每隔一段时间就会往机器里放入一定数量的"通行令牌"。只有拿到令牌的人才能购买商品。如果令牌被用完,就必须等待新的令牌生成。

算法原理

在这里插入图片描述

令牌桶算法的核心思想是:

  1. 有一个固定大小的令牌桶
  2. 系统以恒定速率向桶中添加令牌
  3. 每个请求需要获取一个令牌才能执行
  4. 如果桶中没有令牌,请求被拒绝或等待

代码示例

import java.util.concurrent.TimeUnit;public class TokenBucketRateLimiter {// 令牌桶最大容量private final int maxTokens;// 每秒生成的令牌数private final int tokenGenerateRate;// 当前令牌数private int currentTokens;// 上次令牌生成时间private long lastTokenTime;public TokenBucketRateLimiter(int maxTokens, int tokenGenerateRate) {this.maxTokens = maxTokens;this.tokenGenerateRate = tokenGenerateRate;this.currentTokens = maxTokens;this.lastTokenTime = System.currentTimeMillis();}public synchronized boolean tryAcquire() {long currentTime = System.currentTimeMillis();long timePassed = currentTime - lastTokenTime;// 计算这段时间内生成的令牌数int generatedTokens = (int) (timePassed * tokenGenerateRate / 1000);// 更新当前令牌数,不超过最大容量currentTokens = Math.min(maxTokens, currentTokens + generatedTokens);lastTokenTime = currentTime;// 判断是否有令牌if (currentTokens > 0) {currentTokens--;return true;}return false;}public static void main(String[] args) throws InterruptedException {TokenBucketRateLimiter limiter = new TokenBucketRateLimiter(10, 5);// 模拟请求for (int i = 0; i < 15; i++) {if (limiter.tryAcquire()) {System.out.println("请求" + i + ": 通过");} else {System.out.println("请求" + i + ": 被限流");}TimeUnit.MILLISECONDS.sleep(100);}}
}

优点:

  1. 可以处理突发流量
  2. 能更好地利用系统资源
  3. 灵活控制流量
  4. 支持预热和动态调整

缺点:

  1. 实现相对复杂
  2. 需要额外维护令牌生成和分配逻辑
  3. 可能会有一定的计算开销

总结:

介绍实现限流的几种方式,主要是窗口算法和桶算法,两者各有优势。

  • 窗口算法实现简单,逻辑清晰,可以很直观的得到当前的 QPS 情况,但是会有时间窗口的临界突变问题,而且不像桶一样有队列可以缓冲。
  • 桶算法虽然稍微复杂,不好统计 QPS 情况,但是桶算法也有优势所在。
    • 漏桶模式消费速率恒定,可以很好的保护自身系统,可以对流量进行整形,但是面对突发流量不能快速响应。
    • 令牌桶模式可以面对突发流量,但是启动时会有缓慢加速的过程,不过常见的开源工具中已经对此优化。

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

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

相关文章

java+ssm+mysql高校学籍管理系统

项目介绍&#xff1a; 使用javassmmysql开发的高校学籍管理系统&#xff0c;系统包含超级管理员&#xff0c;系统管理员、教师、学生角色&#xff0c;功能如下&#xff1a; 超级管理员&#xff1a;管理员管理&#xff08;可以新增管理员&#xff09;&#xff1b;专业管理&…

(5)JS-Clipper2之PolyNode

1. 描述 PolyNodes是被封装在PolyTree的容器中&#xff0c;同时提供了一个数据结构来代表由Excute()方法返回的多边形轮廓中的父子关系。 一个PolyNode对象代表一个多边形&#xff1b;它的“IsHole”属性表明它是一个“外轮廓”还是一个“内孔”&#xff0c;PolyNodes可能包含…

Java项目实战II基于微信小程序的无中介租房系统(开发文档+数据库+源码)

目录 一、前言 二、技术介绍 三、系统实现 四、核心代码 五、源码获取 全栈码农以及毕业设计实战开发&#xff0c;CSDN平台Java领域新星创作者&#xff0c;专注于大学生项目实战开发、讲解和毕业答疑辅导。 一、前言 随着城市化进程的加速&#xff0c;租房市场日益繁荣&a…

MATLAB稀疏感知图像和体数据恢复的系统对象研究

稀疏感知图像和体数据恢复是一种用于恢复损坏、噪声或不完整的图像和体数据的技术。它利用了信号的稀疏性&#xff0c;即信号在某种基础下可以用较少的非零系数表示&#xff0c;从而实现高质量的恢复。 在进行稀疏感知图像和体数据恢复的研究时&#xff0c;需要定义一些系统对…

安卓调试环境搭建

前言 前段时间电脑重装了系统&#xff0c;最近准备调试一个apk&#xff0c;没想到装环境的过程并不顺利&#xff0c;很让人火大&#xff0c;于是记录一下。 反编译工具下载 下载apktool.bat和apktool.jar 官网地址&#xff1a;https://ibotpeaches.github.io/Apktool/install…

【工具】音频文件格式转换工具

找开源资源、下载测试不同库的效果&#xff0c;然后找音频、下载音频、编写代码、测试转换、流程通畅。写一个工具花的时间越来越多了&#xff01;这个 5 天 这个工具是一个音频文件格式转换工具&#xff0c;支持对 mp3.aac.wav.caf.flac.ircam.mp2.mpeg.oga.opus.pcm.ra.spx.…

在ARM Linux应用层下使用SPI驱动WS2812

文章目录 1、前言2、结果展示3、接线4、SPI驱动WS2812原理4.1、0码要发送的字节4.2、1码要发送的字节4.3、SPI时钟频率 5、点亮RGB5.1、亮绿灯5.2、亮红灯5.3、亮蓝灯5.4、完整程序 6、RGB呼吸灯7、总结 1、前言 事情是这样的&#xff0c;前段时间&#xff0c;写了一个基于RK3…

BERT:用于语言理解的深度双向 Transformer 的预训练。

文章目录 0. 摘要1. 介绍2. 相关工作2.1 无监督的基于特征的方法2.3 无监督微调方法2.3 从受监督数据中迁移学习 3. BERT3.1 预训练 BERT3.2 微调 BERT 4. 实验4.1 GLUE4.2 SQuAD v1.14.3 SQuAD v2.04.4 SWAG 5. 消融研究5.1 预训练任务的影响5.2 模型大小的影响5.3 使用 BERT …

在算网云平台云端在线部署stable diffusion (0基础小白超详细教程)

Stable Diffusion无疑是AIGC领域中的AI绘画利器&#xff0c;具有以下显著优势&#xff1a; 1、开源性质&#xff0c;支持本地部署 2、能够实现对图像生成过程的精确控制 虽然SD在使用上有很多的有点&#xff0c;但缺点也是不言而喻的&#xff0c;由于AI绘画的整个过程以及现…

设计模式——Chain(责任链)设计模式

摘要 责任链设计模式是一种行为设计模式&#xff0c;通过链式调用将请求逐一传递给一系列处理器&#xff0c;直到某个处理器处理了请求或所有处理器都未能处理。它解耦了请求的发送者和接收者&#xff0c;允许动态地将请求处理职责分配给多个对象&#xff0c;支持请求的灵活传…

macOS 15.1.1 (24B2091) 系统中快捷键符号及其代表的按键的对照表

以下是 macOS 15.1.1 (24B2091) 系统中快捷键符号及其代表的按键的对照表&#xff1a; 符号按键名称描述⌘Command (Cmd)常用的功能键&#xff0c;用于执行大多数快捷操作。⌥Option (Alt)Option 键&#xff0c;常用于辅助操作和特殊字符输入。⇧ShiftShift 键&#xff0c;常用…

el-table一键选择全部行,切换分页后无法勾选

el-table一键全选&#xff0c;分页的完美支持 问题背景尝试解决存在问题问题分析 解决方案改进思路如下具体代码实现如下 问题背景 现在有个需求&#xff0c;一个表格有若干条数据(假设数量大于20&#xff0c;每页10条&#xff0c;保证有2个以上分页即可)。 现在需要在表格上方…

【55 Pandas+Pyecharts | 实习僧网Python岗位招聘数据分析可视化】

文章目录 &#x1f3f3;️‍&#x1f308; 1. 导入模块&#x1f3f3;️‍&#x1f308; 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 去除重复数据2.4 调整部分城市名称 &#x1f3f3;️‍&#x1f308; 3. Pyecharts数据可视化3.1 招聘数量前20岗位3.2 各城市招聘数量3…

【赵渝强老师】PostgreSQL的控制文件

PostgreSQL数据库的物理存储结构主要是指硬盘上存储的文件&#xff0c;包括&#xff1a;数据文件、日志文件、参数文件、控制文件、WAL预写日志文件等等。 下面重点讨论一下PostgreSQL的控制文件。 视频讲解如下 【赵渝强老师】PostgreSQL的控制文件 控制文件记录了数据库运行…

在做题中学习(79):最小K个数

解法&#xff1a;快速选择算法 说明&#xff1a;堆排序也是经典解决问题的算法&#xff0c;但时间复杂度为&#xff1a;O(NlogK)&#xff0c;K为k个元素 而将要介绍的快速选择算法的时间复杂度为: O(N) 先看我的前两篇文章&#xff0c;分别学习&#xff1a;数组分三块&#…

连续大涨,汉王科技跑步进入AI应用舒适区

OpenAI正在进行的“12天12场直播”让行业再次沸腾&#xff0c;二级市场也在寻找AI应用的机会。这刺激了12月首周同花顺sora概念涨超11&#xff05;&#xff0c;远超同期大盘指数涨幅。 截至目前&#xff0c;“满血版”推理模型o1和月收费高达200美元的ChatGPT Pro订阅服务&…

[MySQL基础](三)SQL--图形化界面+DML

本专栏内容为&#xff1a;MySQL学习专栏 &#x1f493;博主csdn个人主页&#xff1a;小小unicorn ⏩专栏分类&#xff1a;MySql &#x1f69a;代码仓库&#xff1a;小小unicorn的代码仓库&#x1f69a; &#x1f339;&#x1f339;&#x1f339;关注我带你学习编程知识 目录 图…

基于单片机的智能灯光控制系统

摘要 现在的大部分的大学&#xff0c;都是采用了一种“绿色”的教学方式&#xff0c;再加上现在的大学生缺乏环保意识&#xff0c;所以在学校里很多的教室&#xff0c;在白天的时候灯都会打开&#xff0c;这是一种极大的浪费&#xff0c;而且随时都有可能看到&#xff0c;这是…

解决Windows与Ubuntu云服务器无法通过Socket(udp)通信问题

今天在写Socket通信代码的时候&#xff0c;使用云服务器自己与自己通信没有问题&#xff0c;但是当我们把客户端换为Windows系统的时候却无法发送信息到Linux当中&#xff0c;耗时一上午终于搞定了&#x1f612;。 问题&#xff1a; 如上图&#xff0c;当我在windows的客户端…

帝可得-运营管理App

运营管理App Android模拟器 本项目的App客户端部分已经由前端团队进行开发完成&#xff0c;并且以apk的方式提供出来&#xff0c;供我们测试使用&#xff0c;如果要运行apk&#xff0c;需要先安装安卓的模拟器。 可以选择国内的安卓模拟器产品&#xff0c;比如&#xff1a;网…