系统设计 - 设计一个速率限制器
实施速率限制器的位置主要取决于我们的应用程序、技术栈、技术团队等因素。通常有三个位置可供选择:客户端、服务器端或中间件。
客户端是不可靠的地方来执行速率限制,因为恶意行为者可以轻易伪造客户端请求。
比将速率限制器放在服务器端更好的方法是使用速率限制器中间件,它甚至可以对我们的服务器端进行限流。因此,如果您正在使用微服务架构,并且已经使用类似身份验证中间件的功能,则可以在其旁边实现速率限制器中间件。

有许多速率限制算法可供选择:我们将介绍一些算法,包括“令牌桶”、“漏桶”、“滑动窗口计数器”等。
令牌桶算法
Stripe (点击此处阅读)[1] 使用令牌桶算法来限制他们的 API 请求。
根据 Stripe 技术博客:
我们使用令牌桶算法来执行速率限制。这个算法有一个中央的桶主机,你每次请求时都从中取出令牌,并慢慢地向桶中滴入更多令牌。如果桶是空的,就拒绝请求。在我们的情况下,每个 Stripe 用户都有一个桶,每次他们发出请求时,我们就从那个桶中删除一个令牌。我们使用 Redis 实现我们的速率限制器。

桶中有预定义容量的令牌。当请求到达时,它从桶中取出一个令牌并进一步处理。如果没有令牌可供获取,则请求将被丢弃,用户将不得不重试。
示例用例:
•我们将速率限制规则设置为每个用户每分钟 3 个请求。•用户1在 00 时间间隔发出请求,当前桶已满,有 3 个令牌,因此请求将被处理。现在桶中的令牌数量更新为 2。•用户1在第 10 秒发出第二个请求,桶中有 2 个令牌,所以请求将进一步处理。现在桶中的令牌数量更新为 1。•用户1在第 30 秒发出第三个请求,桶中有 1 个令牌,所以请求将进一步处理。现在整整 1 分钟内桶都为空。•用户1在第 55 秒发出第四个请求,桶中没有令牌,因此请求被限制,用户将收到 429 状态码 - 请求太多,并被要求稍后重试。HTTP 429 Too Many Requests 响应状态码表示用户在给定的时间内发送了太多请求。•在那 1 分钟的完成时,令牌计数以固定速率刷新,并且桶再次装满了 3 个令牌,可以再次为该特定用户处理请求。
简单来说:
在令牌桶算法中,我们每次请求都会从桶中处理一个令牌。新令牌以速率 r 放入桶中。桶最多可以容纳 b 个令牌。如果请求到来时桶已满,则该请求将被丢弃。
令牌桶算法需要两个参数:

令牌桶算法代码示例
package mainimport ("fmt""math""time"
)const (MAX_BUCKET_SIZE float64 = 3REFILL_RATE int = 1
)type TokenBucket struct {currentBucketSize float64lastRefillTimestamp int
}func (tb *TokenBucket) allowRequest(tokens float64) bool {tb.refill() //refill of bucket happening at constant REFILL_RATEif tb.currentBucketSize >= tokens {tb.currentBucketSize -= tokensreturn true}return false
}func getCurrentTimeInNanoseconds() int {return time.Now().Nanosecond()
}func (tb *TokenBucket) refill() {nowTime := getCurrentTimeInNanoseconds()tokensToAdd := (nowTime - tb.lastRefillTimestamp) * REFILL_RATE / 1e9tb.currentBucketSize = math.Min(tb.currentBucketSize+float64(tokensToAdd), MAX_BUCKET_SIZE)tb.lastRefillTimestamp = nowTime
}func main() {obj := TokenBucket{currentBucketSize: 3,lastRefillTimestamp: 0,}fmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //truefmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //truefmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //truefmt.Printf("Request processed: %v\n", obj.allowRequest(1)) //false, request dropped
}
Leaky Bucket Algorithm
Leaky Bucket是使用队列实现速率限制的一种简单直观的方式。它是一个先进先出的队列(FIFO)。进来的请求被附加到队列中,如果没有足够的空间容纳新的请求,则会被丢弃(泄漏)。

•当一个请求到达时,系统检查队列是否已满。如果没有满,则将请求添加到队列中。•否则,请求将被丢弃。•请求以固定的间隔从队列中提取并处理。
算法的工作原理:

泄漏桶算法接受以下两个参数:
•桶大小:等于队列大小。队列保存要以固定速率处理的请求。•流出速率:定义在固定速率内可以处理多少请求,通常以秒为单位。
该算法的优点是,它平滑处理请求的突发,并以大致平均的速率处理它们。
Sliding Window Algorithm
•该算法跟踪请求的时间戳。时间戳数据通常保存在缓存中,例如 Redis 的有序集合。•当一个新请求到来时,删除所有过期的时间戳。过期的时间戳被定义为早于当前时间窗口的开始时间。•将新请求的时间戳添加到日志中。•如果日志大小与允许的请求数相同或更低,则接受请求。否则,拒绝该请求。

在下面的示例中,速率限制器允许我们每分钟2个请求。窗口内超出此限制的请求将被丢弃。

注意: 为了便于理解,我们使用了hh:mm:ss格式,但在redis中,我们将推送UNIX时间戳。
•当一个新请求在1:00:01到达时,日志为空。因此,请求被允许。•一个新请求在1:00:30到达,时间戳1:00:30被插入日志中。插入后,日志大小为2,不大于允许的请求数。因此,请求被允许。•一个新请求在1:00:50到达,时间戳被插入到日志中。插入后,日志大小为3,大于允许的大小2。因此,该请求被拒绝,即使时间戳仍然存在于日志中。•一个新请求在1:01:40到达。在[1:00:40,1:01:40)范围内的请求在最新时间范围内,但在1:00:40之前发送的请求已过时。两个过时的时间戳1:00:01和1:00:30从日志中删除。删除操作后,日志大小变为2;因此,该请求被接受。
速率限制器的详细设计:
我们将使用以下组件:
•配置和数据存储来存储速率限制器规则。•工作进程经常从磁盘中获取规则并将其存储在缓存中。•速率限制器中间件从缓存中获取规则。它还从Redis中获取时间戳、计数器等数据。当请求到来时,它进行计算并决定是处理该请求还是对其进行速率限制。•如果需要对请求进行速率限制,这里有两个选项•选项1:拒绝请求并向客户端发送状态代码429: too many requests。•选项2:将请求推送到消息队列以稍后处理该请求。

现在,我们可能需要将以上系统扩展到分布式环境中,以支持多个服务器和并发线程。这里可能会出现两个挑战:
竞争条件
如上所述:
从Redis中读取计数器值,检查(计数器+1)是否超过阈值。如果没有,将计数器值在Redis中增加1。

假设Redis中的计数器值为3(如上图所示)。如果两个请求并发读取计数器值,而在任一请求写回值之前,都不检查另一个线程。它们都会将计数器增加1并写回,而不会检查其他线程。两个请求(线程)都认为它们有正确的计数器值4。但是,正确的计数器值应该是5。
锁可以在这里使用,但这可能会影响性能。因此,我们可以使用Redis Lua脚本[2]。
•这可能会导致更好的性能。•此外,脚本中的所有步骤都以原子方式执行。在脚本执行时,没有其他Redis命令可以运行。
同步
在分布式环境中,同步是另一个需要考虑的重要因素。为了支持数百万用户,一个速率限制服务器可能无法处理流量。当使用多个速率限制服务器时,需要同步。
一种可能的解决方案是使用黏性会话,允许客户端将流量发送到相同的速率限制器。这种解决方案既不可扩展也不灵活。更好的方法是使用像Redis这样的集中式数据存储。
在所有事情都就绪之后,我们还可以监视我们的速率限制器以获取性能、规则、算法有效性等指标。例如,如果速率限制规则过于严格,会丢弃许多有效请求。在这种情况下,我们希望稍微放松一下规则。在另一个例子中,我们注意到当有突然增加的流量,如抢购时,我们的速率限制器变得无效。在这种情况下,我们可以更改算法以支持突发流量。令牌桶在这里非常合适。
引用链接
[1]
(点击此处阅读): https://stripe.com/blog/rate-limiters[2]
Redis Lua脚本: https://www.freecodecamp.org/news/a-quick-guide-to-redis-lua-scripting/
相关文章:

系统设计 - 设计一个速率限制器
实施速率限制器的位置主要取决于我们的应用程序、技术栈、技术团队等因素。通常有三个位置可供选择:客户端、服务器端或中间件。 客户端是不可靠的地方来执行速率限制,因为恶意行为者可以轻易伪造客户端请求。 比将速率限制器放在服务器端更好的方法是使…...

[技术分享]Android平台实时音视频录像模块设计之道
实现背景 录像有什么难的?无非就是数据过来,编码保存mp4而已,这可能是好多开发者在做录像模块的时候的思考输出。是的,确实不难,但是做好,或者和其他模块有非常好的逻辑配合,确实不容易。 好多…...
JDKMissionControl官方用户指南--人工翻译
1. JMC8新增功能 暂时用不到,暂略 2. JDK Mission Control是什么 JMC是一组高级工具,用于管理、监视、分析Java应用程序并排除其故障。JMC能够对代码性能、内存和延迟等领域进行高效而详细的数据分析,而不会引入通常与分析和监控工具相关的…...

MySql-高级(分库分表问题简析) 学习笔记
文章目录 1. 为什么要分库分表?2. 用过哪些分库分表中间件?不同的分库分表中间件都有什么优点和缺点?3. 你们具体是如何对数据库如何进行垂直拆分或水平拆分的?4. 分库分表时,数据迁移方案5. 如何设计可以动态扩容缩容…...

【5.20】五、安全测试——安全测试工具
目录 5.4 常见的安全测试工具 1. Web漏洞扫描工具——AppScan 2. 端口扫描工具——Nmap 3. 抓包工具——Fiddler 4. Web渗透测试工具——Metasploit 小提示:Kali Linux 5.4 常见的安全测试工具 安全测试是一个非常复杂的过程,测试所使用到的工具也…...
【13900k】i9 核显升级驱动
这里写自定义目录标题 官方的助手不能用显卡控制中心提示最新的更新搜索显卡 intel uhd graphics 770 手动下载安装自定义音频为啥也要卸载?新的改变功能快捷键合理的创建标题,有助于目录的生成如何改变文本的样式插入链接与图片如何插入一段漂亮的代码片…...
使用Python将绿色转换为红色、红色转换为蓝色的图像处理
使用Python将绿色转换为红色、红色转换为蓝色的图像处理 在图像处理中,我们经常需要对图像进行颜色转换和修改。本篇博客介绍了如何使用Python的Pillow库来读取一个文件夹中的所有图像,并将其中的绿色转换为红色,红色转换为蓝色。我们还展示…...

Web2与Web3开发的不同之处
Web2是引入交互功能的第二代互联网,也是我们今天所熟悉的。随着Web的不断发展,第三代互联网,也被称为Web3,正处于积极开发中。Web3引入了在区块链上运行的去中心化和无需许可的系统。但是Web2和Web3开发之间有什么区别呢ÿ…...
递增数组的判断【python实现】
有时候需要对某一组数组的数据进行判断是否 递增 的场景,比如我在开发一些体育动作场景下,某些肢体动作是需要持续朝着垂直方向向上变化,那么z轴的值是会累增的。同理,逆向考虑,递减就是它的对立面。 下面是查找总结到…...
在自定义数据上训练 YOLOv8 实例分割
图像分割是一个核心视觉问题,可以为大量用例提供解决方案。从医学成像到分析流量,它具有巨大的潜力。实例分割,即对象检测+分割,甚至更强大,因为它允许我们在单个管道中检测和分割对象。为此,Ultralytics YOLOv8 模型提供了一个简单的管道。在本文中,我们将对自定义数据…...
洛谷密钥被破解:加密安全面临新挑战
密钥管理是加密系统中非常重要的一环,它涉及到密钥的生成、存储、分发、管理和销毁等多个方面。在密码学中,密钥是保护数据隐私和安全性的核心因素之一,因此,确保密钥的安全和保密性显得尤为重要。在2016年举办的 CQOI 数论竞赛中…...

02 Android开机启动之BootLoader及kernel的启动
Android开机启动之BootLoader及kernel的启动 1、booloader的启动流程 第一阶段:硬件初始化,SVC模式,关闭中断,关闭看门狗,初始化栈,进入C代码 第二阶段:cpu/board/中断初始化;初始化内存以及flash,将kernel从flash中拷贝到内存中,执行bootm,启动内核 2、kernel的启…...
代码随想录算法训练营 Day 49 | 121.买卖股票的最佳时机,122.买卖股票的最佳时机 II
121.买卖股票的最佳时机 讲解链接:代码随想录-121.买卖股票的最佳时机 确定 dp 数组以及下标的含义: dp[i][0] 表示第 i 天持有股票所得最多现金dp[i][1] 表示第 i 天不持有股票所得最多现金 确定递推公式: 如果第 i 天持有股票即 dp[i][0]&…...

精炼计算机网络——数据链路层(一)
文章目录 前言3.1 数据链路和帧3.1.1 数据链路和帧3.1.2 三个基本问题 3.2 点对点协议PPP3.2.1 PPP协议的特点3.2.2 PPP协议3.2.3 PPP协议的工作状态 总结 前言 上篇文章,我们一同学完了物理层的全部内容,在本篇文章中,我们初步学习数据链路…...

猿创征文|Spring系列框架之面向切面编程AOP
⭐️前面的话⭐️ 本篇文章将介绍一种特别重要的思想,AOP(Aspect Oriented Programming),即面向切面编程,可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。 …...
IoT架构设计
当前有一个支持5000万用户并发访问的网站,每个用户都有一个IOT设备,用户可以查看设备状态,接受设备通知 1.架构设计 针对不同的业务量模型,可以采用不同的架构设计,如下: 低业务量模型 针对低业务量模型…...

EasyRecovery16电脑硬盘数据恢复软件功能讲解
硬盘是很常见的存储数据的设备,硬盘中很多重要的数据一旦丢失会很麻烦,不过现在有硬盘数据恢复软件可以自行在家恢复数据。今天的文章就带大家来看看硬盘恢复数据的软件EasyRecovery。 EasyRecovery 是一款专业的数据恢复软件,支持恢复不同存…...

信道通信基础 - 传输介质(双绞线、光纤)
文章目录 1 概述2 传输介质2.1 双绞线2.2 光纤 3 扩展3.1 网工软考真题 1 概述 2 传输介质 2.1 双绞线 双绞线:8 根铜导线每 2 根扭在一起(百兆用 4 根,千兆必须用 8 根)分类 2.2 光纤 光纤:利用光在 玻璃或塑料纤…...

黑马Redis原理篇
黑马Redis原理篇 1、数据结构1.1、动态字符串SDS1.2、IntSet1.3、Dict1.4、ZipList1.5、QuickList1.6、SkipList1.7、RedisObject1.8、五种数据结构1. String(小EMBSTR,大RAW (SDS),少量整数INT)2. List(Redis3.2之后使用QuickList实现&#…...
Sql Server增加字段、修改字段、修改类型、修改默认值
1、修改字段名: alter table 表名 rename column A to B 2、修改字段类型: alter table 表名 alter column 字段名 type not null 3、修改字段默认值 alter table 表名 add default (0) for 字段名 with values 如果字段有默认值,则需要…...

工业安全零事故的智能守护者:一体化AI智能安防平台
前言: 通过AI视觉技术,为船厂提供全面的安全监控解决方案,涵盖交通违规检测、起重机轨道安全、非法入侵检测、盗窃防范、安全规范执行监控等多个方面,能够实现对应负责人反馈机制,并最终实现数据的统计报表。提升船厂…...
c++ 面试题(1)-----深度优先搜索(DFS)实现
操作系统:ubuntu22.04 IDE:Visual Studio Code 编程语言:C11 题目描述 地上有一个 m 行 n 列的方格,从坐标 [0,0] 起始。一个机器人可以从某一格移动到上下左右四个格子,但不能进入行坐标和列坐标的数位之和大于 k 的格子。 例…...

C++ Visual Studio 2017厂商给的源码没有.sln文件 易兆微芯片下载工具加开机动画下载。
1.先用Visual Studio 2017打开Yichip YC31xx loader.vcxproj,再用Visual Studio 2022打开。再保侟就有.sln文件了。 易兆微芯片下载工具加开机动画下载 ExtraDownloadFile1Info.\logo.bin|0|0|10D2000|0 MFC应用兼容CMD 在BOOL CYichipYC31xxloaderDlg::OnIni…...

代码规范和架构【立芯理论一】(2025.06.08)
1、代码规范的目标 代码简洁精炼、美观,可持续性好高效率高复用,可移植性好高内聚,低耦合没有冗余规范性,代码有规可循,可以看出自己当时的思考过程特殊排版,特殊语法,特殊指令,必须…...
解决:Android studio 编译后报错\app\src\main\cpp\CMakeLists.txt‘ to exist
现象: android studio报错: [CXX1409] D:\GitLab\xxxxx\app.cxx\Debug\3f3w4y1i\arm64-v8a\android_gradle_build.json : expected buildFiles file ‘D:\GitLab\xxxxx\app\src\main\cpp\CMakeLists.txt’ to exist 解决: 不要动CMakeLists.…...

抽象类和接口(全)
一、抽象类 1.概念:如果⼀个类中没有包含⾜够的信息来描绘⼀个具体的对象,这样的类就是抽象类。 像是没有实际⼯作的⽅法,我们可以把它设计成⼀个抽象⽅法,包含抽象⽅法的类我们称为抽象类。 2.语法 在Java中,⼀个类如果被 abs…...
2025年低延迟业务DDoS防护全攻略:高可用架构与实战方案
一、延迟敏感行业面临的DDoS攻击新挑战 2025年,金融交易、实时竞技游戏、工业物联网等低延迟业务成为DDoS攻击的首要目标。攻击呈现三大特征: AI驱动的自适应攻击:攻击流量模拟真实用户行为,差异率低至0.5%,传统规则引…...

向量几何的二元性:叉乘模长与内积投影的深层联系
在数学与物理的空间世界中,向量运算构成了理解几何结构的基石。叉乘(外积)与点积(内积)作为向量代数的两大支柱,表面上呈现出截然不同的几何意义与代数形式,却在深层次上揭示了向量间相互作用的…...
CppCon 2015 学习:REFLECTION TECHNIQUES IN C++
关于 Reflection(反射) 这个概念,总结一下: Reflection(反射)是什么? 反射是对类型的自我检查能力(Introspection) 可以查看类的成员变量、成员函数等信息。反射允许枚…...
MySQL基本操作(续)
第3章:MySQL基本操作(续) 3.3 表操作 表是关系型数据库中存储数据的基本结构,由行和列组成。在MySQL中,表操作包括创建表、查看表结构、修改表和删除表等。本节将详细介绍这些操作。 3.3.1 创建表 在MySQL中&#…...