使用Guava RateLimterr如何解决高并发场景下的限流问题的,假设有一个线程池,每秒只能处理两个任务
那么就需要限流器上场了,接下面是一个关于限流器的基本使用的代码
public static void main(String[] args) {
//限流器流速:2个请求/秒
RateLimiter limiter = RateLimiter.create(2.0);
//执行任务的线程池
ExecutorService es = Executors.newFixedThreadPool(1);
//记录上一次执行时间
prev = System.nanoTime();
//测试执行20次
for (int i=0; i<20; i++){
//限流器限流
limiter.acquire();
//提交任务异步执行
es.execute(()->{
long cur=System.nanoTime();
//打印时间间隔:毫秒
System.out.println((cur-prev)/1000_000);
prev = cur;
});
}
/* 输出结果:
…
500
499
499
500
499*/
}
我们创建了一个限流器,让其每秒可以处理两个请求,然后在真正执行任务之前,调用acquire()方法可以起到限流的作用,而且线程的时间间隔基本在500毫秒之间
那么关于限流器的实现,使用了令牌桶算法,这个算法的核心在于,通过限流器,需要拿到限流器提供的令牌,这个限流器的核心思想就在于令牌的发放速度,如果直接使用一个定时线程池,那么会有一个问题,就是在服务器高压情况下,定时任务可能出现不准确的问题
假设令牌以每500毫秒添加到令牌桶中,
如果令牌桶容量为a,那么如果再往其中添加的令牌都会被丢弃
请求能够通过限流器的前提条件是拿到了令牌
如何去实现令牌桶算法,很简单的一个算法思路,就是去计算下个令牌发放的事件,如果容量为1,速率为1秒一个令牌,那么发放的方式就是
如果发放的时间是第三秒,而在第二秒有一个请求到来,那么他会预定第三秒的令牌,如果同时有第二个请求到来,那么他会被推迟到第四秒获取令牌,且将那个令牌预定下
最后一个空闲的令牌的出现时间被推迟到了第五秒
只要利用一个动态的计算下一个令牌产生的事件,就能完成这个限流的功能,接下来就是这个算法的简单实现
class SimpleLimiter {
//下一令牌产生时间
long next = System.nanoTime();
//发放令牌间隔:纳秒
long interval = 1000_000_000;
//预占令牌,返回能够获取令牌的时间
synchronized long reserve(long now) {
//请求时间在下一令牌产生时间之后
//重新计算下一令牌产生时间
if (now > next) {
//将下一令牌产生时间重置为当前时间
next = now;
}
//能够获取令牌的时间
long at = next;
//设置下一令牌产生时间
next += interval;
//返回线程需要等待的时间
return Math.max(at, 0L);
}
//申请令牌
void acquire() {
//申请令牌时的时间
long now = System.nanoTime();
//预占令牌
long at = reserve(now);
long waitTime = max(at – now, 0);
//按照条件等待
if (waitTime > 0) {
try {
TimeUnit.NANOSECONDS.sleep(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
再往复杂了说,如果桶的容量大于1,那么如何处理,很简单,只是增加一个计算桶中令牌数量的方法,在请求令牌的时候,进行一个查询数量,如果可以直接获取到,那么就无须增加一个interval了
class SimpleLimiter {
//当前令牌桶中的令牌数量
long storedPermits = 0;
//令牌桶的容量
long maxPermits = 3;
//下一令牌产生时间
long next = System.nanoTime();
//发放令牌间隔:纳秒
long interval = 1000_000_000;
//请求时间在下一令牌产生时间之后,则
// 1.重新计算令牌桶中的令牌数
// 2.将下一个令牌发放时间重置为当前时间
void resync(long now) {
if (now > next) {
//新产生的令牌数
long newPermits = (now – next) / interval;
//新令牌增加到令牌桶
storedPermits = min(maxPermits, storedPermits + newPermits);
//将下一个令牌发放时间重置为当前时间
next = now;
}
}
//预占令牌,返回能够获取令牌的时间
synchronized long reserve(long now) {
resync(now);
//能够获取令牌的时间
long at = next;
//令牌桶中能提供的令牌
long fb = min(1, storedPermits);
//令牌净需求:首先减掉令牌桶中的令牌
long nr = 1 – fb;
//重新计算下一令牌产生时间
next = next + nr * interval;
//重新计算令牌桶中的令牌
this.storedPermits -= fb;
return at;
}
//申请令牌
void acquire() {
//申请令牌时的时间
long now = System.nanoTime();
//预占令牌
long at = reserve(now);
long waitTime = max(at – now, 0);
//按照条件等待
if (waitTime > 0) {
try {
TimeUnit.NANOSECONDS
.sleep(waitTime);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
经典的限流算法,可以分为令牌桶算法和漏桶算法,这就是一个硬币的正反面,令牌桶算法要求必须拿到令牌才能通过限流器,而漏桶算法这是将请求归为一个桶中,桶中按照一定的速率来处理请求,只有桶中有空余,才能通过限流器