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查看本机端口是否被占用源码等。感谢大家对网站的支持!
上一篇: JavaScript实现Y组合子函数
下一篇: java中参数传递方式详解