今天介绍Guava的限流RateLimit,主要介绍2个,一个是源码分析,一个是仿造他的原理写一个优化版本。
一、源码分析
首先我个人认为RateLimit的设计思想很好很优秀,但是写的有点瑕疵,后面我会说到。
①create方法
第一个参数是Guava的秒表工具用来计时的,第二个参数是你输入的,也就是每秒生成的令牌数。SmoothBursty是RateLimit的子类,看看他的构造方法。
第二个参数默认值为1,官方意思是当ratelimit不工作时候可以保存多少秒的标签,其实我觉得真的是不清不楚的,网上很多人也都直接来过来粘贴进去。其实这个意思就是这个参数是用来计算你的桶的大小用的,你create时候只输入了每秒产生多少个,乘以这个参数就等于桶大小,也就是说默认方法是每秒产生多少个 == 桶size。
setRate方法就是校验一下参数然后加锁调用doSetRate方法,然后你就可以看到我上面说的maxBurstSeconds参数的作用了。storedPermits是当前桶内令牌数,由于我们舒适化时候都没对maxPermits赋值,所以这个方法最终storedPermits会赋0。
这样create就完成了,这里我只介绍普通模式,热启动不做介绍,其实也差不多。
②tryAcquired
第一个参数是每次消费多少个令牌,第二个是获取不到令牌时候延时多久放弃,第三个是时间工具类。可以看出来最重要其实就是canAcquire和reserveAndGetWaitLength方法,接下来我们逐一介绍着两个方法。
1、canAcquire
queryEarliestAvailable方法是获取 最近一个可以获得的令牌的时间(这个话听上去有点绕,没事有个概念即可,后面会详细介绍)。那么canAcquire方法用公式表示就是:最近可以获得令牌时间 <= 当前时间 + 超时时间。
2、reserveAndGetWaitLength 次方法是时间Ratelimit的核心中的核心,讲的比较多,公式也比较多,耐心看完!
reserveAndGetWaitLength 调用了reserveEarliestAvailable方法,看看里头实现了什么
首先第一个参数是你这次请求需要多少个令牌,第二个是当前时间。然后第一步调用了一个resync同步方法,看看这个方法
这里有几个成员变量,我先解释一下什么意思。
nextFreeTicketMicros:距离下一个可以获得令牌的时间,如果当前时间大于它,那么至于当前时间。
storedPermits:当前桶里令牌数
maxPermits:桶大小
permitsPerSecond:每秒生成的令牌数
stableIntervalMicros:生成一个令牌需要的时间,单位是微秒,计算公式是:1000000 * 1s / permitsPerSecond
这个方法,就是计算当开始生成令牌的时间到现在这段时间生成了多少令牌,这个也是Guava跟一般令牌桶实现不一样的地方,它并不是用线程异步放令牌,而是通过计算时间差而得出。这样做有一个好处就是,如果我现在要针对每个接口限流,我需要每个接口创建一个桶,如果用异步线程生产令牌,那要开多少个线程。
然后把桶最大值和计算出的令牌数对比,选一个小的赋给当前令牌数参数。并且把当前时间给nextFreeTicketMicros。
回到上一个方法。
同步完成后,比较需要的令牌和当前桶里剩余令牌,并且做差值。storePermitsToWaitTime方法在普通限流时候都是返回0,所以不用看。然后计算等待时间,公式是:(需要的令牌数 - 当前剩余令牌数)* 每个令牌生成时间。nextFreeTicketMicros此时由于上面调用过resync方法,所以现在是当前时间所以nextFreeTicketMicros = nowMics + waitMIcros;最后减少桶内令牌即可。
看到这里就问你懵不懵!,尤其是nextFreeTicketMicros这个参数,其实你回到最早我们canacquire方法
这里queryEarliestAvailable方法就是获取nextFreeTicketMicros,来比较时间,看时候有令牌。而nextFreeTicketMicros是由当前时间加上等待时间算出来的,也就是说这个参数是说下一个生成令牌的时间,如果当前时间小于这个参数,那么就是说令牌还没生成所以就返回false。
二、优化
代码我贴出来了,我简化了很多操作,但是性能还是跟Guava是一样的(做过压测),唯一要注意的点,就是计算当前令牌数的时候。
@Slf4j public class RateLimit { //当前桶里ticket数量 private double currentTicketNum; //桶大小 private long bucketSize; //距离下一个可获得的ticket的时间 private long nextFreeTicketMic; //每秒生成的ticket数量 private double ticketPerSecond; private double lastConsumMic; private Stopwatch stopwatch; private Lock lock; //创建 public static RateLimit create(long ticketPerSecond) { if (ticketPerSecond < 1) { throw new IllegalArgumentException("param must > 1"); } RateLimit rateLimit = new RateLimit(); rateLimit.bucketSize = ticketPerSecond; rateLimit.ticketPerSecond = ticketPerSecond; rateLimit.stopwatch = Stopwatch.createStarted(); rateLimit.lock = new ReentrantLock(); return rateLimit; } //非阻塞获取ticket,该方法只允许一次获取一个ticket并且不允许提前消费 public boolean tryAcquire() { try { lock.lock(); long nowMic = this.stopwatch.elapsed(MICROSECONDS); //判断如果当前时间小于可以获得ticket时间直接返回false if (this.nextFreeTicketMic > nowMic) { return false; } else { //计算这一段时间产生的ticket数量,你发现没我这里用的不是nextFreeTicketMic而是lastConsumMic, //确实用作者写的没问题,但是它算出来的总比实际的当前令牌数少1个,所以这里做了优化 this.currentTicketNum = Math.min(bucketSize, currentTicketNum + (nowMic - lastConsumMic) / buildTicketPerMic()); //该次请求消耗ticket if (currentTicketNum > 0) { currentTicketNum--; } long waitMic = 0L; if (currentTicketNum <= 0) { waitMic = (long) buildTicketPerMic(); } //计算下一次可以获取到ticket的时间 lastConsumMic = nowMic; this.nextFreeTicketMic = nowMic + waitMic; } } catch (Exception e) { log.error("获取ticket异常", e); } finally { lock.unlock(); } return true; } private double buildTicketPerMic() { return SECONDS.toMicros(1L) / ticketPerSecond; } }