限流,是服务或者应用对自身保护的一种手段,通过限制或者拒绝调用方的流量,来保证自身的负载。

常用的限流算法有两种:漏桶算法和令牌桶算法

漏桶算法

思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。

对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。

令牌桶算法

原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。

下面的内容主要讨论令牌桶算法。

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

以下是对流程图的讲解:

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令牌桶算法的更多相关文章

  1. 封装RateLimiter 令牌桶算法

    自定义注解封装RateLimiter.实例: @RequestMapping("/myOrder") @ExtRateLimiter(value = 10.0, timeOut = ...

  2. coding++:RateLimiter 限流算法之漏桶算法、令牌桶算法--简介

    RateLimiter是Guava的concurrent包下的一个用于限制访问频率的类 <dependency> <groupId>com.google.guava</g ...

  3. 基于令牌桶算法实现的SpringBoot分布式无锁限流插件

    本文档不会是最新的,最新的请看Github! 1.简介 基于令牌桶算法和漏桶算法实现的纳秒级分布式无锁限流插件,完美嵌入SpringBoot.SpringCloud应用,支持接口限流.方法限流.系统限 ...

  4. coding++:Semaphore—RateLimiter-漏桶算法-令牌桶算法

    java中对于生产者消费者模型,或者小米手机营销 1分钟卖多少台手机等都存在限流的思想在里面. 关于限流 目前存在两大类,从线程个数(jdk1.5 Semaphore)和RateLimiter速率(g ...

  5. 15行python代码,帮你理解令牌桶算法

    本文转载自: http://www.tuicool.com/articles/aEBNRnU   在网络中传输数据时,为了防止网络拥塞,需限制流出网络的流量,使流量以比较均匀的速度向外发送,令牌桶算法 ...

  6. flask结合令牌桶算法实现上传和下载速度限制

    限流.限速: 1.针对flask的单个路由进行限流,主要场景是上传文件和下载文件的场景 2.针对整个应用进行限流,方法:利用nginx网关做限流 本文针对第一中情况,利用令牌桶算法实现: 这个方法:h ...

  7. 令牌桶算法实现API限流

    令牌桶算法( Token Bucket )和 Leaky Bucket 效果一样但方向相反的算法,更加容易理解.随着时间流逝,系统会按恒定 1/QPS 时间间隔(如果 QPS=100 ,则间隔是 10 ...

  8. 限流10万QPS、跨域、过滤器、令牌桶算法-网关Gateway内容都在这儿

    一.微服务网关Spring Cloud Gateway 1.1 导引 文中内容包含:微服务网关限流10万QPS.跨域.过滤器.令牌桶算法. 在构建微服务系统中,必不可少的技术就是网关了,从早期的Zuu ...

  9. php 基于redis使用令牌桶算法 计数器 漏桶算法 实现流量控制

    通常在高并发和大流量的情况下,一般限流是必须的.为了保证服务器正常的压力.那我们就聊一下几种限流的算法. 计数器计数器是一种最常用的一种方法,在一段时间间隔内,处理请求的数量固定的,超的就不做处理. ...

随机推荐

  1. Java开发笔记(一百二十四)AWT绘图操作

    前面介绍了如何使用画笔在控件上展示图像,可是图像来源于磁盘图片,无法即兴绘制个性化的图案.所幸画笔工具Graphics不仅能够描绘图像,还支持绘制常见的几何形状,也支持绘制文本字符串,除了绘制图像用到 ...

  2. 函数的练习2——python编程从入门到实践

    8-9 魔术师:创建一个包含魔术师名字的列表,并将其传递一个名为show_magicians()的函数,这个函数打印列表中每个魔术师的名字. def show_magicians(magicians) ...

  3. 关于类视图选择继承APIView还是工具视图(ListAPIView、CreateAPIView等等)

    APIView使用方法,直接继承APIView,get或者post请求.方法很简单1.先获取到要操作的数据,然后把数据放到serializer中序列化或者反序列化,最后return返回值(记得.dat ...

  4. AVR单片机教程——拨动开关

    在按键的上方有4个拨动开关.开关与按键,在原理和使用方法上都是很类似的,但有不同的用途——按键按下后松开就会弹起,而开关可以保存其状态. <switch.h> 定义了与开关相关的函数.sw ...

  5. spring session cpu占用过高

      集成spring session很简单,只需几行代码即可. @Configuration @EnableRedisHttpSession public class SessionConfig { ...

  6. Java任务调度框架Quartz教程

    一.什么是quartz作业调度? Quartz框架是一个全功能.开源的任务调度服务,可以集成几乎任何的java应用程序—从小的单片机系统到大型的电子商务系统.Quartz可以执行上千上万的任务调度. ...

  7. sshpass非交互式连接

    $ sshpass -p $passwd ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null $USER@$IP 'echo ...

  8. JavaScript内置一些方法的实现原理--new关键字,call/apply/bind方法--实现

    先学习下new操作符吧 new关键字调用函数的心路历程: 1.创建一个新对象 2.将函数的作用域赋给新对象(this就指向这个对象) 3.执行函数中的代码 4.返回这个对象 根据这个的思路,来实现一个 ...

  9. 代替for-in 遍历对象

    object.keys() object.getOwnPropertyName()

  10. ConcurrentLinkedQueue (一)

    ConcurrentLinkedQueue 主要讲一下在JDK8中,ConcurrentLikedQueue是如何入队,出队的. 首先我们要明白,ConcurrentLikedQueue是一种安全的没 ...