接上篇。Guava的令牌桶的实现中,包括一条设计哲学,需要大家注意:它允许瞬间的流量波峰超过QPS,但瞬间过后的请求将会等待较长的时间来缓解上次的波峰,以使得平均的QPS等于预定值。

RateLimiter类提供了令牌桶的接口,它是一个抽象类,其子类有SmoothRateLimiter(也是个抽象类)以及孙子类SmoothBursty,SmoothWarmingUp。SmoothRateLimiter类实现了算法的核心部分,因次我们暂且只讨论SmoothRateLimiter和其实现类SmoothBursty。RateLimiter都是通过静态的create函数实例化。以create(double permitsPerSecond)为例。参数permitsPerSecond为配置的QPS。该方法简洁明了,屏蔽了很多用户无需关心的细节。

public static RateLimiter create(double permitsPerSecond) {

return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer());

}

接着该方法调用了create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer())方法(该方法由于是包的访问权限,在实际的项目中,基本不会直接调用),同时创建了一个StopWatch,自动启动。

static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) {

RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */);

rateLimiter.setRate(permitsPerSecond);

return rateLimiter;

}

该方法创建了SmoothBursty实例,up-casting为RateLimiter。maxBurstSeconds固定为1,说明令牌桶中所能存储的的最大令牌数是1*QPS。接着调用setRate方法,该方法初始化一些重要的参数:

public final void setRate(double permitsPerSecond) {

checkArgument(

permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive");

synchronized (mutex()) {

doSetRate(permitsPerSecond, stopwatch.readMicros());

}

}

主要实现在SmoothRateLimiter中:

@Override

final void doSetRate(double permitsPerSecond, long nowMicros) {

resync(nowMicros);

double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond;

this.stableIntervalMicros = stableIntervalMicros;

doSetRate(permitsPerSecond, stableIntervalMicros);

}

其中resync方法是一个关键的方法,在请求令牌时也会用到,后面还会说明:

void resync(long nowMicros) {

// if nextFreeTicket is in the past, resync to now

if (nowMicros > nextFreeTicketMicros) {

double newPermits = (nowMicros - nextFreeTicketMicros) / coolDownIntervalMicros();

storedPermits = min(maxPermits, storedPermits + newPermits);

nextFreeTicketMicros = nowMicros;

}

}

从中可以看出,如果nowMicros大于nextFreeTicketMicros,会重新计算nextFreeTicketMicros和storedPermit的值。设置stableIntervalMicros,该字段表示1/QPS,即生产令牌的速率。

接着调用doSetRate方法,该方法在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, below

storedPermits = maxPermits;

} else {

storedPermits =

(oldMaxPermits == 0.0)

? 0.0 // initial state

: storedPermits * maxPermits / oldMaxPermits;

}

}

初始化maxPermits和storePermits,后者永远不会大于前者。

到此,rateLimiter初始化完成。除了resync方法,在不重新设置rate的情况,其他方法不在处理请求时用到,暂时忽略。

下面看关键的令牌申请的过程。

首先调用acquire()方法,申请令牌,无参数表示申请一个。

public double acquire() {

return acquire(1);

}

接着调用acquire(int permits)方法:

@CanIgnoreReturnValue

public double acquire(int permits) {

long microsToWait = reserve(permits);

stopwatch.sleepMicrosUninterruptibly(microsToWait);

return 1.0 * microsToWait / SECONDS.toMicros(1L);

}

reserve方法返回获取令牌所需要等待的时间,stopwatch阻塞当前线程,最后返回线程休眠的秒数。如果microsToWait为0,表示立即返回。

final long reserve(int permits) {

checkPermits(permits);

synchronized (mutex()) {

return reserveAndGetWaitLength(permits, stopwatch.readMicros());

}

}

reserve需要获取锁才可以操作,这也是令牌桶线程安全的原因,以下操作都在同步代码块中。

继续reserveAndGetWaitLength方法。

final long reserveAndGetWaitLength(int permits, long nowMicros) {

long momentAvailable = reserveEarliestAvailable(permits, nowMicros);

return max(momentAvailable - nowMicros, 0);

}

首先调用reserveEarliestAvailable,方法名说明了返回值的意义:即返回满足当前请求的最早的时钟,该值大于等于nowMicros。如何保证这一点的呢?我们看该方法:

@Override

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);

this.nextFreeTicketMicros = LongMath.saturatedAdd(nextFreeTicketMicros, waitMicros);

this.storedPermits -= storedPermitsToSpend;

return returnValue;

}

这十多行代码是整个算法实现的核心,重点说明:

首先调用resync(nowMicros),重置nextFreeTicketMicros。如果nowMicros在nextFreeTicketMicros之后,nextFreeTicketMicros=nowMicros,并往storedPermits中增加这段时间能产生的令牌。

返回值设置为当前的nextFreeTicketMicros。为什么要这样设置呢?因为如果nowMicros大于nextFreeTicketMicros,说明令牌桶肯定能满足需求(无论请求的令牌数目是多少,参见最上面的设计哲学),而resync方法已经修改了nextFreeTicketMicros值为nowMicros值,逐层返回给调用者时,等待时间为0,线程无需等待;反之,如果nowMicros小于等于nextFreeTicketMicros,说明请求过快,线程需要等待,等待的时间就是nextFreeTicketMicros-nowMicros。

接下来,storedPermitsToSpend代表令牌桶中已有的令牌数,可以用于当前的请求。但未必满足需求。

其次,freshPermits代表需要新生成的令牌数。如果storedPermits已经满足需求,则freshPermits为0。

再次,计算新生成令牌需要花费的时间,这些需要后来者偿还。

然后修改nextFreeTicketMicros的值。

最后修改storedPermits值。

至此整个处理过程结束。

经过上面的代码梳理,详细大家对RateLimiter的代码有个比较清晰的认识,但要加深理解,还需要多做debug和test。

Guava包里面包括了很多test case。我们可以把test类单拿出来,根据自己的情况添加相应的case即可。该类是com.google.common.util.concurrent. RateLimiterTest。由于很多类都使用了默认访问权限,我们需要定义一个 com.google.common.util.concurrent包,导入RateLimiterTest类。该类中,guava提供了一个FakeStopwatch的nested class。它能够让时钟按照我们的要求暂停,休眠随意的时长,并记录休眠和请求对应的事件,并已特定的格式输出。例如:R1.00代表请求给定的令牌延迟了1秒;U1.05表示stopwatch休眠1.05秒,即模拟时钟过了1.05秒。例如一个测试通过的case:

public void testSimple() {

RateLimiter limiter = RateLimiter.create(5.0, stopwatch);

limiter.acquire(); // R0.00

limiter.acquire(); // R0.20

limiter.acquire(); // R0.20

stopwatch.sleepMillis(1000); // U1.00

assertEvents("R0.00", "R0.20", "R0.20", "U1.00");

}

下面提供一个case,验证下大家的理解。

public void testOneSecondBurst3() {

RateLimiter limiter = RateLimiter.create(1.0, stopwatch);

limiter.acquire(1); // R值?

stopwatch.sleepMillis(1050);//U值?

limiter.acquire(1); // R值? nowMicros? nextFree?

stopwatch.sleepMillis(950);

limiter.acquire(1); // R值? nowMicros? nextFree?

stopwatch.sleepMillis(1000);

limiter.acquire(1); // R值? nowMicros? nextFree?

}

关注公众号“码农走向艺术”,回复消息可以获取答案幺:)

c语言令牌桶原理,令牌桶算法及实现(二)相关推荐

  1. 均匀不变lbp算法c语言代码,LBP原理介绍以及算法实现

    有些读者可能会觉得奇怪,上次推送怎么就突然说起了双线性插值而不是继续介绍经典人脸识别的算法,其实是因为在学习圆形LBP算子的时候发现需要用到双线性插值于是顺带介绍了一下.(因为个人原因没经常看公众号的 ...

  2. 高并发策略之限流:计数器、漏桶、令牌桶 三大算法的原理与实战(史上最全)

    导读 网站高可用指的就是:在绝大多的时间里,网站一直处于可以对外提供服务的正常状态. 一般以"年"为单位来统计,"9"的个数越多,代表一年中允许的不可用时间就越 ...

  3. 带你快速了解:限流中的漏桶和令牌桶算法

    在前文 <限流熔断是什么,怎么做,不做行不行?>中针对 "限流" 动作,有提到流量控制其内部对应着两种常用的限流算法. 其分别对应漏桶算法和令牌桶算法.因此会有的读者会 ...

  4. 限速之令牌桶和漏桶算法

    限速是大型服务里面必备的功能,目的是对并发控制和请求进行限速来保护系统,让系统不会因为单位时间内的请求数量太大,被打爆.对于超过了限速的那些请求,处理方法往往是:直接拒绝服务,排队等待,或者降级处理. ...

  5. 令牌桶生成令牌_使用令牌的经典ASP登录系统

    令牌桶生成令牌 This demonstration started out as a follow up to some recently posted questions on the subje ...

  6. 流量管制-令牌桶与漏桶

    Principle of token bucket 随着互联网的发展,在处理流量的方法也不仅仅为 first-come,first-served,而在共享网络中实现流量管理的基本机制就是排队.而公平算 ...

  7. 令牌桶生成令牌_设计令牌如何有效使用令牌

    令牌桶生成令牌 "It used to be that designers made an object and walked away. Today the emphasis must s ...

  8. 限流的4种策略--固定窗口、滑动窗口、漏桶、令牌桶

    01 Why 分布式系统中,由于接口API无法控制上游调用方的行为,因此当瞬时请求量突增时,会导致服务器占用过多资源,发生响应速度降低.超时.乃至宕机,甚至引发雪崩造成整个系统不可用. 限流,Rate ...

  9. C语言桶排序Bucket sort算法(附完整源码)

    桶排序Bucket sort算法 桶排序Bucket sort算法的完整源码(定义,实现,main函数测试) 桶排序Bucket sort算法的完整源码(定义,实现,main函数测试) #includ ...

  10. Algorithm:C++语言实现之SimHash和倒排索引算法相关(抽屉原理、倒排索、建立查找树、处理Hash冲突、Hash查找)

    Algorithm:C++语言实现之SimHash和倒排索引算法相关(抽屉原理.倒排索.建立查找树.处理Hash冲突.Hash查找) 目录 一.SimHash算法 1.SimHash算法五个步骤 2. ...

最新文章

  1. java执行linux shell命令,并拿到返回值
  2. 大数据 互联网架构阶段 Nginx的使用
  3. HDU 1166 敌兵布阵(线段树单点加区间查询)
  4. 我的世界1 11java,Editing Java版Alpha v1.0.11
  5. Boost::Regex 使用方法 (英文)
  6. 新闻发布项目——业务逻辑层(categoryTBService)
  7. 更换mysql_Docker搭建MySQL主从复制
  8. 注释工具_苹果已购丨Notability丨功能强大而简单易用的笔记及PDF注释工具
  9. python2版本libnum
  10. bzoj 5302: [Haoi2018]奇怪的背包
  11. 我的第一篇CBBLOGS博客
  12. 程序员有多少读过《人性的弱点》?项目经理呢?
  13. 利用Power Design 进行数据库设计(超详细)
  14. matlab之图例legend的数字变量显示
  15. 数据增强——mixup
  16. php中where条件whereRaw,「laravel whereRaw 和 where(DB::raw(''))」- 海风纷飞Blog
  17. 2020-GKCTF-Reverse
  18. vue3兄弟之间传值兄弟之间方法怎么调用?保姆级讲解
  19. 传奇单机版就是自己在家里架设一个
  20. Zxing扫码库优化思路

热门文章

  1. 最适合程序员的笔记软件
  2. 互联网晚报 | 10月27日 星期三 | 高德车道级导航正式发布;阿里淘菜菜发布“本地菜”计划;特斯拉市值破万亿美元...
  3. houdini 常用
  4. 红外测距GY20A21
  5. java yml_Spring Boot使用yml格式进行配置的方法
  6. 人工智能之-产生式系统
  7. 系统JNI调用和使用
  8. 趋势突破策略与期权——以Dual Thrust为例
  9. 【8】电压比较器的阈值,窗口电压
  10. Java入门学习笔记——郝斌(三)线程