RateLimiter令牌桶算法
限流,是服务或者应用对自身保护的一种手段,通过限制或者拒绝调用方的流量,来保证自身的负载。
常用的限流算法有两种:漏桶算法和令牌桶算法
漏桶算法
思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。
令牌桶算法
原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

下面的内容主要讨论令牌桶算法。
仔细讨论之前,先看下一下基于分布式缓存实现的令牌桶的流程图。

以下是对流程图的讲解:
1. key是否存在。因为流程图是基于分布式缓存做的集群限流,需要根据不同key做统计,第一次访问初始化key。
2. 如果key不存在,初始化令牌桶,防止初始令牌数量,并且设置key过期时间为interval*2。这里的初始令牌数量一般可以设置成限流阈值,比如限流10qps,初始值可以设置成10,来应对一开始的流量。interval是间隔时间,比如限流阈值10qps,interval设置为1s。过期时间是缓存中key的时间,interval*2是为了防止key过期无法拦截流量。
3. 如果key存在,将当前请求时间和当前key的最后放置令牌时间做比较。如果间隔超过interval,进入第4步,间隔未超过interval,进入第5步。
4. 间隔已经超过1s,直接放置令牌到最大数量。
5. 间隔没有超过1s,定义delta为时间差,放置令牌数=delta/(1/qps)。放入令牌时保证令牌数不超过桶的容量。同时,重置放入令牌的时间。
6. 从桶中获取令牌,获取令牌成功,执行请求;获取令牌时间,拒绝请求。
以上是对令牌桶算法的一种实现,接下来会具体分析guava RateLimiter的源码,RateLimiter的原理和上述实现类似,但是会有部分区别。
最基础的使用RateLimiter的姿势如下:
RateLimiter rateLimiter = RateLimiter.create(10.0);
rateLimiter.acquire();
create方法用于构建既定速度的实例,acquire方法使用阻塞方式获取令牌。
首先进入源码,看下create方法:
static RateLimiter create(SleepingStopwatch stopwatch, double permitsPerSecond) {
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
这里我们看到,会构建一个SmoothBursty实例,并且给这个实例设置速率。
/**
* This implements a "bursty" RateLimiter, where storedPermits are translated to
* zero throttling. The maximum number of permits that can be saved (when the RateLimiter is
* unused) is defined in terms of time, in this sense: if a RateLimiter is 2qps, and this
* time is specified as 10 seconds, we can save up to 2 * 10 = 20 permits.
*/
SmoothBursty的注释翻译如下:
这是一个“突发性”的RateLimiter实现,这里存储令牌数可以被转义成“零节流”。存储的最大令牌数被存储成(如果RateLimiter实例有一段时间没有被获取令牌)一个时间形式。举例:如果一个RateLimiter实例的速率是2qps,并且maxBurstSeconds时间是10,那么最多可以存储20个令牌(令牌桶容量)。
看一下SmoothBursty的构造函数:
SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) {
super(stopwatch);
this.maxBurstSeconds = maxBurstSeconds;
}
下面简单介绍下SleepingStopwatch是什么。
@VisibleForTesting
abstract static class SleepingStopwatch {
/*
* We always hold the mutex when calling this. TODO(cpovirk): Is that important? Perhaps we need* to guarantee that each call to reserveEarliestAvailable, etc. sees a value >= the previous?
* Also, is it OK that we don't hold the mutex when sleeping?
*/
abstract long readMicros();
abstract void sleepMicrosUninterruptibly(long micros);
static final SleepingStopwatch createFromSystemTimer() {
return new SleepingStopwatch() {
final Stopwatch stopwatch = Stopwatch.createStarted();
@Override
long readMicros() {
return stopwatch.elapsed(MICROSECONDS);
}
@Override
void sleepMicrosUninterruptibly(long micros) {
if (micros > 0) {
Uninterruptibles.sleepUninterruptibly(micros, MICROSECONDS);
}
}
};
}
}
SleepingStopWatch是一个可sleep的秒表,起始时间是构建StopWatch的时间,sleepMicrosUninterruptibly方法支持不受中断的sleep,sleep是当前线程的sleep。
以上构建方法完成,下面再来看一下acquire的源码。
public double acquire(int permits) {
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
}
第一行是获取令牌需要等待时间;第二行是线程sleep时间,如果令牌足够,这里会返回0,无需sleep;第三行是返回等待时间值,单位转换成秒。
接下来看下reserve实现。
final long reserve(int permits) {
checkPermits(permits);
synchronized (mutex()) {
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
}
获取锁之后,直接调用reserveAndGetWaitLength方法,传入参数是需要获取的令牌数、秒表的当前时间。
final long reserveAndGetWaitLength(int permits, long nowMicros) {
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}
首先计算获取令牌需要的时间节点,如果时间节点小于当前时间,无需等待;如果时间节点在当前时间节点之后,需要sleep线程,sleep时间是momentAvailable和nowMicros的差值。
下面看下reserveEarliestAvailable,计算时间节点的实现。
final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
resync(nowMicros);
long returnValue = nextFreeTicketMicros;
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);try {
this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
} catch (ArithmeticException e) {
this.nextFreeTicketMicros = Long.MAX_VALUE;
}
this.storedPermits -= storedPermitsToSpend;
return returnValue;
}
这里面有几个变量需要注意下:
nextFreeTicketMicros 下次允许获取令牌的时间,这个时间是因为RateLimiter允许透支而存在的,比如当前令牌桶只有一个令牌,一个请求来获取5个令牌,请求会成功,但是nextFreeTicketMicros往后推4个时间片段,在当前时间推移到nextFreeTicketMicros之前,所有请求都将等待。如果长时间没有请求到来,这个值会是过去的一个时间值。
storedPermits 当前令牌桶剩余的令牌数。
stableIntervalMicros 时间片段值,qps为5的话,时间片段是200ms。
maxPermits 令牌桶的容量。
下面开始分析代码。
resync(nowMicros);//根据当前时间和nextFreeTicketMicros,往令牌桶放置令牌,最多不超过令牌桶的maxPermits。
long returnValue = nextFreeTicketMicros; //赋值语句
double storedPermitsToSpend =min(requiredPermits,this.storedPermits);//请求的令牌数和令牌桶当前令牌数做比较,取较小值,storedPermitsToSpend是指需要消耗当前令牌桶的令牌数量。
double freshPermits = requiredPermits - storedPermitsToSpend;//需要透支的令牌数量
long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+(long) (freshPermits *stableIntervalMicros);//如果不透支,waitMicros为0,下次请求可以正常获取令牌;如果透支,需要将nextFreeTicketMicros往后推。
try {
this.nextFreeTicketMicros = LongMath.checkedAdd(nextFreeTicketMicros, waitMicros);
} catch (ArithmeticException e) {
this.nextFreeTicketMicros = Long.MAX_VALUE;
}
//将nextFreeTicketMicros往后推
this.storedPermits -= storedPermitsToSpend;//清算令牌桶的令牌。
至此,最基础的创建RateLimiter和阻塞获取令牌的过程已经分析完毕。
总结:
1. 文章前端的流程图和guava RateLimiter的实现类似。
2. guava RateLimiter支持透支,如果每次获取单个令牌,那么透支将不会生效。
RateLimiter令牌桶算法的更多相关文章
- 封装RateLimiter 令牌桶算法
自定义注解封装RateLimiter.实例: @RequestMapping("/myOrder") @ExtRateLimiter(value = 10.0, timeOut = ...
- coding++:RateLimiter 限流算法之漏桶算法、令牌桶算法--简介
RateLimiter是Guava的concurrent包下的一个用于限制访问频率的类 <dependency> <groupId>com.google.guava</g ...
- 基于令牌桶算法实现的SpringBoot分布式无锁限流插件
本文档不会是最新的,最新的请看Github! 1.简介 基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot.SpringCloud应用,支持接口限流.方法限流.系统限 ...
- coding++:Semaphore—RateLimiter-漏桶算法-令牌桶算法
java中对于生产者消费者模型,或者小米手机营销 1分钟卖多少台手机等都存在限流的思想在里面. 关于限流 目前存在两大类,从线程个数(jdk1.5 Semaphore)和RateLimiter速率(g ...
- 15行python代码,帮你理解令牌桶算法
本文转载自: http://www.tuicool.com/articles/aEBNRnU 在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法 ...
- flask结合令牌桶算法实现上传和下载速度限制
限流.限速: 1.针对flask的单个路由进行限流,主要场景是上传文件和下载文件的场景 2.针对整个应用进行限流,方法:利用nginx网关做限流 本文针对第一中情况,利用令牌桶算法实现: 这个方法:h ...
- 令牌桶算法实现API限流
令牌桶算法( Token Bucket )和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100 ,则间隔是 10 ...
- 限流10万QPS、跨域、过滤器、令牌桶算法-网关Gateway内容都在这儿
一.微服务网关Spring Cloud Gateway 1.1 导引 文中内容包含:微服务网关限流10万QPS.跨域.过滤器.令牌桶算法. 在构建微服务系统中,必不可少的技术就是网关了,从早期的Zuu ...
- php 基于redis使用令牌桶算法 计数器 漏桶算法 实现流量控制
通常在高并发和大流量的情况下,一般限流是必须的.为了保证服务器正常的压力.那我们就聊一下几种限流的算法. 计数器计数器是一种最常用的一种方法,在一段时间间隔内,处理请求的数量固定的,超的就不做处理. ...
随机推荐
- spring-data-jpa模糊查询
记录一条关于spring-data-jpa模糊查询的语句 方法一: @Query(value="select * from search_key a where a.key_name lik ...
- go 学习笔记(3)benchmark
benchmark函数以benchmark开头 benchmark的case一般会跑b.N次,且每次执行都如此 在执行过程中会根据实际case的执行时间是否稳定会增加b.N的次数以达到稳态. pack ...
- Linux进程自保护攻防对抗技术研究(Process Kill Technology && Process Protection Against In Linux)
0. 引言 0x1: Linux系统攻防思想 在linux下进行"进程kill"和"进程保护"的总体思路有以下几个,我们围绕这几个核心思想展开进行研究 . 直接 ...
- uni-app项目导入第三方组件库muse-ui
你说uni-app是什么 我说,uni-app是一套基于vue.js开发跨平台应用的前端框架,可编译多个平台,比如:Android.IOS.H5.微信小程序.支付宝小程序.头条小程序.百度小程序 懂行 ...
- Springboot 结合百度IORC实现自定义模板图片识别
前言: 首先呢,最近再公司的项目当中遇到这样的一个问题,就是需要识别图片,提取图片当中的关键语句,而且识别的语句当然是人家手写体识别,翻来覆去一想,最终还是决定使用百度的OCR帮助我解决这一项需求 话 ...
- 全栈项目|小书架|微信小程序-首页水平轮播实现
首页效果 首页功能主要有 搜索(下篇文章介绍) 图书列表 图书列表 分析一波: 列表是水平滑动 点击列表会有按压效果:布局整体缩小 每个布局的信息从上到下排列分别是:图片.书名.作者.出版社 每个布局 ...
- How to read request body in a asp.net core webapi controller?
原文 How to read request body in a asp.net core webapi controller? A clearer solution, works in ASP.Ne ...
- Asp.Net Core File的操作
FileOption 内置类(通过服务注入) 该操作类的功能是实现对文件的删除,修改查询功能,该类基本完成了对文件的操作,同样是用最简单的代码实现了文件操作功能.
- 前端开发 vue,angular,react框架对比2
在过去一年里,前端开发发展迅速,前端工程师的薪资亦是水涨船高.2019 更是热度不减,而作为近年来尤为热门的前端框架,Vue.js 自是积累了大量关注.那么,Vue.js 是适合你的框架吗? ...
- 使用jQuery开发accordion手风琴插件
一.插件效果 手风琴插件常用的功能均已实现,包括:手风琴菜单项的折叠展开效果.选中指定菜单项.判断菜单项是否选中等. 效果如下: 二.插件内部HTML元素结构 <!-- accordioon组件 ...