常见接口限流算法

常见接口限流算法

今天面试时,面试官对我之前实习时实现的限流功能很感兴趣,发起了夺命连问…

正好趁此机会好好整理一下,很棒。

常用的限流算法

固定窗口

实现思想

固定窗口的实现原理是:在指定周期内累加访问次数,当访问次数达到限制时,触发限流策略。

比如我们限制3s内的请求不超过两次,当第三次访问的时候检测到当前窗口内以处理2次,对本次进行限制。

在这里插入图片描述

优点:

  • 实现简单。可以直接通过 redis 的 string 类型实现。将客户端 ip + 访问端口作为 key,访问次数作为 value,并设置过期时间。

缺点:

  • 限流不平滑,如第2秒到第5秒的窗口内之间可以有3次请求。
代码实现

固定窗口我们可以基于 redis 设置过期时间的 string 实现。当 对 redis 的操作出现 err 时,建议放行,因为限流的目的是降低服务器的压力,而不是让服务器“宕机”。

var limit struct {count intcycle time.Duration
}func init() {limit.count = 2limit.cycle = 3 * time.Second
}func ratelimit() func(c *gin.Context) {return func(c *gin.Context) {// 获取客户端IPclientIP := c.ClientIP()// 不包括参数path := c.Request.URL.Pathkey := clientIP + pathvalStr, err := rdb.Get(c, key).Result()if err != nil {// 根据业务处理(放行/拦截)}val, _ := strconv.Atoi(valStr)if val >= limit.count {c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 1,"msg":  "请求过于频繁",})return}count, err := rdb.Incr(context.Background(), key).Result()if err != nil {// 根据业务处理(放行/拦截)}if count == 1 {err = rdb.Expire(context.Background(), key, limit.cycle).Err()if err != nil {// 删除key或者重试}}if int(count) > limit.count {c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 1,"msg":  "请求过于频繁",})return}}
}

滑动窗口

实现思想

滑动窗口是指在每一个时间窗口内的次数都不超过限制次数。

还是以3秒内请求不超过两次为例子,当我们每次请求时,统计一下前3秒到现在次数。如果大于等于2次时,则进行拦截。

在这里插入图片描述

优点:

  • 可以保证任意时间窗口内的请求次数都不超过限制。

缺点:

  • 实现相对复杂

  • 还是不够平滑。假如我们限制在60s 内请求20次,会存在第一秒内请求了20次,而在后面59秒内都进行拦截的情况。

代码实现

滑动窗口可以基于 reids 的 zset 实现,以请求时的时间戳作为分数。通过当前查询分数区间[ 当前时间戳 - 时间窗口 , 当前时间戳 ),可以快速统计出时间窗口内的次数。下面的代码比固定窗口的代码短的原因是因为直接将 err 忽略了(均不影响限流功能)。

var limit struct {count int64cycle int64 // 单位s
}func init() {limit.count = 2limit.cycle = 3
}func ratelimit() func(c *gin.Context) {return func(c *gin.Context) {clientIp := c.ClientIP()path := c.Request.URL.Pathkey := clientIp + patht := time.Now().Unix()has, _ := rdb.Exists(context.Background(), key).Result()count, _ := rdb.ZCount(context.Background(), key, fmt.Sprintf("%d", t-limit.cycle), "+inf").Result()if has == 0 { // 如果是第一次创建,最长时间不超过1小时rdb.Expire(context.Background(), key, 1*time.Hour) // 从功能上来说,此处不管是否设置成功,都不影响限流功能}if count >= limit.count { // 超出次数,限制c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 1,"msg":  "请求过于频繁",})return}rdb.ZAdd(context.Background(), key, &redis.Z{Score: float64(t), Member: strconv.Itoa(int(t))})// 删除窗口外的数据go func() {memberToRemove, _ := rdb.ZRangeByScore(context.Background(), key, &redis.ZRangeBy{Max: strconv.Itoa(int(t - limit.cycle)),Min: "0",}).Result()if len(memberToRemove) > 0 {rdb.ZRem(context.Background(), key, memberToRemove)}}()}
}

漏桶算法

实现思想

漏桶算法就像小学的游泳池加水放水问题,不管如何加水,放水的速度都是固定的。

漏桶算法的原理是将请求视为水,漏桶用来存贮这些请求。漏桶有一个固定的容量,并且底部有一个小孔,以固定的速度漏水,如果漏桶已满,超出部分的流量将被丢弃(或排队等待)。

在这里插入图片描述

优点:

  • 平滑限制请求的处理速度,避免瞬间请求过多导致系统崩溃,通过桶的大小和漏出速率灵活时应不同场景。

缺点:

  • 太平滑了,无法应对突发流量场景。
中间件

go有相关的中间件,何苦自己造轮子。"go.uber.org/ratelimit" 包正是基于漏桶算法实现的。

使用方式:

  1. 通过 ratelimit.New 创建限流器对象,参数为每秒允许的请求数(RPS)。
  2. 使用 Take() 方法来获取限流许可,该方法会阻塞请求知道满足限速要求。

官方示例:

import ("fmt""time""go.uber.org/ratelimit"
)func main() {rl := ratelimit.New(100) // 每秒多少次prev := time.Now()for i := 0; i < 10; i++ {now := rl.Take()	// 平均时间fmt.Println(i, now.Sub(prev))prev = now}// Output:// 0 0// 1 10ms// 2 10ms// 3 10ms// 4 10ms// 5 10ms// 6 10ms// 7 10ms// 8 10ms// 9 10ms
}
代码实现

如果是以所有的请求为粒度则定义一个全局的 ratelimit 即可。下面是以ip+接口为粒度的限制,需要定义一个map存放 key 和 与之对应的限流器。

import ("github.com/gin-gonic/gin""go.uber.org/ratelimit""sync""time"
)var limiters sync.Mapfunc ratelimitMiddleware() func(c *gin.Context) {return func(c *gin.Context) {clientIp := c.ClientIP()path := c.Request.URL.Pathkey := clientIp + pathvar rl ratelimit.Limiterif limiterVal, ok := limiters.Load(key); ok {rl = limiterVal.(ratelimit.Limiter)} else {newLimiter := ratelimit.New(1)	// 每秒只能请求1次limiters.Store(key, newLimiter)rl = newLimitergo func(string) { // 简易回收key,防止limiters 无限增大time.Sleep(1 * time.Hour)limiters.Delete(key)}(key)}rl.Take() // 超过请求次数会进行阻塞,直到放行或放弃请求}
}

令牌桶算法

实现思想

令牌桶(Token Bucket)算法与漏桶十分相似,不过前者是服务端产生“水”,后者是服务端消费“水”。

令牌桶算法是指在固定时间间隔内向“桶”中添加“令牌”,桶满则暂时不放。请求在处理前需要从桶中获取令牌。如果桶中有足够的令牌,请求被处理;否则,请求被拒绝或等待。

在这里插入图片描述

中间件

基于此算法实现的中间件有:github.com/juju/ratelimitgolang.org/x/time/rate等。

下面简单说一下 time/rate 的使用。

声明一个限流器
limiter := rate.NewLimiter(10, 2)

第一个参数代表每秒向令牌桶中产生多少token。第二个参数代表令牌桶的大小,且初始状态下令牌桶是满的。

消费Token
Wait、WaitN
func (lim *Limiter) Wait(ctx context.Context) (err error)
func (lim *Limiter) WaitN(ctx context.Context, n int) (err error)

Wait实际上就是WaitN(context.Background(),1)。当桶内 Token 数组不足(小于N),那么Wait方法将会阻塞一段时间,直至Token满足条件。如果充足则直接返回。

Allow、AllowN

Allow与Wait十分相似,都是用来消费Token,区别是当桶中Token数量不足时,前者立即返回,后者阻塞至满足条件。

func (lim *Limiter) Allow() bool
func (lim *Limiter) AllowN(now time.Time, n int) bool 

Allow 实际上是 AllowN(time.Now(),1)。

AllowN方法表示,截止到当前某一时刻,目前桶中数目是否至少为n个,满足则返回true,同时从桶中消费 n 个 token。反之返回不消费 Token,false。

通常应对这样的线上场景,如果请求速率过快,就直接丢弃到某些请求。

Reserver、ReserveN

官方提供的限流器有阻塞等待式的 Wait,也有直接判断方式的 Allow,还有提供了自己维护预留式的,但核心的实现都是下面的 reserveN 方法。

func (lim *Limiter) Reserve() *Reservation
func (lim *Limiter) ReserveN(now time.Time, n int) *Reservation

当调用完成后,无论 Token 是否充足,都会返回一个 *Reservation 对象。

你可以调用该对象的 Delay() 方法, 该方法返回了需要等待的时间。如果等待时间为0,则说明不用等待。必须等到等待时间结束之后,才能进行接下来的工作。

如果不想等待,可以调用 Cancel() 方法,该方法会将 Token 归还。

代码实现

下面还是以Ip + path 为粒度进行限制,和令牌桶差不多。

func ratelimitMiddleware() func(gin.Context) {return func(c gin.Context) {client := c.ClientIP()path := c.Request.URL.Pathkey := client + pathvar rl *rate.Limiterif limitersVal, ok := limiters.Load(key); ok {rl = limitersVal.(*rate.Limiter)} else {newLimiter := rate.NewLimiter(1, 10)limiters.Store(key, newLimiter)rl = newLimitergo func(string2 string) {time.Sleep(1 * time.Second)limiters.Delete(key)}(key)}if !rl.Allow() {c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"code": 1,"msg":  "请求过于频繁",})}}
}

小结

  1. 固定窗口计数器算法:
    • 优点
      • 实现简单,易于理解。
      • 可以精确控制每个窗口期内的请求数量。
    • 缺点
      • 无法应对短时间内的请求高峰,可能导致请求在窗口切换时突然增加,造成瞬时压力。
      • 无法平滑处理请求,可能导致用户体验不佳。
  2. 滑动窗口算法:
    • 优点
      • 相对于固定窗口算法,可以更平滑地处理请求,减少瞬时压力。
      • 可以更灵活地应对请求的波动。
    • 缺点
      • 实现相对复杂,需要维护多个计数器。
      • 可能会因为窗口滑动导致计数器更新的开销。
  3. 漏桶算法:
    • 优点
      • 实现简单,易于控制数据的输出速率。
      • 可以平滑处理请求,避免瞬时压力。
    • 缺点
      • 无法应对突发请求,可能导致请求长时间等待。
      • 处理速度固定,不够灵活。
  4. 令牌桶算法:
    • 优点
      • 可以控制平均传输速率,同时允许一定程度的突发请求。
      • 灵活性高,适用于请求速率不均匀的场景。
    • 缺点
      • 实现相对复杂,需要维护令牌的生成和消耗。
      • 需要合理设置令牌的生成速率和桶的大小,否则可能无法达到预期的限流效果。

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

BP神经网络学习内容分享:学习过程中常见的问题

BP神经网络是一种常用的机器学习算法&#xff0c;它在各个领域都有广泛的应用。然而&#xff0c;在学习BP神经网络的过程中&#xff0c;往往会遇到一些困难和问题。本文将介绍一些学习BP神经网络常见问题&#xff0c;并提供解决方法供参考。 一、过拟合问题 BP神经网络的一个常…

iPhone短信误删了?别急,这几招帮你轻松恢复!

在快节奏的生活中&#xff0c;我们频繁地使用iPhone进行各种操作&#xff0c;包括发送和接收短信。然而&#xff0c;有时候一个不小心&#xff0c;重要的短信就可能被误删&#xff0c;让人焦急万分。别担心&#xff0c;今天就来分享几个实用的方法&#xff0c;帮助你找回那些“…

VScode 使用记录

插件 1、代码提示插件&#xff1a;Codeium 安装说明&#xff1a;Codeium&#xff1a;强大且免费的AI智能编程助手 - Su的技术博客 (verysu.com) 用google账号登陆&#xff0c;跳转按照官网给的三个步骤来 step1&#xff1a;复制token&#xff1b; step2&#xff1a;在文件页…

重生之我们在ES顶端相遇第10 章- 分分分词器的基本使用

文章目录 思维导图0. 前言1. 光速上手1.1 指定分词器1.2 测试分词器 2. 分词流程(重要)2.1 基本介绍2.2 深入如何测试分词器 3. 自定义一个简单的分词器 思维导图 0. 前言 分词器在 ES 搜索使用中非常关键&#xff0c;一个好的分词器能够提高搜索的质量&#xff0c;让用户搜索…

mysql中的mysql 库不存在,进行恢复

mysql中的mysql 库不存在&#xff0c;进行恢复 解决方法&#xff1a; 关闭数据库 service mysqld stop 以跳过权限认证方式启动mysql mysqld_safe --defaults-file/etc/my.cnf --skip-grant-tables & 在输入&#xff1a;mysql -u root 在输入&#xff1a;use mysql 在输…

【C++ Primer Plus习题】9.1

问题: 解答: main.cpp #include <iostream> #include <string> #include "golf.h" using namespace std;#define SIZE 5int main() {golf ann;setgolf(ann, "AnnBirdfree", 24);golf andy;setgolf(andy);showgolf(ann);showgolf(andy);return…

JVM1-初识JVM

目录 什么是JVM JVM的功能 解释和运行 内存管理 即时编译 Java性能低的主要原因和跨平台特性 常见的JVM 什么是JVM JVM 全称是 Java Virtual Machine&#xff0c;中文译名&#xff1a;Java虚拟机 JVM本质上是一个运行在计算机上的程序&#xff0c;它的职责是运行Java字…

自建远程桌面RustDesk服务器(CentOS配置,保姆级案例)

安装环境: 系统:Centos7 网络:连接互联网 一、环境准备: ①变更国内yum源(方便安装包下载) 备份源文件 mv /etc/yum.repos.d/CentOS-Base.repo /etc/yum.repos.d/CentOS-Base.repo.backup 下载国内(阿里)源文件: curl -o /etc/yum.repos.d/CentOS-Base.repo htt…

蔡英丽医生:“斑块克星”三种食物,轻松守护心血管健康

在这个快节奏的时代&#xff0c;心血管疾病悄然成为威胁我们健康的“隐形杀手”。尤其是血管斑块&#xff0c;它不仅悄悄堵塞着我们的生命通道&#xff0c;还可能引发心脏病、中风等严重后果。但别担心&#xff0c;今天我们就来揭秘那些藏在日常餐桌上的“斑块克星”&#xff0…

CMU 10423 Generative AI:lec1

文章目录 1 概述2 内容摘录AIGC的主要应用大模型训练时&#xff0c;分布式训练有哪几种方式&#xff1f;NLP模型和CV模型发展历史本课程触及的主题课程前提、评分标准、阅读材料、5个作业、大项目课程学习目标 3 阅读材料3.1 Sequence Modeling: Recurrent and Recursive Nets.…

Faiss向量数据库

Faiss&#xff08;Facebook AI Similarity Search&#xff09;向量数据库是由Facebook AI研究院开发的一种高效相似性搜索和聚类的库。Faiss不仅支持在高维空间中进行高效的相似性搜索&#xff0c;还能够在处理大规模数据集时展现出卓越的性能&#xff0c;尤其适用于图像检索、…

C++和蓝图混用事件

一、在C中创建动态多播委托 1、UEBpAndCpp_Sender.h //声明一个蓝图可调用的多播委托的类型DECLARE_DYNAMIC_MULTICAST_DELEGATE_OneParam(FUEBpAndCpp_Broadcast, int, Param);//创建对象UPROPERTY(BlueprintAssignable)FUEBpAndCpp_Broadcast UEBpAndCpp_Broadcast;注意&…

Vue3其他Api

1.shallowRef与shallowReactive <template><div class"app"><h2>求和为:{{ sum }}</h2><h2>名字为:{{ person.name }}</h2><h2>年龄为:{{ person.age }}</h2><button click"sum 1">sum1</butto…

深入浅出:模拟实现 C++ STL 中的 unordered_map 和 unordered_set

目录 引言基础知识 散列表哈希函数负载因子模拟实现 unordered_set 数据结构设计哈希函数碰撞解决策略插入操作查找操作删除操作模拟实现 unordered_map 键值对存储插入操作查找操作删除操作代码示例总结 1. 引言 unordered_map 和 unordered_set 是 C 标准模板库 (STL) 中非…

LeetCode: 543. 二叉树的直径

二叉树的直径 原题 给你一棵二叉树的根节点&#xff0c;返回该树的 直径 。 二叉树的 直径 是指树中任意两个节点之间最长路径的 长度 。这条路径可能经过也可能不经过根节点 root 。 两节点之间路径的 长度 由它们之间边数表示。 示例 1&#xff1a; 输入&#xff1a;roo…

Vulnhub:hacksudo search

靶机下载地址。下载完成后&#xff0c;在VirtualBox中导入虚拟机&#xff0c;系统处理器修改为2&#xff0c;网卡配置修改为桥接。 信息收集 主机发现 扫描攻击机同网段存活主机。 nmap 192.168.31.0/24 -Pn -T4 靶机ip&#xff1a;192.168.31.218 端口扫描 nmap 192.168…

大数据采集迁移工具

Flume Sqoop kafka框架 MQ&#xff1a;消息队列 broker相当于服务器 消息队列

视频化时代,用好AIGC产品赋能企业培训打造增效降本“最佳实践”

根据IBM的数据&#xff0c;85%的中国企业正在加速投资AI领域&#xff0c;其中超过63%的企业已积极采用生成式AI。德勤的调研进一步显示&#xff0c;近80%的全球受访企业高管认为&#xff0c;生成式AI的兴起与发展将在3年内推动组织和行业发生实质性变革&#xff0c;这也就意味着…

2024爆火全网大模型书籍:《从零构建大型语言模型》星标17.8k

2024爆火全网大模型书籍&#xff1a;《从零构建大型语言模型》星标17.8k 近期&#xff0c;机器学习和 AI 研究员、畅销书《Python 机器学习》作者 Sebastian Raschka 又写了一本新书 ——《Build a Large Language Model (From Scratch)》&#xff0c;旨在讲解从头开始构建大型…

线性代数 -- 矩阵求导

Tips&#xff1a;本文为理解神经网络的前置知识&#xff0c;整体内容并不全&#xff0c;相关内容还需后续进一步完善。 一、基础 1、标量、向量和矩阵 标量&#xff1a;只有大小&#xff0c;没有方向的量 向量&#xff08;欧几里得向量&#xff09;&#xff1a;具有大小和方向…