开源限流组件分析(一):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…

Rust语法基础

注释 所有的开发者都在努力使他们的代码容易理解,但有时需要额外的解释。在这种情况下,开发者在他们的源码中留下注释,编译器将会忽略掉这些内容,但阅读源码的人可能会发现有用。 和大多数的编程语言一样,主要有一下两种: 单行注释 // 多行注释 /* */ 基本数据类型 Ru…

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

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

2024.10.19小米笔试题解

第一题数独计数 考虑dfs遍历所有情况 n = int(input())def check(grid, x, y, v):dx = [1, 0, -1, 0]dy = [0, 1, 0, -1]for i in range(4):nx, ny = x + dx[i], y + dy[i]if 0 <= nx < 3 and 0 <= ny < 3:if grid[nx][ny] == 0:continueif abs(grid[nx][ny] - v…

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;里面的内容是需要填充的。然后填充的内容…

Qt中的Base64编码

Qt中的Base64编码 Qt之Base64编解码 Base64编码是一种用于表示二进制数据的文本编码方式&#xff0c;通常用于在需要通过文本传输二进制数据的场景中&#xff0c;比如在电子邮件和URL中传递数据。它将二进制数据转换为由64个ASCII字符组成的字符串&#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;公告信…

MySQL 实现简单的性能优化

一&#xff1a;硬件优化 更高的网络带宽&#xff1a;在处理大规模的远程请求时可以提高MySQL服务器的响应速度&#xff1b; 更大的内存空间&#xff1a;有助于缓存更多的数据库数据&#xff0c;减少磁盘I/O操作&#xff0c;提高整体性能&#xff1b; 换用企业级SSD&#xff1…

题解:P11215 【MX-J8-T3】水星湖

好久没写题解了&#xff0c;交一发吧。 题目传送门 思路讲解 我们用一个结构体 struct 存储每一个格子的状态&#xff0c;tp 表示格子的类型&#xff0c;t 表示如果该格子种过树&#xff0c;种树的最近时间&#xff0c;die 表示如果该格子有树&#xff0c;这棵树会不会永远都…

Offset Explorer 连接kafka使用SASL 进行身份验证详解

使用 Offset Explorer&#xff08;也称为 Kafka Tool&#xff09;3.0.1 连接到 Kafka 并通过 SASL 进行身份验证&#xff0c;可以按照以下步骤进行配置&#xff1a; 1. 确保 Kafka 配置支持 SASL 首先&#xff0c;确保你的 Kafka 集群已配置为支持 SASL。你需要在 server.pro…

笔记:weblogic升级及版本信息

官方文档 Patch Set Update (PSU) Release Listing for Oracle WebLogic Server (WLS) (Doc ID 1470197.1) 版本信息 12.2.1.412.2.1.312.2.1.212.2.1.112.2.1.012.1.3.012.1.2.012.1.1.010.3.610.3.510.3.4

【小白学机器学习17】 概率论的认识论和方法论

目录 1 分析概率问题的思路&#xff1a;三段论逻辑 2 学习概率时的三段论推导 1 分析概率问题的思路&#xff1a;三段论逻辑 现在很多辩证法&#xff0c;从黑格尔这继承&#xff0c;却变成了2段论&#xff0c;感觉缺乏一个桥梁&#xff0c;逻辑上思考问题的链条变难了基于一…

用python-pptx轻松统一调整演示文档配色方案

哈喽,大家好,我是木头左! 安装与准备:python-pptx入门 确保你的Python环境中已经安装了python-pptx库。如果没有,可以通过pip进行快速安装: pip install python-pptx此外,对于PPT文档的操作,了解一些基本的PowerPoint概念是有帮助的,比如幻灯片母版(Slide Master)…

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

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

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

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