开源限流组件分析(一):juju/ratelimit

文章目录

  • 本系列
  • 前言
  • 数据结构
  • 对外提供接口
    • 初始化令牌桶
    • 获取令牌
  • 核心方法
    • adjustavailableTokens
    • currentTick
    • take
    • TakeAvailable
    • Wait系列

本系列

  • 开源限流组件分析(一):juju/ratelimit(本文)
  • 开源限流组件分析(二):uber-go/ratelimit
  • 开源限流组件分析(三):golang-time/rate

前言

这篇文章分析下go开源限流组件juju-ratelimit的使用方式和源码实现细节

源码地址:https://github.com/juju/ratelimit 版本:v1.0.2

其提供了一种高效的令牌桶限流实现

令牌桶相比于其他限流算法(如漏桶算法)的一个显著优势,就是在突发流量到来时,可以短时间内提供更多的处理能力,以应对这些额外的请求

直观上来说,令牌桶算法可以实现为:桶用channel实现

  • 在后台每隔一段固定的时间向桶中发放令牌
  • 要获取令牌时,从channel取数据

后台定时填充令牌:

 func Start(bucket chan struct{}, interval time.Duration) {go func() {ticker := time.NewTicker(interval)defer ticker.Stop()// 每隔interval时间往channel中塞一个令牌for range ticker.C {select {// 放令牌case bucket <- struct{}{} :// 桶满了,丢弃default:}}}()}

业务请求要获取获取令牌时(非阻塞式):

func GetToken(bucket chan struct{}) bool {select {case <- bucket:return truedefault:return false}
}

但这样有多少容量就要开多少空间,对内存不友好

更好的方式是只用一个int变量availableTokens维护桶中有多少token:

  1. 每次有请求要获取令牌时,先根据当前时间now减去上次获取令牌的时间last,计算因为这段时间流逝,应该给桶中补充多少令牌,并加到availableTokens
  2. 如果availableTokens < 本次请求的令牌数 request,说明令牌不够。否则令牌够,放行请求,并根据本次需求的令牌数在桶中扣减 :availableTokens -= request

数据结构

type Bucket struct {clock Clock// bucket创建时间,仅初始化一次,用于计算时间相对偏移量startTime time.Time// 令牌桶最大容量capacity int64// 每次放入多少个令牌quantum int64// 每次放入令牌桶的间隔fillInterval time.Durationmu sync.Mutex// 当前桶中有多少token,注意:可能为负值availableTokens int64// 上一次访问令牌桶的ticklatestTick int64
}

注意:桶中剩余令牌availableTokens可能为负值,至于为啥在下文分析扣减令牌流程时说明

对外提供接口

初始化令牌桶

其提供了几种根据不同需求初始化令牌桶的方法:

fillInterval时间放1个令牌,桶容量为capacity

func NewBucket(fillInterval time.Duration, capacity int64) *Bucket 

fillInterval时间放入quantum个令牌

func NewBucketWithQuantum(fillInterval time.Duration, capacity, quantum int64) *Bucket 

这两个方法都是简单设置Bucket的字段,比较简单

每秒放入rate个令牌:

func NewBucketWithRate(rate float64, capacity int64) *Bucket 

其构造方法如下:主要是需要根据参数rate推算出tb.fillInterval以及tb.quantum

func NewBucketWithRateAndClock(rate float64, capacity int64, clock Clock) *Bucket {tb := NewBucketWithQuantumAndClock(1, capacity, 1, clock)// 每次放入令牌数quantum从1开始,每轮 * 1.1for quantum := int64(1); quantum < 1<<50; quantum = nextQuantum(quantum) {// 假设quantum=1, rate=10,即每次放1个,每秒放10个 => 则放令牌的间隔是0.1sfillInterval := time.Duration(1e9 * float64(quantum) / rate)if fillInterval <= 0 {continue}tb.fillInterval = fillIntervaltb.quantum = quantum// 误差小于0.01就返回tbif diff := math.Abs(tb.Rate() - rate); diff/rate <= rateMargin {return tb}}}

虽然这里用了for循环寻找最合适的fillInterval和quantum,但只要rate小于10亿,都会在执行第一轮循环后退出

所以可以近似看做quantum=1fillInterval=1e9 / rate

例如:quantum=1,当要求 rate=10,即每次放1个,每秒放10个时,计算出来放令牌的时间间隔时是 0.1s

nextQuantum方法:

// 每次增大1.1倍
func nextQuantum(q int64) int64 {q1 := q * 11 / 10if q1 == q {q1++}return q1
}

Rate方法:根据quantumfillInterval,计算每秒放入多少个令牌

func (tb *Bucket) Rate() float64 {// 举个例子:假设每次放入2个,每500ms放一个 => 每秒就放4个return 1e9 * float64(tb.quantum) / float64(tb.fillInterval)
}

获取令牌

非阻塞获取count个令牌,返回的Duration表示调用者需要等待的时间才能获得令牌

func (tb *Bucket) Take(count int64) time.Duration 

如果在maxWait时间内能获取到count个令牌就获取,否则不获取

返回获取成功时需要等待的时间,是否获取成功

func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) 

非阻塞获取最多count个令牌,桶中还有多少获取多少,返回实际获得的令牌数(可能小于count)

func (tb *Bucket) TakeAvailable(count int64) int64

获取count个令牌,并在方法内部等待直到获取到令牌

func (tb *Bucket) Wait(count int64) 

只有当最多阻塞等待maxWait时间,能获取到count个令牌时才等待,并在内部等待。如果不能返回false

func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool

这5个方法中,业务上实用的是WaitMaxDuration,毕竟被限流的请求不能无限等下去

核心方法

adjustavailableTokens

将桶中的令牌数调整为tick时间的令牌数,相当于给桶补充token

  • 计算在tick与上一次lastTick之间能够生产多少个令牌

    • 一般是计算从上次请求到本次请求中产生的令牌数
  • 追加令牌到桶中

func (tb *Bucket) adjustavailableTokens(tick int64) {lastTick := tb.latestTicktb.latestTick = tickif tb.availableTokens >= tb.capacity {return}// 补充令牌tb.availableTokens += (tick - lastTick) * tb.quantumif tb.availableTokens > tb.capacity {tb.availableTokens = tb.capacity}return
}

其参数tick如何计算的?调currentTick方法

currentTick

计算当前时间与开始时间(桶的初始化时间)之间,需要放入多少次令牌

func (tb *Bucket) currentTick(now time.Time) int64 {return int64(now.Sub(tb.startTime) / tb.fillInterval)
}

用于计算当前到哪个tick了

take

Take系列的方法,底层会调到take方法:

Take:

func (tb *Bucket) Take(count int64) time.Duration {tb.mu.Lock()defer tb.mu.Unlock()// 可以等待无限大的时间d, _ := tb.take(tb.clock.Now(), count, infinityDuration)return d
}

TakeMaxDuration:

func (tb *Bucket) TakeMaxDuration(count int64, maxWait time.Duration) (time.Duration, bool) {tb.mu.Lock()defer tb.mu.Unlock()return tb.take(tb.clock.Now(), count, maxWait)
}

差别在于Take会等待无限大的时间,直到拿到token。TakeMaxDuration最多只会等待maxWait时间


take是获取令牌的核心方法,其流程如下:

  1. 计算出当前时刻可用的令牌数,并补充到令牌桶中
  2. 如果当前令牌桶存量够,在桶中扣减令牌
  3. 如果当前令牌桶存量不够,在桶中预扣减令牌,并返回需要等待的时间waitTime
/** 
当前时间是now
要获取count个令牌
最多等待maxWait时间
*/
func (tb *Bucket) take(now time.Time, count int64, maxWait time.Duration) (time.Duration, bool) {if count <= 0 {return 0, true}tick := tb.currentTick(now)// 给桶补充tokentb.adjustavailableTokens(tick)avail := tb.availableTokens - count// 当前桶中的令牌就能满足需求,直接返回if avail >= 0 {tb.availableTokens = availreturn 0, true}// 到这avail是负数,-avail就是还需要产生多少个令牌,// 要产生avail个令牌,还需要多少个tick(向上取整)// 再加上当前tick,就是能满足需求的tickendTick := tick + (-avail+tb.quantum-1)/tb.quantum// 需要等到这个时间才有令牌endTime := tb.startTime.Add(time.Duration(endTick) * tb.fillInterval)// 需要等待的时间waitTime := endTime.Sub(now)// 如果需要等待的时间超过了最大等待时间,则不等待,也不扣减,直接返回if waitTime > maxWait {return 0, false}// 这里将availableTokens更新为负值tb.availableTokens = avail// 返回要等待的时间,且已经在桶里预扣减了令牌return waitTime, true
}

怎么理解桶中的availableTokens变为负值?表示有请求已经提前预支了令牌,相当于欠账

  • 之后有请求要获取令牌时,需要先等时间流逝,把这些账还了,才能获取成功

    • TakeAvailable方法
  • 当然也可以在之前已欠账的基础上继续欠账,这样要等待更久的时间才能获取令牌成功

    • Take,TakeMaxDuration方法

例如:令牌桶每秒产生1个令牌,假设在**t1时刻**桶中已经没有令牌了
  1. 请求A在t1时刻调Take()获取5个令牌,将availableTokens更新为-5,并返回5s,表示需要等待5s才能让请求放行
  2. 时间过去1s,此时时刻t2 = t1 + 1s
  3. 请求B在t2时刻调Take()获取5个令牌,首先因为时间流逝,将availableTokens更新为-4。再将availableTokens更新为-4 - 5 = -9,也就是继续欠账。返回9s,表示要等待9s才能让请求放行
    在这里插入图片描述

TakeAvailable

非阻塞获取最多count个令牌,桶中还有多少获取多少,返回实际获得的令牌数(可能小于count)

func (tb *Bucket) TakeAvailable(count int64) int64 {tb.mu.Lock()defer tb.mu.Unlock()return tb.takeAvailable(tb.clock.Now(), count)
}// 返回可用的令牌数(可能小于count),如果没可用的令牌,将返回0,不会阻塞
func (tb *Bucket) takeAvailable(now time.Time, count int64) int64 {if count <= 0 {return 0}// 基于当前时间,给桶补充令牌tb.adjustavailableTokens(tb.currentTick(now))if tb.availableTokens <= 0 {return 0}// 获取max(count, availableTokens)个令牌,也就是有多少就获取多少if count > tb.availableTokens {count = tb.availableTokens}tb.availableTokens -= countreturn count
}

Wait系列

Wait系列的两个方法Wait和WaitMaxDuration就是对Take的封装,也就是如果需要等待,在Wait方法内部等待

func (tb *Bucket) Wait(count int64) {if d := tb.Take(count); d > 0 {// 在内部等待tb.clock.Sleep(d)}
}func (tb *Bucket) WaitMaxDuration(count int64, maxWait time.Duration) bool {d, ok := tb.TakeMaxDuration(count, maxWait)if d > 0 {tb.clock.Sleep(d)}return ok
}

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

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

相关文章

Race Track Generator Ultimate:Race Track Generator(赛车场赛道看台场景创建工具)

下载&#xff1a;​​Unity资源商店链接资源下载链接 效果图&#xff1a;

【论文阅读】Bi-Mamba+: Bidirectional Mamba for Time Series Forecasting

文章目录 概要阅读背景知识引言创新之处 研究方法概述方法部分的核心模块多尺度打补丁&#xff08;Multi-Scale Patching&#xff09;Mamba&#xff1a;全局模式专家Local Window Transformer&#xff08;LWT&#xff09;&#xff1a;局部变化专家长短期路由器&#xff08;Long…

Bootstrap Blazor实现多个Select选择器联合选择

Bootstrap Blazor官方目前只提供单个Select选择器&#xff0c;如果要想实现下图所示的多个Select选择器联合选择&#xff0c;则需要通过编写自定义组件来实现。 主要通过Bootstrap的data-bs-toggle属性来实现展开和折叠效果。 .razor文件内容如下&#xff1a; typeparam TValu…

【路径规划】蚁群算法的优化计算——旅行商问题(TSP)优化

摘要 旅行商问题&#xff08;TSP&#xff09;是一种经典的组合优化问题&#xff0c;其目标是找到一条遍历所有城市且总路程最短的环路。由于其计算复杂度高&#xff0c;求解大规模TSP问题往往依赖于启发式算法。本文研究了基于蚁群算法&#xff08;Ant Colony Optimization, A…

034_基于php万怡酒店管理系统

目录 系统展示 开发背景 代码实现 项目案例 获取源码 博主介绍&#xff1a;CodeMentor毕业设计领航者、全网关注者30W群落&#xff0c;InfoQ特邀专栏作家、技术博客领航者、InfoQ新星培育计划导师、Web开发领域杰出贡献者&#xff0c;博客领航之星、开发者头条/腾讯云/AW…

python之数据结构与算法(数据结构篇)-- 字典

一、字典的概念 这里我使用“小羊们”举例子&#xff0c;现在我需要去创建一个"羊村小羊们的身高"字典去保存小羊们的身高&#xff0c;对小羊们的身高进行查询、增加、删除、遍历等一系列操作。去更好的理解&#xff0c;字典是个什么东东&#xff01;&#xff01;&…

Java根据word 模板,生成自定义内容的word 文件

Java根据word 模板&#xff0c;生成自定义内容的word 文件 背景1 使用技术2 实现方法依赖啊 3 问题4 背景 主要是项目中需要定制化一个word&#xff0c;也就是有一部分是固定的&#xff0c;就是有一个底子&#xff0c;框架&#xff0c;里面的内容是需要填充的。然后填充的内容…

宝塔部署前后端分离若依项目--CentOS7版

准备&#xff1a; CentOS7服务器一台 通过网盘分享的文件&#xff1a;CentOS 7 h 链接: https://pan.baidu.com/s/17DF8eRSSDuj9VeqselGa_Q 提取码: s7x4 大家有需要可以下载这个&#xff0c;密码61 若依前端编译后文件 通过网盘分享的文件&#xff1a;ruoyi-admin.jar 链…

基于SSM网络在线考试系统的设计

管理员账户功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;学生管理&#xff0c;在线考试管理&#xff0c;试题管理&#xff0c;考试管理&#xff0c;系统管理 前台账号功能包括&#xff1a;系统首页&#xff0c;个人中心&#xff0c;在线考试&#xff0c;公告信…

基于微信小程序的购物系统【附源码、文档】

博主介绍&#xff1a;✌IT徐师兄、7年大厂程序员经历。全网粉丝15W、csdn博客专家、掘金/华为云//InfoQ等平台优质作者、专注于Java技术领域和毕业项目实战✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精彩专栏推荐订阅&#x1f447;&#x1f3…

印章图片怎么抠出透明背景?这4个一键抠图工具建议收藏!

在处理印章图片时&#xff0c;背景色的存在往往成为影响使用效果的一大障碍&#xff0c;特别是在需要将印章与不同背景融合时&#xff0c;不透明的背景色会显得尤为突兀。为了应对这一挑战&#xff0c;市场上涌现了一系列高效的一键抠图工具&#xff0c;它们能够迅速将印章图片…

Scala的继承

Scala中的继承是指在原有的类的基础上定义一个新类&#xff0c;原有的类称为父类&#xff0c;新类成为子类 例&#xff1a; class Animal(){var leg4def run():Unit{println("animal run.....")} } //继承&#xff1a;不劳而获 class Dog extends Animal(){} object…

ORB-SLAM2 ---- Frame中在主函数中被调用的函数

文章目录 一、Frame::isInFrustum1. 函数讲解2. 源码 二、Frame::GetFeaturesInArea1. 函数讲解2. 函数源码 三、Frame::ComputeBoW1. 函数讲解2. 函数源码 四、Frame::UnprojectStereo1. 函数讲解2. 函数源码 五、总结 一、Frame::isInFrustum 1. 函数讲解 此函数判断地图点…

【Linux】进程池

目录 进程池 进程池的概念&#xff1a; 手搓进程池&#xff1a; 1、创建信道和子进程 2、通过channel控制子进程 3、回收管道和子进程 进程池 进程池的概念&#xff1a; 定义一个池子&#xff0c;在里面放上固定数量的进程&#xff0c;有需求来了&#xff0c;就拿一个池中…

YoloV10——专栏目录

摘要 &#x1f525;&#x1f680;本专栏教你如何嗨翻YoloV10&#xff01;&#x1f680;&#x1f525; &#x1f4a1;升级大招&#xff1a;汲取最新论文精华&#xff0c;给你一整套YoloV10升级秘籍&#xff01;包括但不限于&#xff1a;注意力加持、卷积大换血、Block革新、Ba…

微软运用欺骗性策略大规模打击网络钓鱼活动

微软正在利用欺骗性策略来打击网络钓鱼行为者&#xff0c;方法是通过访问 Azure 生成外形逼真的蜜罐租户&#xff0c;引诱网络犯罪分子进入以收集有关他们的情报。 利用收集到的数据&#xff0c;微软可以绘制恶意基础设施地图&#xff0c;深入了解复杂的网络钓鱼操作&#xff…

使用JMeter进行Spring Boot接口的压力测试

使用 Apache JMeter 对接口进行压力测试是一个相对简单的过程。以下是详细的步骤&#xff0c;包括安装、配置和执行测试计划。 1. 下载和安装 JMeter 下载 JMeter 从 JMeter 官方网站https://jmeter.apache.org/download_jmeter.cgi 下载最新版本的 JMeter。 解压缩 将下载的 …

MATLAB支持的字体

listfonts 列出可用的系统字体 {Adobe Devanagari } {Agency FB } {Algerian } {AlienCaret } {AMS } {Arial } {Arial Black …

炒股VS炒游戏装备,哪个更好做

这个项目&#xff0c;赚个10%都是要被嫌弃的 虽然天天都在抒发自己对股市的看法&#xff0c;但自己自始至终也没有买进任何一支股票。之所以对这个话题感兴趣&#xff0c;着实是因为手上的游戏搬砖项目也是国际性买卖&#xff0c;跟国际形势&#xff0c;国际汇率挂钩&#xff0…

【C++ 11】移动构造函数

文章目录 【 1. 问题背景&#xff1a;深拷贝引起的内存开销问题 】【 2. 移动构造函数 】【 3. 左值的移动构造函数: move 实现 】 【 1. 问题背景&#xff1a;深拷贝引起的内存开销问题 】 拷贝构造函数 在 C 11 标准之前&#xff08;C 98/03 标准中&#xff09;&#xff0c;…