前言

RateLimiter是基于令牌桶算法实现的一个多线程限流器,它可以将请求均匀的进行处理,当然他并不是一个分布式限流器,只是对单机进行限流。它可以应用在定时拉取接口数据,

预防单机过大流量使用。

原理

首先先讲一下令牌桶的原理,每隔一段时间生产一个令牌放入桶里,请求在执行时需要拿到令牌才可以执行,如果拿不到令牌将等待令牌产生,一个生产者,多个消费者。

但是这样的令牌桶有一个问题,如果CPU负载过高,生产令牌的线程没有获取到时间片生产令牌,那么限制的流量将会比设定值更低。

可能是出于这个原因,guava并没有这样做,而是一个惰性生产令牌,每次请求令牌时,通过当前时间和下次产生令牌时间的差值计算出现在有多少个令牌,如果当前时间比发放时间大,会获得令牌,并且会生成令牌存储。如果令牌不够,则让线程sleep,并且将下次令牌产生时间更新成当前时间+sleep时间

sleep,并且将下次发放令牌的时间,设置成当前时间+线程sleep的时间。这样说,可能不是很清楚,看图。

这样做的好处是什么,如果获取令牌的线程抢不到cpu,只是这个线程的执行时间会晚,其他线程不会受到影响。

源码阅读

public static void main(String[] args) {
RateLimiter rateLimiter = RateLimiter.create(10);
while (true) {
long start = System.currentTimeMillis();
rateLimiter.acquire();
System.out.println(System.currentTimeMillis() - start);
}
}  

运行可以发现,上面的代码除了第一次输出的是0或者1,其他都接近100。下面先看一下RateLimiter.create做了哪些事情

static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {
//创建对象,并且赋值,permitsPerSecond这个是我们设置的qps,stopwatch这个相当于一个计时器,记录相对时间,类似于我上面图中的10ms,100ms等,下面传入的1.0就是一秒的意思,设置上速率就是一秒多少次
RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0);
rateLimiter.setRate(permitsPerSecond);
return rateLimiter;
}
看一下setRate
public final void setRate(double permitsPerSecond) {
checkArgument(permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");
//从名字就可以看出这是一个互斥锁,这个互斥锁采用了double check懒汉单例模式生成,
synchronized (mutex()) {
doSetRate(permitsPerSecond, stopwatch.readMicros());
}
}

下面看一下doSetRate,真正开始设置速率了

final void doSetRate(double permitsPerSecond, long nowMicros) {
//这个方法非常重要里面是nextFreeTicketMicros和storedPermits的设置,在生成对象的时候没有用,获取令牌时再讲
resync(nowMicros);
//这个就是令牌生成间隔,微秒表示
double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;
this.stableIntervalMicros = stableIntervalMicros;
这里又有一个doSetRate
doSetRate(permitsPerSecond, stableIntervalMicros);
} void doSetRate(double permitsPerSecond, double stableIntervalMicros) {
double oldMaxPermits = this.maxPermits;
//设置最大令牌
maxPermits = maxBurstSeconds * permitsPerSecond; //设置存储令牌
if (oldMaxPermits == Double.POSITIVE_INFINITY) {
storedPermits = maxPermits;
} else {
storedPermits =
(oldMaxPermits == 0.0)
? 0.0 // initial state
: storedPermits * maxPermits / oldMaxPermits;
}

到了这里整个创建过程就结束了,基本上就是一些设置,创建了锁,设置了生成令牌的间隔时间等等,下面看一下获取令牌的方法。

//获取一个令牌
public double acquire() {
return acquire(1);
} public double acquire(int permits) {
//reserve返回等待时间,内部进行了令牌的获取
long microsToWait = reserve(permits);
stopwatch.sleepMicrosUninterruptibly(microsToWait);
return 1.0 * microsToWait / SECONDS.toMicros(1L);
} final long reserve(int permits) {
checkPermits(permits);
//创建对象时生成的锁
synchronized (mutex()) {
//stopwatch.readMicros()拿到当前的时间,预订
return reserveAndGetWaitLength(permits, stopwatch.readMicros());
}
} final long reserveAndGetWaitLength(int permits, long nowMicros) {
//将数据透传,拿到最早可预订的时间,如果预订时间在未来时间,返回一个大于0的值为等待时间
long momentAvailable = reserveEarliestAvailable(permits, nowMicros);
return max(momentAvailable - nowMicros, 0);
}

下面的代码就是我图中的实现

final long reserveEarliestAvailable(int requiredPermits, long nowMicros) {
//这个方法很重要先看下面这个方法的讲解,在从这里往下看
resync(nowMicros);
//返回下次发放令牌时间,如果这个时间大于当前时间,在调用的上层会sleep
long returnValue = nextFreeTicketMicros;
//拿到此次花费的令牌
double storedPermitsToSpend = min(requiredPermits, this.storedPermits);
// 如果令牌不够,这里就会大于0,下面就会得出一个等待时间
double freshPermits = requiredPermits - storedPermitsToSpend;
long waitMicros =
storedPermitsToWaitTime(this.storedPermits, storedPermitsToSpend)
+ (long) (freshPermits * stableIntervalMicros);
//将下次发放令牌的时间加上等待时间
this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);
this.storedPermits -= storedPermitsToSpend;
return returnValue;
} void resync(long nowMicros) { if (nowMicros > nextFreeTicketMicros) {
//当前时间大于下次令牌发放时间,新的令牌为当前时间减去下次发放令牌时间除以生成令牌的时间间隔
double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();
//不能超过最大令牌数
storedPermits = min(maxPermits, storedPermits + newPermits);
//更新下次发放令牌时间为当前时间
nextFreeTicketMicros = nowMicros;
}
}

总结

RateLimiter的原理用语言描述,很容易把人绕晕,上面的图其实是最好的总结,懂得原理才能更好的使用,在多种限流器中选择合适的限流器。了解源码,能更进一步的掌握原理,并且从源码中可以学到设计思路和

一些设计模式的应用。

guava限流器RateLimiter原理及源码分析的更多相关文章

  1. OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波

    http://blog.csdn.net/chenyusiyuan/article/details/8710462 OpenCV学习笔记(27)KAZE 算法原理与源码分析(一)非线性扩散滤波 201 ...

  2. ConcurrentHashMap实现原理及源码分析

    ConcurrentHashMap实现原理 ConcurrentHashMap源码分析 总结 ConcurrentHashMap是Java并发包中提供的一个线程安全且高效的HashMap实现(若对Ha ...

  3. HashMap和ConcurrentHashMap实现原理及源码分析

    HashMap实现原理及源码分析 哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景及其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表, ...

  4. (转)ReentrantLock实现原理及源码分析

    背景:ReetrantLock底层是基于AQS实现的(CAS+CHL),有公平和非公平两种区别. 这种底层机制,很有必要通过跟踪源码来进行分析. 参考 ReentrantLock实现原理及源码分析 源 ...

  5. 【转】HashMap实现原理及源码分析

    哈希表(hash table)也叫散列表,是一种非常重要的数据结构,应用场景极其丰富,许多缓存技术(比如memcached)的核心其实就是在内存中维护一张大的哈希表,而HashMap的实现原理也常常出 ...

  6. 【OpenCV】SIFT原理与源码分析:DoG尺度空间构造

    原文地址:http://blog.csdn.net/xiaowei_cqu/article/details/8067881 尺度空间理论   自然界中的物体随着观测尺度不同有不同的表现形态.例如我们形 ...

  7. 《深入探索Netty原理及源码分析》文集小结

    <深入探索Netty原理及源码分析>文集小结 https://www.jianshu.com/p/239a196152de

  8. HashMap实现原理及源码分析之JDK8

    继续上回HashMap的学习 HashMap实现原理及源码分析之JDK7 转载 Java8源码-HashMap  基于JDK8的HashMap源码解析  [jdk1.8]HashMap源码分析 一.H ...

  9. 【OpenCV】SIFT原理与源码分析:关键点描述

    <SIFT原理与源码分析>系列文章索引:http://www.cnblogs.com/tianyalu/p/5467813.html 由前一篇<方向赋值>,为找到的关键点即SI ...

随机推荐

  1. 学会使用数据讲故事——Excel研究网络研讨会

    编者按:在数据密集型研究的新时代,Excel将成为研究者讲故事的强大工具.在即将举行的Excel研究网络研讨会中,我们将与你探讨如何用新的方式来寻找.查询.分析数据并实现数据可视化.Office 36 ...

  2. spring和mybatis整合报错:org.springframework.beans.MethodInvocationException: Property 'dataSource' threw exception; nested exception is java.lang.NoClassDefFoundError

    Exception in thread "main" org.springframework.beans.factory.UnsatisfiedDependencyExceptio ...

  3. MSSS攝影大賽計劃書(第三版)

    比賽內容:對香港的城市風景以及自然風光的攝影 預期成果: 提升同學對香港的認識,鼓勵學生走出大學學園去瞭解香港,同時豐富會員的課餘活動,培養同學的興趣愛好 比賽時間:4月1-15日 最後作品提交時間: ...

  4. 详解Javascript中的原型与原型链

    目录 知识点 参考资料 结束语 知识点 面向对象编程 我们熟悉的Java和C#里,面向对象的两个基本概念是类class和实例instance,而ES6以前的Javascript并没有设计class. ...

  5. 使用阿里大于平台发送短信验证码java代码实现

    待续..网站app后台还未完成,不能添加签名,短信不能正常发送. Tip: https://help.aliyun.com/document_detail/55284.html?spm=5176.sm ...

  6. 介绍vue-cli脚手架config目录下index.js配置文件

    1.config/index.js var path = require('path') module.exports = { build: { // production 环境 env: requi ...

  7. RDS的tar文件恢复到本地mysql5.7版本数据库

    参考博客: 安装qpress软件 https://blog.csdn.net/a18838964650/article/details/82800621 文章介绍补充 https://www.cnbl ...

  8. 为何印度打车软件Ola,也难逃“资本合并”命运?

    从全球市场来看,共享经济已经引发了多场具有颠覆性的风暴.尤其是在与大众关系紧密的衣食住行方面,诞生了具有强势影响力的独角兽企业.如,共享打车企业Uber.共享房屋出租企业Airbnb等.而鉴于每个国家 ...

  9. <JZOJ5937&luogu3944>斩杀计划&肮脏的牧师

    第一次写桶排相关题.... #include<cstdio> #include<iostream> #define rint register int template < ...

  10. 8.2.2 使用Java8增强的Iterator遍历集合元素

    8.2.2 使用Java 8增强的Iterator遍历集合元素 Iterator接口方法 程序示例 Iterator仅用于遍历集合 Iterator必须依附于Collection对象 修改迭代变量的值 ...