令牌桶算法与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.hqwc.cn/news/417864.html

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

相关文章

Go 知识slice

Go 知识slice 1. 什么是slice2. slice 基础2.1 定义 2.2 实现原理2.2.1 make 创建2.2.2 切片 创建 2.3 操作2.3.1 append 追加2.3.2 表达式切片2.3.3 扩展表达式2.3.4 扩容2.3.5 拷贝 3. 测试一下3.1 len && cap3.2 append && 扩容3.3 切片表达式 1. 什么是sli…

Spring第六天(注解开发第三方Bean)

注解开发管理第三方Bean 显然&#xff0c;我们无法在第三方Bean中写入诸如service这样的注解&#xff0c;所以&#xff0c;Spring为我们提供了Bean这一注解来让我们通过注解管理第三方Bean 第二种导入方式由于可读性太低&#xff0c;故只介绍第一种导入方式&#xff0c;这里我…

外包干了5个月,技术退步明显...

先说一下自己的情况&#xff0c;大专生&#xff0c;18年通过校招进入武汉某软件公司&#xff0c;干了接近4年的功能测试&#xff0c;今年年初&#xff0c;感觉自己不能够在这样下去了&#xff0c;长时间呆在一个舒适的环境会让一个人堕落! 而我已经在一个企业干了四年的功能测…

内网安全管理系统(保密管理系统)

在当今信息化的时代&#xff0c;企业的内网已经成为其核心资产的重要组成部分。 随着企业的快速发展和信息化程度的提升&#xff0c;内网安全问题日益凸显&#xff0c;如何保障内网的安全和机密信息的保密性&#xff0c;已经成为企业亟待解决的问题。 内网安全管理系统(保密管…

第11章 GUI Page507 步骤三十五:处理应用退出事件

为wxFrame&#xff0c;生成一个EVT_CLOSE事件响应函数&#xff1a; 实现如下&#xff1a; 运行效果&#xff1a;关闭时&#xff0c;会弹出对话框询问是否保存

(蓝桥杯每日一题)love

问题描述 马上就要到七夕情人节了&#xff0c;小蓝在这天想要心爱得男神表白&#xff0c;于是她写下了一个长度为n仅由小写字母组成的字符串。 她想要使这个字符串有 1314个 love 子序列但是马虎的小蓝却忘记了当前已经有多少个子序列为 love。 请你帮小蓝计算出当前字符串有多…

初识MQ-同步异步

提示&#xff1a;文章写完后&#xff0c;目录可以自动生成&#xff0c;如何生成可参考右边的帮助文档 文章目录 前言一、同步通讯二、异步通讯2.1.异步调用方案2.2.异步的优缺点 三、什么时MQ3.1 mq和broker3.2几种mq的优缺点对比 总结 前言 一、同步通讯 同步调用问题&#…

Spring Boot框架中Controller层API接口如何支持使用多个@RequestBody注解接受请求体参数

一、前言 众所周知&#xff0c;在Spring Boot框架中&#xff0c;Controller层API接口编码获取请求体参数时&#xff0c;在参数上会使用RequestBody注解&#xff1b;如果一次请求中&#xff0c;请求体参数携带的内容需要用多个参数接收时&#xff0c;能不能多次使用RequestBody…

【从零到一AIGC源码解析系列1】文本生成图片Stable Diffusion的diffusers实现

目录 1. 如何使用 StableDiffusionPipeline 1.1环境配置 1.2 Stable Diffusion Pipeline 1.3生成非正方形图像 2. 如何使用 diffusers 构造自己的推理管线 关注公众号【AI杰克王】 Stable Diffusion是由CompVis、StabilityAl和LAION的研究人员和工程师创建的文本到图像潜在…

《Linux高性能服务器编程》笔记01

Linux高性能服务器编程 本文是读书笔记&#xff0c;如有侵权&#xff0c;请联系删除。 参考 Linux高性能服务器编程源码: https://github.com/raichen/LinuxServerCodes 豆瓣: Linux高性能服务器编程 文章目录 Linux高性能服务器编程第05章 Linux网络编程基础API5.1 socket…

【MySQL】——关系数据库标准语言SQL(大纲)

&#x1f383;个人专栏&#xff1a; &#x1f42c; 算法设计与分析&#xff1a;算法设计与分析_IT闫的博客-CSDN博客 &#x1f433;Java基础&#xff1a;Java基础_IT闫的博客-CSDN博客 &#x1f40b;c语言&#xff1a;c语言_IT闫的博客-CSDN博客 &#x1f41f;MySQL&#xff1a…

从零开始配置vim(Windows版)

事情是这样的&#xff0c;之前linux下vim用习惯了...然后就给自己win下vscode也装了个vim插件&#xff0c;用下来还是感觉不顺手&#xff0c;并且处理太多文本时有明显卡顿&#xff0c;于是乎自己配了下win版的vim。 不过好像也并不是从零开始的...初始基础版的.vimrc有copy他们…