第四部分-并发编程案例分析1:限流Guava RateLimiter
1.并发案例,限流Guava RateLimiter
Guava RateLimiter 如何解决高并发的限流问题
guava中的工具类RateLimiter
2.简单实用RateLimiter
假设已有线程池,每秒只能处理两个任务,提交任务过快,可能导致系统不稳定,用到限流
创建一个2个请求/秒的限流器。每秒最多允许2个请求通过限流器,也就是1个请求500ms
伪代码
//限流器流速: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
3.限流算法:令牌桶
Guava的RateLimiter使用的是令牌桶算法,核心就是通过限流器的话,必须拿到令牌。
我们限制发放令牌的速率,就能控制流速。
令牌桶算法说明:
1.令牌以固定的速率添加到令牌桶中,假设速率是2/秒,则令牌每1/2秒会添加一个。
2.假设令牌桶容量是b,如果令牌桶已满,新的令牌会被丢弃。
3.请求能通过限流的前提是令牌桶中有令牌
令牌桶容量b是burst的简写,允许的最大突发流量。比如b=10,令牌桶中令牌已满,则限流器允许10个请求同时通过限流器,当然只是突发流量,这10个请求带走10个令牌,后续流量只能按照速率2/秒通过限流器
原理知道了?如何实现呢?
最简单的生产者,消费者模式就可以支持。生产者线程定时向阻塞队列中添加令牌,试图通过限流器的线程则作为消费者线程,只有从阻塞队列中取到令牌,才会运行通过限流器
有什么问题没?
并发量不大时,没什么问题。但高并发场景,系统压力到极限,定时器的精度会误差非常大。定时器本身会创建调度线程,对系统性能产生影响
4.Guava的Limiter是如何基于令牌桶算法实现限流器呢
核心:记录并动态计算下一令牌发放的时间
假设桶容量b=1,限流速率r=1/秒,如果桶中没有令牌,下一个令牌发送时间第三妙,第二秒有一个线程T1请求令牌,如何处理?
显然需要等待1秒,因未等1秒后就能拿到令牌。此时令牌产生时间往后顺延1秒,第三秒发的令牌被线程T1预占了
在T1预占了第三秒的令牌后,又有线程T2请求令牌
下一个令牌产生时间是第4秒,T2需要等待2秒才能获取令牌,T2预占了第4秒的令牌,令牌产生时间顺延1秒,需要第5秒再产生令牌
T1和T2都是在下一令牌产生时间之前请求令牌
如果线程T3在下一令牌产生时间(第5秒)之后,请求令牌如何呢
线程T3可以直接拿到令牌,无需等待。因为brust=1,所以第6,7秒产生的令牌就直接丢弃了。
5.guava的限流逻辑伪代码化
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();
}
}
}
}
6.总结
令牌桶:定时向令牌桶发送令牌,请求能够从令牌桶中拿到令牌,才能通过限流
漏桶算法:请求像水一样注入漏桶,漏桶会按照一定的速率自动将水滤掉,只有漏桶留能注水的时候,请求才能通过限流器