令牌桶算法与Guava的实现RateLimiter源码分析

令牌桶算法与Guava的实现RateLimiter源码分析

  • 令牌桶RateLimiter
    • 简介
    • RateLimiter使用示例
      • 导入maven依赖
      • 编写测试代码
    • RateLimiter的实现
    • 源码解析
      • SmoothRateLimiter
      • SmoothBursty恒速
      • 获取令牌
        • acquire(int)
        • tryAcquire(int,long,TimeUnit)
      • 存量桶系数
      • 小结
    • 优缺点
    • 与漏桶的区别
    • 总结

令牌桶RateLimiter

简介

令牌桶算法是一种限流算法。

令牌桶算法的原理就是以一个恒定的速度往桶里放入令牌,每一个请求的处理都需要从桶里先获取一个令牌,当桶里没有令牌时,则请求不会被处理,要么排队等待,要么降级处理,要么直接拒绝服务。当桶里令牌满时,新添加的令牌会被丢弃或拒绝。

令牌桶算法主要是可以控制请求的平均处理速率,它允许预消费,即可以提前消费令牌,以应对突发请求,但是后面的请求需要为预消费买单(等待更长的时间),以满足请求处理的平均速率是一定的。

RateLimiter使用示例

导入maven依赖

<dependency><groupId>com.google.guava</groupId><artifactId>guava</artifactId><version>29.0-jre</version>
</dependency>

编写测试代码

public class RateLimiterTest {public void limit() {// 创建一个限流器,设置每秒放置的令牌数为1个RateLimiter rateLimiter = RateLimiter.create(1);IntStream.range(1, 10).forEach(i -> {// 一次获取i个令牌double waitTime = rateLimiter.acquire(i);System.out.println("acquire:" + i + " waitTime:" + waitTime);});}public static void main(String[] args) {RateLimiterTest rateLimiterTest = new RateLimiterTest();rateLimiterTest.limit();}
}

这段代码创建一个限流器,设置每秒放置的令牌数为1个,并循环获取令牌,每次获取i个。
执行结果:
image.png
第一次获取一个令牌时,等待0s立即可获取到(这里之所以不需要等待是因为令牌桶的预消费特性),第二次获取两个令牌,等待时间1s,这个1s就是前面获取一个令牌时因为预消费没有等待延到这次来等待的时间,这次获取两个又是预消费,所以下一次获取(取3个时)就要等待这次预消费需要的2s了,依此类推。可见预消费不需要等待的时间都由下一次来买单,以保障一定的平均处理速率(上例为1s一次)。

RateLimiter的实现

RateLimiter类在guava里是一个抽象类,其有两个具体实现:

  1. SmoothBursty(平滑突发):以恒定的速率生成令牌。
  2. SmoothWarmingUp(顺利热身):令牌生成的速度逐渐提升,直到达到一个稳定的值。

其类图如下:
image.png
他们的关系与作用:

  • RateLimiter是顶层封装,提供新建令牌桶的方法
  • SleepingStopwatch是guava实现的一个时钟类,提供时钟功能
  • SmoothRateLimiter是令牌桶抽象,提供操作令牌桶的抽象方法,其有两个内部类SmoothBursty和SmoothWarmingUp
  • SmoothBursty是恒定速率令牌桶实现
  • SmoothWarmingUp是逐渐加速知道稳定的令牌桶实现

源码解析

SmoothRateLimiter

SmoothRateLimiter是令牌桶抽象,其有四个关键的属性:

/** 当前存储的许可证。 */
double storedPermits;/** 存储许可证的最大数量。 */
double maxPermits;/*** 两个单位请求之间的间隔,以我们的稳定速率。* 例如,每秒 5 个许可的稳定速率具有 200 毫秒的稳定间隔。*/
double stableIntervalMicros;/*** 授予下一个请求(无论其大小如何)的时间。* 在批准请求后,这将在将来进一步推送。大请求比小请求更进一步。*/
private long nextFreeTicketMicros = 0L; // could be either in the past or future

他们的作用可以看注释。此外还有两个内部类,实现此抽象:

  1. SmoothBursty(平滑突发):以恒定的速率生成令牌。
  2. SmoothWarmingUp(顺利热身):令牌生成的速度逐渐提升,直到达到一个稳定的值。

SmoothBursty恒速

首先来看下恒定速率生成令牌的实现。其使用方法是:

// 创建一个限流器,设置每秒放置的令牌数为1个
RateLimiter rateLimiter = RateLimiter.create(1);

RateLimiter#create:

public static RateLimiter create(double permitsPerSecond) {/** The default RateLimiter configuration can save the unused permits of up to one second. This* is to avoid unnecessary stalls in situations like this: A RateLimiter of 1qps, and 4 threads,* all calling acquire() at these moments:** T0 at 0 seconds* T1 at 1.05 seconds* T2 at 2 seconds* T3 at 3 seconds** Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also* have to sleep till 3.05 seconds.*/return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());
}
static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);rateLimiter.setRate(permitsPerSecond);return rateLimiter;
}

实际是创建了一个SmoothBursty实例,默认的maxBurstSeconds是1,其中SleepingStopwatch是guava实现的一个时钟类。
代码第三行调用了RateLimiter#setRate:

public final void setRate(double permitsPerSecond) {checkArgument(permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");synchronized (mutex()) {doSetRate(permitsPerSecond, stopwatch.readMicros());}
}abstract void doSetRate(double permitsPerSecond, long nowMicros);

方法签名为:更新此 RateLimiter的稳定速率, permitsPerSecond 即构造 RateLimiter的工厂方法中提供的参数。当前受限制的 线程不会因此 调用而被唤醒,因此它们不会遵守新的速率,只有后续请求才会。
但请注意,由于每个请求都会偿还(如有必要,通过等待)前一个请求的成本,这意味着调用setRate后的下一个请求将不受新速率的影响,它将支付前一个请求的成本,这是根据以前的速率计算的。

此方法调用了抽象方法doSetRate,这里的实现是SmoothRateLimiter提供的,来看SmoothRateLimiter#doSetRate源码:

@Override
final void doSetRate(double permitsPerSecond, long nowMicros) {resync(nowMicros);double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;this.stableIntervalMicros = stableIntervalMicros;doSetRate(permitsPerSecond, stableIntervalMicros);
}

此方法:

  1. 调用 resync(nowMicros) 对 storedPermits 与 nextFreeTicketMicros 进行了调整——如果当前时间晚于 nextFreeTicketMicros,则计算这段时间内产生的令牌数,累加到 storedPermits 上,并更新下次可获取令牌时间 nextFreeTicketMicros 为当前时间。
  2. 计算stableIntervalMicros的值,此值代表产生令牌的时间间隔,1/permitsPerSecond(每秒几个令牌)
  3. 调用doSetRate(double, double)

其将输入的permitsPerSecond转换为速度,传给SmoothRateLimiter的doSetRate(permitsPerSecond, stableIntervalMicros)方法,doSetRate(permitsPerSecond, stableIntervalMicros)方法是抽象方法

abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros);

由SmoothBursty的实现为:

@Override
void doSetRate(double permitsPerSecond, double stableIntervalMicros) {double oldMaxPermits = this.maxPermits;maxPermits = maxBurstSeconds * permitsPerSecond;if (oldMaxPermits == Double.POSITIVE_INFINITY) {// if we don't special-case this, we would get storedPermits == NaN, belowstoredPermits = maxPermits;} else {storedPermits =(oldMaxPermits == 0.0)? 0.0 // initial state: storedPermits * maxPermits / oldMaxPermits;}
}

此方法计算maxPermits的值maxBurstSeconds * permitsPerSecond。maxBurstSeconds是new SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds)构造方法传进来的,这里离默认是1。

获取令牌

acquire(int)

acquire(int)方法在获取不到令牌时阻塞等待,直到获取到令牌。
获取令牌方法为RateLimiter#acquire(int):

// 从中 RateLimiter获取给定数量的令牌,阻塞直到请求可以被批准。告诉睡眠时间(如果有)
@CanIgnoreReturnValue
public double acquire(int permits) {long microsToWait = reserve(permits);stopwatch.sleepMicrosUninterruptibly(microsToWait);return 1.0 * microsToWait / SECONDS.toMicros(1L);
}

调用RateLimiter#reserve 获取需要等待的时间:

// RateLimiter 保留给定数量的令牌以供将来使用,并返回使用这些令牌需要等待的微秒数。
final long reserve(int permits) {checkPermits(permits);synchronized (mutex()) {return reserveAndGetWaitLength(permits, stopwatch.readMicros());}
}

调用RateLimiter#reserveAndGetWaitLength:

//	保留令牌并返回使用者需要等待的时间。
final long reserveAndGetWaitLength(int permits, long nowMicros) {long momentAvailable = reserveEarliestAvailable(permits, nowMicros);return max(momentAvailable - nowMicros, 0);
}

调用RateLimiter#reserveEarliestAvailable:

abstract long reserveEarliestAvailable(int permits, long nowMicros);

是抽象方法,具体实现为SmoothRateLimiter#reserveEarliestAvailable:

// 更新下次可取令牌时间点与存储的令牌数,返回本次可取令牌的时间点
@Override
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {resync(nowMicros); // 如果nextFreeTicket小于当前时间,更新当前存储的令牌数,和下次可使用令牌的时间为now// nextFreeTicketMicros表示下一个可以分配令牌的时间点,这个值返回后,// 上一层的函数会调用stopwatch.sleepMicrosUninterruptibly(microsToWait);// 即阻塞到这个分配的时间点long returnValue = nextFreeTicketMicros;// 本次需要用掉的令牌数,取本次需要的和当前可以使用的令牌数的最小值double storedPermitsToSpend = min(requiredPermits, this.storedPermits);// 需要用的令牌大于暂存的令牌数,计算需要新增的令牌数double freshPermits = requiredPermits - storedPermitsToSpend;// 计算补齐需要新增的令牌需要等待的时间long waitMicros =storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)+ (long) (freshPermits * stableIntervalMicros);// 将下一个可分配令牌的时间点向后移动!!!!这里是实现与消费的关键this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);// 更新当前存储的令牌数this.storedPermits -= storedPermitsToSpend;return returnValue;
}
// 如果nextFreeTicket小于当前时间,更新当前存储的令牌数,和下次可使用令牌的时间为now
void resync(long nowMicros) {// if nextFreeTicket is in the past, resync to nowif (nowMicros > nextFreeTicketMicros) {double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();storedPermits = min(maxPermits, storedPermits + newPermits);nextFreeTicketMicros = nowMicros;}
}

上面是获取令牌的关键方法:

tryAcquire(int,long,TimeUnit)

指定时间内尝试获取令牌,获取到或获取超时返回。

public boolean tryAcquire(int permits, long timeout, TimeUnit unit) {// 将timeout换算成微秒long timeoutMicros = max(unit.toMicros(timeout), 0);checkPermits(permits);long microsToWait;synchronized (mutex()) {long nowMicros = stopwatch.readMicros();// 判断是否可以在timeoutMicros时间范围内获取令牌if (!canAcquire(nowMicros, timeoutMicros)) {return false;} else {// 获取令牌,并返回需要等待的毫秒数microsToWait = reserveAndGetWaitLength(permits, nowMicros);}}// 等待microsToWait时间stopwatch.sleepMicrosUninterruptibly(microsToWait);return true;
}private boolean canAcquire(long nowMicros, long timeoutMicros) {return queryEarliestAvailable(nowMicros) - timeoutMicros <= nowMicros;
}// 返回可用令牌的最早时间
abstract long queryEarliestAvailable(long nowMicros);// SmoothRateLimiter#queryEarliestAvailable
final long queryEarliestAvailable(long nowMicros) {// 授予下一个请求(无论其大小如何)的时间return nextFreeTicketMicros;
}

该方法执行下面三步:

  1. 判断能否在指定超时时间内获取到令牌,通过 nextFreeTicketMicros <= nowMicros + timeoutMicros 是否为true来判断,即当前时间+超时时间 在可取令牌时间 之后,则可取(预消费的特性),否则不可获取。
  2. 如果不可获取,立即返回false。
  3. 如果可获取,则调用 reserveAndGetWaitLength(permits, nowMicros) 来更新下次可取令牌时间点与当前存储的令牌数,返回等待时间(逻辑与acquire(int)相同),并阻塞等待相应的时间,返回true。

存量桶系数

令牌桶算法中,多余的令牌会放到桶里。这个桶的容量是有上限的,决定这个容量的就是存量桶系数,默认为 1.0,即默认存量桶的容量是 1.0 倍的限流值。推荐设置 0.6~1.5 之间。
存量桶系数的影响有两方面:

  • 突发流量第一个周期放过的请求数。如存量桶系数等于 0.6,第一个周期最多放过 1.6 倍限流值的请求数。
  • 影响误杀率。存量桶系数越大,越能容忍流量不均衡问题。误杀率:服务限流是对单机进行限流,线上场景经常会用单机限流模拟集群限流。由于机器之间的秒级流量不够均衡,所以很容易出现误限。例如两台服务器,总限流值 20,每台限流 10,某一秒两台服务器的流量分别是 5、15,这时其中一台就限流了 5 个请求。减小误杀率的两个办法:
    • 拉长限流周期。
    • 使用令牌桶算法,并且调出较好的存量桶系数。

小结

RateLimiter 令牌桶的实现并不是起一个线程不断往桶里放令牌,而是以一种延迟计算的方式(参考resync函数),在每次获取令牌之前计算该段时间内可以产生多少令牌,将产生的令牌加入令牌桶中并更新数据来实现,比起一个线程来不断往桶里放令牌高效得多。(想想如果需要针对每个用户限制某个接口的访问,则针对每个用户都得创建一个RateLimiter,并起一个线程来控制令牌存放的话,如果在线用户数有几十上百万,起线程来控制是一件多么恐怖的事情)

优缺点

优点:

  • 放过的流量比较均匀,有利于保护系统。
  • 存量令牌能应对突发流量,很多时候,我们希望能放过脉冲流量。而对于持续的高流量,后面又能均匀地放过不超过限流值的请求数。

缺点:

  • 存量令牌没有过期时间,突发流量时第一个周期会多放过一些请求,可解释性差。即在突发流量的第一个周期,默认最多会放过 2 倍限流值的请求数。
  • 实际限流数难以预知,跟请求数和流量分布有关。

与漏桶的区别

  • 令牌桶是按照固定速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数减为零时,则拒绝新的请求。
  • 漏桶则是按照固定速率流出请求,流入请求速率任意,当流入的请求数累积到漏桶容量时,则新流入的请求被拒绝。
  • 令牌桶限制的是平均流入速率(允许突发请求,只要有令牌就可以处理,支持一次拿3个令牌,或4个令牌), 并允许一定程度的突发流量。
  • 漏桶限制的是常量流出速率(即流出速率是一个固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2) , 从而平滑突发流入速率。
  • 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率。

总结

令牌桶算法是一种单机限流算法,已一定速率向桶中添加令牌,允许突发流量,支持预消费,预消费的等待时间由之后的请求承担。
当QPS小于100时,比较适合使用。

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

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

相关文章

Ontrack EasyRecovery2024恢复软件最新版本有哪些新功能特色?

Ontrack EasyRecovery 16是由Ontrack官方最新出品的一款全面的自助数据恢复软件&#xff0c;中文名称叫做&#xff1a;易恢复。它能够轻松恢复所有的文件类型&#xff0c;包括文档、表格、图片、音视频和其他文件等&#xff0c;支持恢复不同存储介质数据&#xff1a;硬盘、光盘…

关于C#中的LINQ的延迟执行

简介 Linq中的绝大多数查询运算符都有延迟执行的特性,查询并不是在查询创建的时候执行,而是在遍历的时候执行 实例&#xff1a; public void Test2(){List<int> items new List<int>() { -1, 1, 3, 5 };IEnumerable<int> items2 items.Where(x > x &g…

java数据结构与算法刷题-----LeetCode209. 长度最小的子数组

java数据结构与算法刷题目录&#xff08;剑指Offer、LeetCode、ACM&#xff09;-----主目录-----持续更新(进不去说明我没写完)&#xff1a;https://blog.csdn.net/grd_java/article/details/123063846 解题思路 代码:时间复杂度O(n).空间复杂度O(1) class Solution {public in…

完美实现校验:利用Spring Validation实现强大的输入验证

概述 校验例子 大家平时编码中经常涉及参数的校验&#xff0c;对于一个用户注册的方法来说会校验用户名密码信息&#xff1a; public class UserController {public ResponseEntity<String> registerUser(String username, String password) {if (username null || u…

Python 之 enumerate

enumerate 是一个内置函数&#xff0c;它允许你在遍历一个序列&#xff08;例如一个列表&#xff09;时&#xff0c;同时获取元素的索引和值。 以下是一个使用 enumerate 的例子&#xff1a; fruits [apple, banana, mango] for i, fruit in enumerate(fruits):print(f"E…

UG NX二次开发(C#)-机械管线布置-添加管道路径

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档 文章目录 1、前言2、UG NX中创建线性路径的操作3、采用UG NX二次开发创建线性管线3.1 构建管线上点的数据结构3.2 创建一个链表3.3 创建线性管线的方法3.4 测试结果1、前言 机械管道布局是UG NX的一个应用模…

uniapp、vue、小程序、js图片转base64 示例代码

uniapp是一款跨平台的应用开发框架&#xff0c;基于Vue.js和小程序原生能力进行封装&#xff0c;旨在帮助开发者快速构建跨平台的应用程序。在uniapp中&#xff0c;可以使用Vue.js语法进行页面开发&#xff0c;同时通过使用小程序原生能力&#xff0c;可以实现调用设备的摄像头…

GCC 安装编译linux

1.下载gcc源码 2.配置编译源码 3.安装 下载地址:GCC mirror sites - GNU Project France (no snapshots): ftp.lip6.fr, thanks to ftpmaintlip6.frFrance, Brittany: ftp.irisa.fr, thanks to ftpmaintirisa.frGermany, Berlin: ftp.fu-berlin.de, thanks to ftpfu-berlin…

【数据结构与算法】1.时间复杂度和空间复杂度

&#x1f4da;博客主页&#xff1a;爱敲代码的小杨. ✨专栏&#xff1a;《Java SE语法》 ❤️感谢大家点赞&#x1f44d;&#x1f3fb;收藏⭐评论✍&#x1f3fb;&#xff0c;您的三连就是我持续更新的动力❤️ &#x1f64f;小杨水平有限&#xff0c;欢迎各位大佬指点&…

二进制数据传输中错误的检测与修正

1 文章问题概述 在二进制传输中&#xff0c;由于各种原因&#xff08;如电磁干扰、硬件故障等&#xff09;&#xff0c;数据位可能会发生变化&#xff0c;导致“1”变成“0”&#xff0c;或者“0”变成“1”&#xff0c;这就是所谓的传输错误。 1.1 如何检测错误 一般通过在发…

【论文阅读】GPT4Graph: Can Large Language Models Understand Graph Structured Data?

文章目录 0、基本介绍1、研究动机2、准备2.1、图挖掘任务2.2、图描述语言&#xff08;GDL&#xff09; 3、使用LLM进行图理解流程3.1、手动提示3.2、自提示 4、图理解基准4.1、结构理解任务4.1、语义理解任务 5、数据搜集5.1、结构理解任务5.2、语义理解任务 6、实验6.1、实验设…

史上最全EasyExcel

一、EasyExcel介绍 1、数据导入&#xff1a;减轻录入工作量 2、数据导出&#xff1a;统计信息归档 3、数据传输&#xff1a;异构系统之间数据传输 二、EasyExcel特点 Java领域解析、生成Excel比较有名的框架有Apache poi、jxl等。但他们都存在一个严重的问题就是非常的耗内…

以后要做GIS开发的话是学GIS专业还是学计算机专业好一些?

GIS开发其实严格来说分为前后端以及底层开发。不同的方向&#xff0c;代表了不同的开发语言。 所以大家首先要了解自己具体要做的岗位类型是什么&#xff0c;其次才是选择专业侧重点。 但是严格来说&#xff0c;选择某个专业&#xff0c;到就业方向这个过程&#xff0c;并不是…

el-table样式错乱解决方案

bug&#xff1a; 图片的椭圆框住的地方&#xff0c;在页面放大缩小之后就对不齐了。 原因&#xff1a; 主要原因是当你对页面放大缩小的时候&#xff0c;页面进行了重构&#xff0c;页面的宽高及样式进行了变化&#xff0c;但是在这个更新的过程中&#xff0c;table的反应并没…

Redis: Redis介绍

文章目录 一、redis介绍二、通用的命令三、数据结构1、字符串类型&#xff08;String&#xff09;&#xff08;1&#xff09;介绍&#xff08;2&#xff09;常用命令&#xff08;3&#xff09;数据结构 2、列表&#xff08;List&#xff09;&#xff08;1&#xff09;介绍&…

CompletableFuture**应用源码分析(三)

2.3 CompletableFuture源码分析 CompletableFuture的源码内容特别多。不需要把所有源码都看了&#xff0c;更多的是要掌握整个CompletableFuture的源码执行流程&#xff0c;以及任务的执行时机。 从CompletableFuture中比较简单的方法作为分析的入口&#xff0c;从而掌握整体执…

python实操之网络爬虫介绍

一、什么是网络爬虫 网络爬虫&#xff0c;也可以叫做网络数据采集更容易理解。它是指通过编程向网络服务器&#xff08;web&#xff09;请求数据&#xff08;HTML表单&#xff09;&#xff0c;然后解析HTML&#xff0c;提取出自己想要的数据。 它包括了根据url获取HTML数据、解…

R.swift SwiftGen 资源使用指南

R.swift 和 SwiftGen 资源转换使用指南 R.swift &#xff08;原始代码会打包到项目&#xff1f;&#xff09; Pod platform :ios, 12.0 target LBtest do# Comment the next line if you dont want to use dynamic frameworksuse_frameworks!pod R.swift # pod SwiftGen, ~&g…

ConcurrentHashMap和HashMap的区别

什么是HashMap &#xff08;1&#xff09;HashMap 是基于 Map 接口的非同步实现&#xff0c;线程不安全&#xff0c;是为了快速存取而设计的&#xff1b;它采用 key-value 键值对的形式存放元素&#xff08;并封装成 Node 对象&#xff09;&#xff0c;允许使用 null 键和 nul…

笨蛋学设计模式行为型模式-观察者模式【14】

行为型模式-观察者模式 8.1观察者模式:arrow_up::arrow_up::arrow_up:8.1.1概念8.1.2场景8.1.3优势 / 劣势8.1.4观察者模式可分为观察者的基本结构&#xff1a; 8.1.5观察者模式8.1.6实战8.1.6.1题目描述8.1.6.2输入描述8.1.6.3输出描述8.1.6.4代码 8.1.7总结 8.1观察者模式⬆️…