大纲
1.漏桶算法的实现对比
(1)普通思路的漏桶算法实现
(2)节省线程的漏桶算法实现
(3)Sentinel中的漏桶算法实现
(4)Sentinel中的漏桶算法与普通漏桶算法的区别
(5)Sentinel中的漏桶算法存在的问题
2.令牌桶算法的实现对比
(1)普通思路的令牌桶算法实现
(2)节省线程的令牌桶算法实现
(3)Guava中的令牌桶算法实现
(4)Sentinel中的令牌桶算法实现
(5)Sentinel中的令牌桶算法总结
1.漏桶算法的实现对比
(1)普通思路的漏桶算法实现
(2)节省线程的漏桶算法实现
(3)Sentinel中的漏桶算法实现
(4)Sentinel中的漏桶算法与普通漏桶算法的区别
(5)Sentinel中的漏桶算法存在的问题
(1)普通思路的漏桶算法实现
一.漏桶算法的处理流程
二.漏桶算法的主要特点
三.漏桶算法的优缺点
一.漏桶算法的处理流程
漏桶算法的核心思想是以固定速率流出。
步骤一:当新的请求到达时,会将新的请求放入缓冲区(请求队列)中,类似于往水桶里注水。
步骤二:系统会以固定的速度处理缓冲区中的请求,类似于水从窟窿中以固定的速度流出,比如开启一个后台线程定时以固定的速度从缓冲区中取出请求然后进行分发处理。
步骤三:如果缓冲区已满,则新的请求将被拒绝或丢弃,类似于水溢出。
二.漏桶算法的主要特点
特点一:固定速率
水从桶底的孔洞中以固定速率流出,类似于网络中以固定速率发送数据包。但写入速度不固定,也就是请求不是匀速产生的。相当于生产者生产消息不固定,消费者消费消息是匀速消费的。
特点二:有限容量
桶的容量有限,当桶满时,新到达的水会溢出,即拒绝超过容量的请求。
特点三:先进先出(FIFO)
水按照先进先出的顺序从桶中流出,类似于请求的处理顺序。
这种算法的一个重要特性是:无论请求的接收速率如何变化,请求的处理速率始终是稳定的,这就确保了系统的负载不会超过预设的阈值。但是由于请求的处理速率是固定的,所以无法处理突发流量。此外如果入口流量过大,漏桶可能会溢出,导致请求丢失。
三.漏桶算法的优缺点
优点一:平滑流量
由于以固定的速率处理请求,所以可以有效地平滑和整形流量,避免流量的突发和波动,类似于消息队列的削峰填谷的作用。
优点二:防止过载
当流入的请求超过桶的容量时,可以直接丢弃请求,防止系统过载。
缺点一:无法处理突发流量
由于漏桶的出口速度是固定的,无法处理突发流量。例如,即使在流量较小的时候,也无法以更快的速度处理请求。
缺点二:可能会丢失数据
如果入口流量过大,超过了桶的容量,那么就需要丢弃部分请求。在一些不能接受丢失请求的场景中,这可能是一个问题。
缺点三:不适合处理速率变化大的场景
如果处理速率变化大,或需要动态调整处理速率,则无法满足。
漏桶算法适用于需要以固定速率处理请求的场景。在多数业务场景中,其实并不需要按照严格的速率进行请求处理。而且多数业务场景都需要应对突发流量的能力,所以会使用令牌桶算法。
(2)节省线程的漏桶算法实现
漏桶算法可以通过延迟计算的方式来实现。延迟计算指的是不需要单独的线程来定时生成令牌或从漏桶中定时取请求,而是由调用限流器的线程自己计算是否有足够的令牌以及需要sleep的时间。延迟计算的方式可以节省一个线程资源。
public class LeakyBucketLimiter { //桶的最大容量 public static long threshold = 10; //当前桶内的水量 public static long count = 0; //漏水速率(每秒5次) public static long leakRate = 5; //上次漏水时间 public static long lastLeakTime = System.currentTimeMillis(); //限流方法,返回true表示通过 public boolean canPass() { //调用漏水方法 this.leak(); //判断是否超过最大请求数量 if (count < threshold) { count++; return true; } return false; } //漏水方法,计算并更新这段时间内漏水量 private void leak() { //获取系统当前时间 long currentTime = System.currentTimeMillis(); //计算这段时间内,需要流出的水量 long leakWater = (currentTime - lastLeakTime) * leakRate / 1000; count = Math.max(count - leakWater, 0); //更新最近一次的漏水时间 lastLeakTime = currentTime; } }
(3)Sentinel中的漏桶算法实现
在RateLimiterController的canPass()方法中,为了判断是否超出QPS阈值,通过原子类变量latestPassedTime简化成单线程让请求先后通过的处理模型。为了尽量让业务不受Sentinel影响,采用预估请求的被处理时间点的方式。也就是无需等前面的请求完全被处理完,才确定后面的请求被处理的时间。因为在普通的漏桶算法中,是处理完一个请求,才从漏桶取出水滴。而RateLimiterController的漏桶算法,则是假设请求已经被通过了。
具体的判断逻辑如下:首先获取系统的当前时间currentTime。然后计算在满足流控规则中限制的QPS阈值count的情况下,先后的两个请求被允许通过时的最小时间间隔costTime。接着计算当前请求最早的预期通过时间expectedTime,也就是此次请求预计会在几时几分几秒内通过。最后比较expectedTime和currentTime就可知当前请求是否允许通过了。
一.如果expectedTime小于等于currentTime
也就是当前请求最早的预期通过时间比系统当前时间小。如果在此时(currentTime)通过当前请求,则当前请求的通过时间就比它最早的预期通过时间(expectedTime)要晚,即当前请求和最近通过的请求的时间间隔变大了,所以此时不会超QPS阈值。于是返回true允许通过,同时更新最近允许请求通过的时间戳为当前时间。
二.如果expectedTime大于currentTime
也就是当前请求最早的预期通过时间比系统当前时间大。如果在此时(currentTime)通过当前请求,则当前请求的通过时间就比它最早的预期通过时间(expectedTime)要早,即当前请求和最近通过的请求的时间间隔变小了,比最小间隔时间costTime还小,所以此时必然会超QPS阈值。因此返回进行等待或者返回false不允许通过,等待的最小时间就是:最近通过请求的时间 + 先后两个请求允许通过时的最小间隔时间 - 当前时间。
需要注意:Sentinel流量控制的漏桶算法,只能限制在costTime内的流量激增,限制不了costTime外的流量激增。比如系统启动完一瞬间就涌入大量并发请求,此时的流量激增限制不了。又比如系统处理完正常流量的最后一个请求,隔了costTime+的时间后,突然涌入超QPS阈值的并发请求,此时也限制不了这种情况的流量激增。但如果系统处理完正常流量的最后一个请求,隔了costTime-的时间后,突然涌入超QPS阈值的并发请求,此时则可以限制这种情况的流量激增。
同时,为了避免等待的各个并发线程被同时唤醒,可以利用原子变量的addAndGet()方法 + 假设等待请求已被通过的方式,实现需要等待的并发请求进行睡眠等待的时间都不一样,从而实现并发请求排队等待的效果。
实现排队等待效果的核心逻辑:由于latestPassedTime的原子性,每个线程都会获得不一样的oldTime。接着根据oldTime - 当前时间,就可以得到每个线程需要睡眠等待的时间waitTime。此时的waitTime都将会不一样,从而避免并发线程同时被唤醒的情况。将latestPassedTime按costTime进行自增,其实相当于假设当前请求在不超过QPS阈值的情况下,被允许通过了。
public class RateLimiterController implements TrafficShapingController { //排队等待的意思是超出阈值后等待一段时间,maxQueueingTimeMs就是请求在队列中的最大等待时间 private final int maxQueueingTimeMs; //流控规则中限制QPS的阈值,也就是QPS超出多少后会进行限制 private final double count; //最近允许一个请求通过的时间,每次请求通过后就会更新此时间,可以根据该时间计算出当前请求最早的预期通过时间 //注意:Sentinel是在业务前面的,尽量不要让业务受到Sentinel的影响,所以不需要等请求完全被处理完,才确定请求被通过的时间 private final AtomicLong latestPassedTime = new AtomicLong(-1); public RateLimiterController(int timeOut, double count) { this.maxQueueingTimeMs = timeOut; this.count = count; } @Override public boolean canPass(Node node, int acquireCount) { return canPass(node, acquireCount, false); } @Override public boolean canPass(Node node, int acquireCount, boolean prioritized) { //acquireCount代表每次从桶底流出多少个请求 //如果acquireCount小于等于0,则表示无需限流直接通过,不过acquireCount一般默认是1 if (acquireCount <= 0) { return true; } //如果限流规则的count(即限制QPS的阈值)小于等于0,则直接拒绝,相当于一个请求也不能放行 if (count <= 0) { return false; } //1.首先获取系统的当前时间 long currentTime = TimeUtil.currentTimeMillis(); //2.然后计算,在满足流控规则中限制的QPS阈值count的情况下,先后的两个请求被允许通过时的最小间隔时间(假设请求是单线程处理的) long costTime = Math.round(1.0 * (acquireCount) / count * 1000); //3.接着计算当前请求最早的预期通过时间 = 满足QPS阈值下的两个请求的最小时间间隔 + 上次请求的通过时间 long expectedTime = costTime + latestPassedTime.get(); //4.最后判断当前请求最早的预期通过时间是否比系统当前时间小 if (expectedTime <= currentTime) {//等价于没有超出QPS阈值 //当前请求最早的预期通过时间比系统当前时间小 //如果在此时(currentTime)通过当前请求,那么当前请求的实际通过时间就比它最早的预期通过时间(expectedTime)要晚 //也就是当前请求和最近通过的请求的时间间隔变大了,所以此时不会超QPS阈值,返回true允许通过 //由这里可知,latestPassedTime并不会影响costTime,也就是说,多个线程可以并发执行到这里而不受阈值的影响 //这意味着,Sentinel流量控制的漏桶算法,只能限制在costTime时间内的流量激增,限制不了costTime时间外的流量激增 //比如系统启动完的那一瞬间就涌入超出QPS阈值的并发请求,此时的这种流量激增是限制不了的; //又比如系统正常运行时处理完了正常流量的最后一个请求,隔了costTime+的时间后,突然涌入超出QPS阈值的并发请求,此时也限制不了; //只能限制住这样的一种情况:系统正常运行处理完正常流量的最后一个请求,隔了costTime-的时间,突然涌入超出QPS阈值的并发请求 latestPassedTime.set(currentTime); return true; } else { //如果不是,即当前请求最早的预期通过时间比系统当前时间大 //则说明latestPassedTime.get()大了,也就是上一个可能由于QPS超出阈值的原因导致请求处理慢了,所以需要进行等待 //计算当前请求的等待时间,用于判断是否超出流控规则设置的最大等待时间 long waitTime = costTime + latestPassedTime.get() - TimeUtil.currentTimeMillis(); if (waitTime > maxQueueingTimeMs) { //如果超出最大等待时间,则直接返回false return false; } else {//等价于超出了QPS阈值 //当前请求最早的预期通过时间比系统当前时间大 //如果在此时(currentTime)通过当前请求,那么当前请求的实际通过时间就比它最早的预期通过时间(expectedTime)要早 //也就是当前请求和最近通过的请求的时间间隔变小了,比最小间隔时间costTime还小 //所以此时必然会超QPS阈值,因此返回进行等待或者返回false不允许通过 //而等待的最小时间,就是最近通过请求的时间 + 先后两个请求允许通过时的最小间隔时间 - 当前时间 //首先通过latestPassedTime这个原子变量的addAndGet()方法 //将最近通过请求的时间latestPassedTime,加上先后两次请求需要的最小间隔时间costTime,得到当前请求本来预期的通过时间 //注意: //当多个并发线程执行到此处时,由于latestPassedTime的原子性,每个线程都会获得不一样的oldTime //接着根据oldTime - 当前时间,就可以得到每个线程需要睡眠等待的时间waitTime //此时的waitTime都将会不一样,从而避免并发线程同时被唤醒的情况 //将latestPassedTime进行自增,其实相当于假设当前请求在不超过QPS阈值的情况下,被允许通过了 long oldTime = latestPassedTime.addAndGet(costTime); try { //然后计算当前请求需要等待多久 = 当前请求最早的预期通过时间 - 当前系统时间 waitTime = oldTime - TimeUtil.currentTimeMillis(); //如果等待时间大于流控规则设置的最大等待时间,则需要回滚刚才更新的最近通过请求的时间 //也就是将latestPassedTime减去costTime,然后返回false表示请求无法通过 if (waitTime > maxQueueingTimeMs) { //如果发现新计算的等待时间 大于 最大等待时间,则需要回滚latestPassedTime latestPassedTime.addAndGet(-costTime); return false; } //in race condition waitTime may <= 0 if (waitTime > 0) { //当前请求需要进行等待 Thread.sleep(waitTime); } return true; } catch (InterruptedException e) { } } } return false; } }
(4)Sentinel中的漏桶算法与普通漏桶算法的区别
区别一:普通漏桶算法使用的是真实队列
节省线程的漏桶算法会有一个单独的字段去记录当前桶内的水量,也就是请求量。每通过一个请求,则该字段值-1。反之,每新进一个请求,此字段值+1。
区别二:Sentinel漏桶算法使用的是虚拟队列
它没有单独的字段去记录当前桶内的请求量,而是根据最近通过请求的时间得出当前请求最早的预期通过时间来实现。其本质就是先假设当前请求可以通过,然后再按照先后请求在QPS阈值下可以允许通过时的最大时间间隔,来计算出当前请求最早的预期通过时间,再对比是否和当前发生冲突。
区别三:普通漏桶算法使用的策略是直接拒绝
如果流入速度大于流出速度,则直接拒绝。
区别四:Sentinel漏桶算法使用的策略是排队等待
如果超出了阈值,则不会直接拒绝请求,而是会等待一段时间,只要在这段时间内能处理到这个请求就不会拒绝掉。
(5)Sentinel中的漏桶算法存在的问题
问题一:在costTime时间内出现流量激增才能限流
如果在costTime时间外,即最后一次请求通过的时间已经好久了,突然流量激增以及并发进入系统,那么此时是无法限制住的。
问题二:Sentinel排队等待流控效果支持的QPS阈值不能超过1000
如果超过1000,且costTime计大于等于0.5,则会认为间隔时间都是1ms。如果costTime小于0.5,则认为配置失效,相当于没有配置此条流控规则。
long costTime = Math.round(1.0 * (acquireCount) / count * 1000); long costTime = Math.round(1.0 * (1) / 1100 * 1000)约等于0.9ms;
默认情况下,acquireCount的值是1,那么:如果QPS阈值count在1000~2000,此时costTime = 1,限流不受阈值影响。如果QPS阈值count大于2000,此时costTime = 0,限流配置失效。
2.令牌桶算法的实现对比
(1)普通思路的令牌桶算法实现
(2)节省线程的令牌桶算法实现
(3)Guava中的令牌桶算法实现
(4)Sentinel中的令牌桶算法实现
(5)Sentinel中的令牌桶算法总结
(1)普通思路的令牌桶算法实现
一.令牌桶算法的处理流程
二.令牌桶算法的特点
三.令牌桶算法的优缺点
四.漏桶算法和令牌桶算法的对比
五.令牌桶算法和漏桶算法的核心区别
一.令牌桶算法的处理流程
令牌桶算法的核心思想是以固定速率流入。
步骤一:初始化令牌桶,设置其容量和生成速率。
步骤二:当有新请求到来时,检查令牌桶中是否有足够的令牌。如果有足够的令牌,则允许请求通过,并从令牌桶中扣除相应数量令牌。如果没有足够的令牌,则拒绝请求。
步骤三:系统会以固定的速度添加令牌,直到达到令牌桶容量,比如开启一个后台线程以固定的速度向令牌桶中添加令牌。
二.令牌桶算法的特点
特点一:支持突发流量
令牌桶算法允许在限流内应对突发流量,有助于提高系统的响应能力。
特点二:平滑处理速率
和漏桶算法一样,令牌桶算法也可以平滑处理流量,避免处理速率突变。
令牌桶算法的一个重要特性是,它能够处理突发流量。当桶中有足够的令牌时,可以一次性处理多个请求。这对于需要处理突发流量的应用场景非常有用,但是又不会无限制的增加处理速率导致压垮服务器,因为桶内令牌数量是有限制的。
三.令牌桶算法的优缺点
优点一:可以处理突发流量
令牌桶算法可以处理突发流量。当桶满时,能够以最大速度处理请求。这对于需要处理突发流量的应用场景非常有用。
优点二:限制请求处理的平均速率
在长期运行中,请求的处理速率会被限制在预定义的平均速率下,也就是生成令牌的速率。
优点三:灵活性
与漏桶算法相比,令牌桶算法提供了更大的灵活性。例如,可以动态地调整生成令牌的速率。
缺点一:可能导致过载
如果令牌产生速度过快,可能会导致大量突发流量,使网络或服务过载。
缺点二:需要存储空间
令牌桶需要一定的存储空间来保存令牌,可能会导致内存资源的浪费。
四.漏桶算法和令牌桶算法的对比
漏桶算法是突然往桶里注水,但是漏水的窟窿是固定大小的,因此流出水的速度是固定的,也就是"生产不限速,消费限速"。
令牌桶算法是突然从桶中抽水,也就是固定大小的窟窿变成了入水口,而没桶盖的桶口变成了出水口。相当于入水速度变得固定了,而出水速度不做限制了,也就是"生产限速,消费不限速"。
五.令牌桶算法和漏桶算法的核心区别
令牌桶算法允许用户在短时间内发起更多请求,从而支持突发流量。漏桶算法只能支持每秒固定处理一定数量的请求,从而不支持突发流量。这就是令牌桶和漏桶的核心区别。
(2)节省线程的令牌桶算法实现
令牌桶算法可以通过延迟计算的方式来实现。延迟计算指的是不需要单独的线程来定时生成令牌或从漏桶中定时取请求,而是由调用限流器的线程自己计算是否有足够的令牌以及需要sleep的时间。延迟计算的方式可以节省一个线程资源。
public class TokenBucketLimiter { //桶的最大容量 public static long threshold = 10; //当前桶内的令牌数 public static long count = 0; //令牌生成速率(每秒5次) public static long tokenRate = 5; //上次生成令牌的时间 public static long lastRefillTime = System.currentTimeMillis(); //限流方法,返回true表示通过 public boolean canPass() { //调用生成令牌方法 this.refillTokens(); //判断桶内是否还有令牌 if (count > 0) { count--; return true; } return false; } //生成令牌方法,计算并更新这段时间内生成的令牌数量 private void refillTokens() { long currentTime = System.currentTimeMillis(); //计算这段时间内,需要生成的令牌数量 long refillTokens = (currentTime - lastRefillTime) * tokenRate / 1000; //更新桶内的令牌数 count = Math.min(count + refillTokens, threshold); //更新令牌生成时间 lastRefillTime = currentTime; } }
(3)Guava中的令牌桶算法实现
一.SmoothBursty的初始化
二.SmoothBursty的acquire()方法
三.SmoothWarmingUp的初始化
四.SmoothWarmingUp的acquire()方法
SmoothBursty和SmoothWarmingUp这两种限流器都使用了预支令牌的思路来实现,就是当前线程获取令牌的代价(阻塞时间)需要由下一个线程来支付。这样可以减少线程阻塞的概率,因为下一个请求不确定什么时候才来。如果下一个请求很久才来,那么这段时间产生的新令牌已满足下一个线程的需求,这样就不用阻塞了。
一.SmoothBursty的初始化
RateLimiter不保存上一次请求的时间,但是它保存下一次请求期望到达的时间。如果下一个请求的预期到达时间实际上已经过去了,并且假设下次请求期望到达的时间点是past,现在的时间点是now。那么now - past的这段时间表示RateLimiter没有被使用,所以在这段空闲的时间内RateLimiter就会增加storedPermits的数量。
@Beta @GwtIncompatible @SuppressWarnings("GoodTime") public abstract class RateLimiter { ... //Creates a RateLimiter with the specified stable throughput, //given as "permits per second" (commonly referred to as QPS, queries per second). //The returned RateLimiter ensures that on average no more than permitsPerSecond are issued during any given second, //with sustained requests being smoothly spread over each second. //When the incoming request rate exceeds permitsPerSecond the rate limiter will release one permit every (1.0 / permitsPerSecond) seconds. //When the rate limiter is unused, bursts of up to permitsPerSecond permits will be allowed, //with subsequent requests being smoothly limited at the stable rate of permitsPerSecond. //创建一个具有指定稳定吞吐量的RateLimiter,传入的"permits per second"通常称为QPS、每秒查询量; //返回的RateLimiter确保在任何给定的秒期间平均不超过permitsPerSecond的令牌被发出,持续的请求将在每一秒内被平稳地通过; //当传入请求的速率超过permitsPerSecond时,速率限制器将每隔(1.0/permitsPerSecond)秒释放一个令牌; //当速率限制器未被使用时,将允许突发式的高达permitsPerSecond的令牌,而随后的请求将以permitsPerSecond的稳定速率被平滑地限制; //对外暴露的创建方法 //@param permitsPerSecond the rate of the returned RateLimiter, measured in how many permits become available per second. public static RateLimiter create(double permitsPerSecond) { //The default RateLimiter configuration can save the unused permits of up to one second. //This is to avoid unnecessary stalls in situations like this: //A RateLimiter of 1qps, and 4 threads, all calling acquire() at these moments: //T0 at 0 seconds、T1 at 1.05 seconds、T2 at 2 seconds、T3 at 3 seconds //Due to the slight delay of T1, T2 would have to sleep till 2.05 seconds, and T3 would also have to sleep till 3.05 seconds. //默认的RateLimiter配置可以保存长达一秒钟的未被使用的令牌; //这是为了避免在这种情况下出现不必要的停顿: //一个由1qps和4个线程组成的RateLimiter,所有线程都在如下这些时刻调用acquired(): //Thread0在0秒、Thread1在1.05秒、Thread2在2秒、Thread3在3秒 //由于Thread1的轻微延迟,Thread2必须睡眠到2.05秒,Thread3也必须睡眠到3.05秒 //内部调用一个qps设定 + 起始时间StopWatch的构建函数. //这里传入的SleepingStopwatch是一个以系统启动时间的一个相对时间的计量. //后面的读时间偏移是以这个开始的时间偏移为起始的. return create(permitsPerSecond, SleepingStopwatch.createFromSystemTimer()); } @VisibleForTesting static RateLimiter create(double permitsPerSecond, SleepingStopwatch stopwatch) { //指定了令牌桶中最多存储1秒的令牌数 RateLimiter rateLimiter = new SmoothBursty(stopwatch, 1.0 /* maxBurstSeconds */); //调用RateLimiter.setRate()方法 rateLimiter.setRate(permitsPerSecond); return rateLimiter; } //Updates the stable rate of this RateLimiter, //that is, the permitsPerSecond argument provided in the factory method that constructed the RateLimiter. //Currently throttled threads will not be awakened as a result of this invocation, //thus they do not observe the new rate; only subsequent requests will. //Note though that, since each request repays (by waiting, if necessary) the cost of the previous request, //this means that the very next request after an invocation to setRate() will not be affected by the new rate; //it will pay the cost of the previous request, which is in terms of the previous rate. //The behavior of the RateLimiter is not modified in any other way, //e.g. if the RateLimiter was configured with a warmup period of 20 seconds, //it still has a warmup period of 20 seconds after this method invocation. //更新该RateLimiter的稳定速率,即在构造RateLimiter的工厂方法中提供permitsPerSecond参数; //当前被限流的线程将不会由于这个调用而被唤醒,因此它们没有观察到新的速率;只有随后的请求才会; //但是要注意的是,由于每个请求(如果需要,通过等待)会偿还先前请求的成本, //这意味着调用setRate()方法后的下一个请求将不会受到新速率的影响, //它将按照先前的速率处理先前请求的成本; //RateLimiter的行为不会以任何其他方式修改, //例如:如果RateLimiter被配置为具有20秒的预热周期,在该方法调用之后,它仍然有20秒的预热期; //@param permitsPerSecond the new stable rate of this {@code RateLimiter} public final void setRate(double permitsPerSecond) { checkArgument(permitsPerSecond > 0.0 && !Double.isNaN(permitsPerSecond), "rate must be positive"); //在同步代码块中设定速率 synchronized (mutex()) { //调用SmoothRateLimiter.doSetRate()方法 doSetRate(permitsPerSecond, stopwatch.readMicros()); } } ... } @GwtIncompatible abstract class SmoothRateLimiter extends RateLimiter { //The currently stored permits. //令牌桶中当前缓存的未消耗的令牌数 double storedPermits; //The maximum number of stored permits. //令牌桶中允许存放的最大令牌数 double maxPermits; //The interval between two unit requests, at our stable rate. //E.g., a stable rate of 5 permits per second has a stable interval of 200ms. //按照我们稳定的速率,两个单位请求之间的时间间隔;例如,每秒5个令牌的稳定速率具有200ms的稳定间隔 double stableIntervalMicros; //The time when the next request (no matter its size) will be granted. //After granting a request, this is pushed further in the future. Large requests push this further than small requests. //下一个请求(无论大小)将被批准的时间. //在批准请求后,这将在未来进一步推进,大请求比小请求更能推动这一进程。 private long nextFreeTicketMicros = 0L;//could be either in the past or future ... //这是一个可以重复调用的函数. //第一次调用和非第一次调用的过程有些不一样,目的是设定设定最大令牌数maxPermits和已存储的令牌数storedPermits @Override final void doSetRate(double permitsPerSecond, long nowMicros) { //调用SmoothRateLimiter.resync()方法,重试计算和同步存储的预分配的令牌. resync(nowMicros); //计算稳定的发放令牌的时间间隔. 单位us, 比如qps为5, 则为200ms即20万us的间隔进行令牌发放. double stableIntervalMicros = SECONDS.toMicros(1L) / permitsPerSecond; this.stableIntervalMicros = stableIntervalMicros; //调用SmoothBursty.doSetRate()设定最大令牌数maxPermits和已存储的令牌数storedPermits doSetRate(permitsPerSecond, stableIntervalMicros); } //Updates storedPermits and nextFreeTicketMicros based on the current time. //根据当前时间,更新storedPermits和nextFreeTicketMicros变量 //注意: 在初始化SmoothBursty时会第一次调用resync()方法,此时各值的情况如下: //coolDownIntervalMicros = 0、nextFreeTicketMicros = 0、newPermits = 无穷大. //maxPermits = 0(初始值,还没有重新计算)、最后得到的: storedPermits = 0; //同时,nextFreeTicketMicros = "起始时间" 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; } } abstract void doSetRate(double permitsPerSecond, double stableIntervalMicros); ... //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,其中的storedPermits会被转换为0; //它可以保存的最大令牌数量(当RateLimiter未使用时)是根据时间定义的, //从这个意义上说:如果RateLimiter是2qps,并且这个时间被指定为10秒,那么我们最多可以保存2*10=20个令牌; static final class SmoothBursty extends SmoothRateLimiter { //The work (permits) of how many seconds can be saved up if this RateLimiter is unused? //如果这个速率限制器RateLimiter没有被使用,那么可以节省多少秒的工作(令牌)? final double maxBurstSeconds; SmoothBursty(SleepingStopwatch stopwatch, double maxBurstSeconds) { super(stopwatch); this.maxBurstSeconds = maxBurstSeconds; } @Override void doSetRate(double permitsPerSecond, double stableIntervalMicros) { //初次设定的时候,oldMaxPermits = 0.0 double oldMaxPermits = this.maxPermits; //新的(当前的)maxPermits为burst的时间周期(1秒) * 每周期的令牌数. maxPermits = maxBurstSeconds * permitsPerSecond; if (oldMaxPermits == Double.POSITIVE_INFINITY) { //if we don't special-case this, we would get storedPermits == NaN, below storedPermits = maxPermits; } else { //初始化SmoothBursty,执行到此处时,storedPermits为0 storedPermits = (oldMaxPermits == 0.0) ? 0.0 : storedPermits * maxPermits / oldMaxPermits; } } @Override long storedPermitsToWaitTime(double storedPermits, double permitsToTake) { return 0L; } @Override double coolDownIntervalMicros() { return stableIntervalMicros; } } ... }
二.SmoothBursty的acquire()方法
