在高并发情况下,经常会出现接口或服务不可用的情况,甚至会引发系统崩溃,对于该种情况需要使用限流,当请求数达到一定的并发数,就进行服务降级、拒绝、等待等。常见的限流算法是计数器限流算法、漏桶算法、令牌桶算法。
1. 计数器限流算法:主要用来限制总并发数,如数据库连接池大小、程序访问并发数、线程池大小等都是使用计数器算法。
package com.test;
public class CountRateLimiterDemo1 {
private static AtomicInteger count = new AtomicInteger(0);
public static void exec() {
if (count.get() >= 5) {
System.out.println("请求用户过多,请稍后在试!"+System.currentTimeMillis()/1000);
} else {
count.incrementAndGet();
try {
//处理核心逻辑
TimeUnit.SECONDS.sleep(1);
System.out.println("--"+System.currentTimeMillis()/1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
count.decrementAndGet();
}
}
}
}
使用AtomicInteger来统计当前正在并发执行的次数,如果超过阈值就直接响应给用户,说明系统繁忙等信息。
AtomicInteger是提供原子操作的integer类,通过线程安全的方式来操作加减。
弊端:使用AtomicInteger超过阈值就拒绝请求会存在可能只是瞬时的请求量高,也会拒绝请求。
public class CountRateLimiterDemo2 {
private static Semaphore semphore = new Semaphore(5);
public static void exec() {
if(semphore.getQueueLength()>100){
System.out.println("当前等待排队的任务数大于100,请稍候再试...");
}
try {
semphore.acquire();
// 处理核心逻辑
TimeUnit.SECONDS.sleep(1);
System.out.println("--" + System.currentTimeMillis() / 1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semphore.release();
}
}
}
使用semphore信号量来控制并发执行次数,如果超过阈值信号量,则进入阻塞队列中排队获取信号量。如果阻塞队列中排队的请求过多超出系统处理的能力,就可以拒绝请求。
相对于AtomicInteger:如果是瞬时的高并发,可使请求阻塞在队列中,而不是马上拒绝请求。
2. 漏桶算法
漏桶算法描述如下:
① 一个固定容量的漏桶,按照常量固定速率流出水滴;
② 如果桶是空的,则不需要流出水滴;
③ 水滴可以以任意速率流入漏桶;
④ 如果流入水滴超过了桶的容量,则流入的水滴溢出了(被丢弃),而漏桶容量是不变的。
3. 令牌桶
令牌桶算法是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌,算法如下:
① 令牌按照固定的速率被放入令牌桶。比如每秒100个;
② 桶中最多放a个令牌,当桶满时,新添加的令牌会被丢弃或拒绝;
③ 当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着将数据包发送到网络;
④ 如果桶中的令牌不足n个,则不会删除令牌,该数据包将被限流(要么在缓冲区等待,要么丢弃)。
4. 令牌桶与漏桶比较:
① 令牌桶按照固定的速率往桶中添加令牌,请求是否被处理需要看桶中令牌是否足够,当令牌数量减为0时则拒绝新的请求;
② 漏桶是按照常量固定速率流出,流入请求速率任意,当流入的请求数累计到漏桶容量时,则新流入的请求被拒绝;
③ 令牌桶限制平均速率流入,允许突发请求,只要有令牌就可以处理,支持一次拿多个令牌,并允许一定程度突发流量;
④ 漏桶限制的是常量流出速率,即流出速率是固定常量值,比如都是1的速率流出,而不能一次是1,下次又是2,从而平滑突发流入速率;
⑤ 令牌桶允许一定程度的突发,而漏桶主要目的是平滑流入速率;
⑥ 两个算法实现可一样,但方向相反,对于相同参数得到的限流效果一样。
guava中ratelimiter: http://ifeve.com/guava-ratelimiter/