RateLimiter 源码分析

俗话说得好,缓存,限流和降级是系统的三把利剑。刚好项目中每天早上导出数据时因调订单接口频率过高,订单系统担心会对用户侧的使用造成影响,让我们对调用限速一下,所以就正好用上了。

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

漏桶算法

漏桶算法:请求先进入“桶”中,然后桶以一定的速率处理请求。如果请求的速率过快会导致桶溢出。根据描述可以知道,漏桶算法会强制限制请求处理的速度。任你请求的再快还是再慢,我都是以这种速率来处理。

但是对于很多情况下,除了要求能够限制平均处理速度外,还要求能允许一定程度的的突发情况。这样的话,漏桶算法就不合适了,用令牌桶算法更合适。

令牌桶算法

令牌桶算法的原理是:系统以恒定的速率往桶里丢一定数量的令牌,请求只有拿到了令牌才能处理。当桶里没有令牌时便可拒绝服务。

Guava中的Ratelimiter便是实现的令牌桶算法,同时能支持一定程度的突发请求。

private static RateLimiter one=RateLimiter.create(2);//每秒2个
  private static RateLimiter two=RateLimiter.create(2);//每秒2个
  private RateLimitUtil(){};
  public static void acquire(RateLimiter r,int num){
    double time =r.acquire(num);
    System.out.println("wait time="+time);
  }
  public static void main(String[] args) throws InterruptedException {
    acquire(one,1);
    acquire(one,1);
    acquire(one,1);
    System.out.println("-----");
    acquire(two,10);
    acquire(two,1);
  }

输出结果:

wait time=0.0
wait time=0.499163
wait time=0.489308
-----
wait time=0.0
wait time=4.497819

可以看到,我们以每秒2个请求的速度生成令牌。对one来说,当第2次和第3次获取请求的时候,等待的时间加起来就差不多刚好是1秒。对two来说,当第一次获取了10个令牌之后,第二次获取1个请求,就差不多等待5S(10/2=5)。可以看到,guava通过限制后面请求的等待时间,来支持一定程度的突发请求。

接下来,就是通过源码来解析它! 

当我第一次看到令牌桶的算法描述的时候,我还以为真是有一个线程每隔X秒往一个类似计数器的地方加数字呢….

guava的限流算法有2种模式,一种是稳定速度,还有一种是生成令牌的速度慢慢提升直到维持在一个稳定的速度。2种模式原理类似,只是在具体等待多久的时间计算上有区别。以下就专门指稳定速度的模式。

先来看看它的acquire()方法:

public double acquire(int permits) {
  long microsToWait = reserve(permits);//先计算获取这些请求需要让线程等待多长时间
  stopwatch.sleepMicrosUninterruptibly(microsToWait);//让线程阻塞microTowait微秒长的时间
  return 1.0 * microsToWait / SECONDS.toMicros(1L);//返回阻塞的时间
 }

主要分3步:

1. 根据limiter创建时传入的参数,计算出生成这些数量的令牌需要多长的时间。

2. 让线程阻塞microTowait这么长的时间(单位:微秒)

3. 再返回阻塞了多久,单位:秒

具体它是怎么计算需要多长时间的呢?让我们来看看reserve(permits)方法。

final long reserve(int permits) {
  checkPermits(permits);//检查参数是否合法
  synchronized (mutex()) {
   return reserveAndGetWaitLength(permits, stopwatch.readMicros());
  }
 }
    ↓
    ↓
    ↓
 final long reserveAndGetWaitLength(int permits, long nowMicros) {
  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
  return max(momentAvailable - nowMicros, 0);
 }
    ↓
    ↓
    ↓
 final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
  resync(nowMicros);//here
  long returnValue = nextFreeTicketMicros;
  double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
  double freshPermits = requiredPermits - storedPermitsToSpend;
  long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
    + (long) (freshPermits * stableIntervalMicros);
  this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;
  this.storedPermits -= storedPermitsToSpend;
  return returnValue;
 }

最终调用的是reserveEarliestAvailable方法。先看看resync(nowMicros)方法。

private void resync(long nowMicros) {
  // if nextFreeTicket is in the past, resync to now
  if (nowMicros > nextFreeTicketMicros) {
   storedPermits = min(maxPermits,
     storedPermits + (nowMicros - nextFreeTicketMicros) / stableIntervalMicros);
   nextFreeTicketMicros = nowMicros;
  }
 }

nextFreeTicketMicros的意思是:下次获取的时候需要减去的时间。如果是第一次调用accquire()方法,那nowMicros - nextFreeTicketMicros 就是从初始化(初始化的时候会给nextFreeTicketMicros 赋值一次,具体可以看RateLimiter的构造器)到第一次请求,这中间发生的时间。

这个方法的意思,如果当前时间比上一轮设置的下次获取的时间大(因为存在提前获取的情况,比如上次直接获取了10个,那上轮设置的nextFreeTicketMicros就是上一轮的时间+5s。后面会提到),那就计算这个中间理论上能生成多少的令牌。比如这中间隔了1秒钟,然后stableIntervalMicros=5000(稳定生成速度的情况下),那么,就这中间就可以生成2个令牌。再加上它原先存储的storedPermits个,如果比maxPermits大,那最大也只能存maxPermits这么多。如果比maxPermits小,那就是storedPermits=原先存的+这中间生成的数量。同时记录下下次获取的时候需要减去的时间,也就是当前时间 (nextFreeTicketMicros )。

接下来继续看reserveEarliestAvailable方法:

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) { //1
  resync(nowMicros);   //2
  long returnValue = nextFreeTicketMicros;//3
  double storedPermitsToSpend = min(requiredPermits, this.storedPermits);//4
  double freshPermits = requiredPermits - storedPermitsToSpend;//5
  long waitMicros = storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
    + (long) (freshPermits * stableIntervalMicros);//6
  this.nextFreeTicketMicros = nextFreeTicketMicros + waitMicros;//7
  this.storedPermits -= storedPermitsToSpend;//8
  return returnValue;//9
 }

我们一行一行来看:

第二行设置好之后。第3行中将下次获取的时候需要减去的时间作为返回值(这点很重要)。

这2句是什么意思呢?

其实这2句就是使得RateLimiter能一定程度的突发请求的原因。假设requiredPermits=10,而我们能存的storedPermits=2,那么freshPermits=8,也就是多取了8个。而第6行就是计算这多取的8个需要多长时间才能生成?需要3秒。那么,就将这3秒钟加到我们前面赋值的“下次获取的时候需要减去的时间 ”。

比如在05秒的时候一次性获取了10个,那么,第7行的意思就是nextFreeTicketMicros=13S对应的系统的毫秒数。然后storedPermits就是-8。当过了1秒钟,下一次请求来调用acquire(1)的时候,resync方法中由于nowMicros

final long reserveAndGetWaitLength(int permits, long nowMicros) {
  long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
  return max(momentAvailable - nowMicros, 0);//取较大的值
 }

也就是说,reserveAndGetWaitLength会返回max(13-6,0),也就是7。而该方法的返回值又是用于sleep线程的,也就是我们在一开始看到的:

public double acquire(int permits) {
  long microsToWait = reserve(permits);
  stopwatch.sleepMicrosUninterruptibly(microsToWait);
  return 1.0 * microsToWait / SECONDS.toMicros(1L);
 }

总结起来,最主要的是nowMicros,nextFreeTicketMicros这2个值。nextFreeTicketMicros在一开始构造器执行的时候会赋值一次为构造器执行的时间。当第一次调用accquire()的时候,resync会被执行,然后在accquire()中将nextFreeTicketMicros设置为当前时间。但是,需要注意的是,在reserveEarliestAvailable中会根据请求的令牌数和当前存储的令牌数进行比较。如果请求的令牌数很大,则会计算出生成这些多余的令牌需要的时间,并加在nextFreeTicketMicros上,从而保证下次调用accquire()的时候,根据nextFreeTicketMicros和当时的nowMicros相减,若>0,则需要等到对应的时间。也就能应对流量的突增情况了。

所以最重要的是nextFreeTicketMicros,它记录了你这次获取的时候,能够开始生成令牌的时间。比如当前是05S,那若nextFreeTicketMicros=10,表示它要到10S才能开始生成令牌,谁叫前面的多拿了这么多呢。至于它这次是多拿了还是只是拿一个令牌,等待时间都是这么多。如果这次又多拿了,那下次就等待更久!

private static RateLimiter too=RateLimiter.create(2);//每秒2个
  private RateLimitUtil(){};
  public static void acquire(RateLimiter r,int num){
    double time =r.acquire(num);
    System.out.println("wait time="+time);
  }
  public static void main(String[] args) throws InterruptedException {
    acquire(too,1);
    acquire(too,10);//只等待了0.5秒就获取了10个
    acquire(too,10);//等待了5秒就获取了10个
    acquire(too,1);//虽然只获取1个,也是等待5秒
  }

总结

以上就是本文关于RateLimiter 常用方法以及源码分析的全部内容,希望对大家有所帮助。感兴趣的朋友可以参阅:关于Openfire集群源码的分析 、 Spring SpringMVC在启动完成后执行方法源码解析 、 Java查看本机端口是否被占用源码等。感谢大家对我们网站的支持!

(0)

相关推荐

  • nginx 如何实现读写限流的方法

    nginx 读写限流 前段时间,开发了一个供外部调用的api,领导说要限流,请求单个IP,每秒50读次,写10次 万能的nginx,几行配置搞定 # 先定义好规则,需要写在server外面 limit_req_zone $binary_remote_addr $uri zone=api_write:20m rate=10r/s; # 写 limit_req_zone $binary_remote_addr $uri zone=api_read:20m rate=50r/s; # 读 # 把需要限

  • 来自CSDN的"无限流"分页程序

    以下是代码片段:  '******************************************************************   '** 本程序名:"无限流"分页程序   '** 作者:Arbiter(AAsx)   '** 版本:Million Level   '**   '** QQ:22222xx   '** Email:Arbiter@21cn.com   '** http://www.imagecity.org/   '*************

  • asp中"无限流"分页程序代码

    <% '****************************************************************** '** 本程序名:"无限流"分页程序 '** 作者:Arbiter(AAsx) '** 版本:Million Level '** '** QQ:22222xx '** Email:Arbiter@21cn.com '** http://www.imagecity.org/ '*********************************

  • RateLimiter 源码分析

    俗话说得好,缓存,限流和降级是系统的三把利剑.刚好项目中每天早上导出数据时因调订单接口频率过高,订单系统担心会对用户侧的使用造成影响,让我们对调用限速一下,所以就正好用上了. 常用的限流算法有2种:漏桶算法和令牌桶算法. 漏桶算法 漏桶算法:请求先进入"桶"中,然后桶以一定的速率处理请求.如果请求的速率过快会导致桶溢出.根据描述可以知道,漏桶算法会强制限制请求处理的速度.任你请求的再快还是再慢,我都是以这种速率来处理. 但是对于很多情况下,除了要求能够限制平均处理速度外,还要求能允许一

  • 浅谈bootstrap源码分析之tab(选项卡)

    实现tab选项卡的应用,此插件相对比较简单 源码文件: tab.js 实现原理 1.单击一个元素时,首先将原来高亮的元素取消 2.然后给被单击元素进行高亮 3.如果单击元素是下拉框中某个选项,则选中本身,还要选中下拉框 5.如果定义了动画,先执行动画,然后回调 源码分析: 1.Show方法,是在单击一个元素的时候触发,会触发如下四个事件 1.1.Hiden.bs.tab:隐藏上一个元素 1.2.Show.bs.tab:显示当前元素 1.3.Hideen.bs.tab:隐藏上一个元素完成 1.4.

  • nginx源码分析configure脚本详解

    nginx源码分析--configure脚本 一.前言 在分析源码时,经常可以看到类似 #if (NGX_PCRE) .... #endif 这样的代码段,这样的设计可以在不改动源码的情况下,通过简单的定义宏的方式来实现功能的打开与关闭,但是在nginx/src目录下始终没有找到宏 NGX_PCRE 对应的 #define 语句. 在之前介绍event模块的时候,讲到init_cycle函数中对cycle进行了初始化,其中很重要一步操作就是讲包含所有module信息的数组拷贝到这个cycle对应

  • jQuery 1.9.1源码分析系列(十五)动画处理之缓动动画核心Tween

    在jQuery内部函数Animation中调用到了createTweens()来创建缓动动画组,创建完成后的结果为: 可以看到上面的缓动动画组有四个原子动画组成.每一个原子动画的信息都包含在里面了. 仔细查看createTweens函数,实际上就是遍历调用了tweeners ["*"]的数组中的函数(实际上就只有一个元素). function createTweens( animation, props ) { jQuery.each( props, function( prop, v

  • java使用websocket,并且获取HttpSession 源码分析(推荐)

    一:本文使用范围 此文不仅仅局限于spring boot,普通的spring工程,甚至是servlet工程,都是一样的,只不过配置一些监听器的方法不同而已. 本文经过作者实践,确认完美运行. 二:Spring boot使用websocket 2.1:依赖包 websocket本身是servlet容器所提供的服务,所以需要在web容器中运行,像我们所使用的tomcat,当然,spring boot中已经内嵌了tomcat. websocket遵循了javaee规范,所以需要引入javaee的包 <

  • Mybatis源码分析之存储过程调用和运行流程

    这一篇我们学习一下Mybatis调用存储过程的使用和运行流程.首先我们先创建一个简单的存储过程 DELIMITER $ CREATE PROCEDURE mybatis.ges_user_count(IN age INT, OUT user_count INT) BEGIN SELECT COUNT(*) FROM users WHERE users.age=age INTO user_count; END $ 这个存储过程的含义其实比较简单的,就是输入age,然后执行select count(

  • java 源码分析Arrays.asList方法详解

    最近,抽空把java Arrays 工具类的asList 方法做了源码分析,在网上整理了相关资料,记录下来,希望也能帮助读者! Arrays工具类提供了一个方法asList, 使用该方法可以将一个变长参数或者数组转换成List . 其源代码如下: /** * Returns a fixed-size list backed by the specified array. (Changes to * the returned list "write through" to the arr

  • java集合类源码分析之Set详解

    Set集合与List一样,都是继承自Collection接口,常用的实现类有HashSet和TreeSet.值得注意的是,HashSet是通过HashMap来实现的而TreeSet是通过TreeMap来实现的,所以HashSet和TreeSet都没有自己的数据结构,具体可以归纳如下: •Set集合中的元素不能重复,即元素唯一 •HashSet按元素的哈希值存储,所以是无序的,并且最多允许一个null对象 •TreeSet按元素的大小存储,所以是有序的,并且不允许null对象 •Set集合没有ge

  • MyBatis 源码分析 之SqlSession接口和Executor类

    mybatis框架在操作数据的时候,离不开SqlSession接口实例类的作用.可以说SqlSession接口实例是开发过程中打交道最多的一个类.即是DefaultSqlSession类.如果笔者记得没有错的话,早期是没有什么getMapper方法的.增删改查各志有对应的方法进行操作.虽然现在改进了很多,但是也保留了很多.我们依旧可以看到类似于selectList这样子的方法.源码的例子里面就可以找到.如下 SqlSession session = sqlMapper.openSession(T

  • 浅谈bootstrap源码分析之scrollspy(滚动侦听)

    源码文件: Scrollspy.js 实现功能 1.当滚动区域内设置的hashkey距离顶点到有效位置时,就关联设置其导航上的指定项 2.导航必须是 .nav > li > a 结构,并且a上href或data-target要绑定hashkey 3.菜单上必须有.nav样式 4.滚动区域的data-target与导航父级Id(一定是父级)要一致 <div id="selector" class="navbar navbar-default">

随机推荐