背景
在说限流方案之前我们需要知道在什么样的场景下需要限流,以前我们都是应用部署在单台机器上,但是随着流量的增大单台服务已经扛不住压力了,所以就变成了新的的集群部署,将流量压力分担到多台机器上,来提高吞吐量,但是互联网企业嘛,在一些节日或者特殊的日子里会搞一些抢购的活动,这个并发压力可能是平时的很多倍吧,按照正常的思路来说我们增加机器横向扩展吧,可是平时的话流量没有这么大,购买这些机器其实是非常浪费的,而且我们的抢购只是流量大,但是实际的有效流量是很少的,所以限流方案出来了,guava的工具类中RateLimiter就提供了这样的功能,下面来看下几种限流方案。
一、限流方案
1、计数器方式
采用AtomicInteger:
使用AomicInteger来进行统计当前正在并发执行的次数,如果超过域值就简单粗暴的直接响应给用户,说明系统繁忙,请稍后再试或其它跟业务相关的信息。
弊端:使用 AomicInteger 简单粗暴超过域值就拒绝请求,可能只是瞬时的请求量高,也会拒绝请求。
采用令牌Semaphore:
使用Semaphore信号量来控制并发执行的次数,如果超过域值信号量,则进入阻塞队列中排队等待获取信号量进行执行。如果阻塞队列中排队的请求过多超出系统处理能力,则可以在拒绝请求。
相对Atomic优点:如果是瞬时的高并发,可以使请求在阻塞队列中排队,而不是马上拒绝请求,从而达到一个流量削峰的目的。
采用ThreadPoolExecutor java线程池:
固定线程池大小,超出固定先线程池和最大的线程数,拒绝线程请求;
2、令牌桶方式
令牌桶算法是网络流量整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。先有一个木桶,系统按照固定速度,往桶里加入Token,如果桶已经满了就不再添加。当有请求到来时,会各自拿走一个Token,取到Token 才能继续进行请求处理,没有Token 就拒绝服务。
这里如果一段时间没有请求时,桶内就会积累一些Token,下次一旦有突发流量,只要Token 足够,也能一次处理,所以令牌桶算法的特点是允许突发流量。
我们看一个例子,看看令牌桶如何允许突发流量,假如令牌则按照每秒5 个的速度放入令牌桶,桶中最多存放20 个令牌,那系统可以支持两种类型的请求流量,一种是允许持续的每秒处理5 个请求,第二种是每隔4 秒,等桶中20 个令牌攒满后,就可以处理一次有20 个请求的突发情况。
令牌桶的方案设计:
使用guava提供工具库里的RateLimiter类(内部采用令牌捅算法实现)进行限流
3、漏桶算法
一个固定容量的漏桶,按照常量固定速率流出水滴;
先想象有一个木桶,新请求就像水滴一样,不断地滴进来,水滴进来的速度是不确定的,有时会快一点,有时会慢一点,同时桶底下有个洞,可以按照固定的速度把水漏走,如果水进来的速度比漏走的快,桶就会满了,桶满了水就会漫出来,对应的就是拒绝请求。
漏桶算法的主要特点是可以平滑网络上的突发流量,请求可以被整形成稳定的流量。
二、实现(令牌通方式)
1、代码
public static void main(String[] args) throws InterruptedException {
RateLimiter limiter = RateLimiter.create(5);
long start = System.currentTimeMillis();
System.out.println("start:"+start);
for (int i = 0; i <20 ; i++) {
Thread.sleep(10);
boolean getToken =limiter.tryAcquire();
if (getToken){
System.out.println("获取了令牌====="+(i+1));
}
}
long end = System.currentTimeMillis();
System.out.println("end:"+end);
System.out.println("花费时间:"+(end-start)+"毫秒");
RateLimiter limiter1 = RateLimiter.create(1);
System.out.println(limiter1.acquire(10));
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
System.out.println(limiter1.acquire());
}
2、结果
start:1575013943433
获取了令牌=====1
获取了令牌=====19
end:1575013943652
花费时间:219毫秒
0.0
9.998836
0.99797
1.000141
0.998961
0.99968
0.999161
0.999291
Disconnected from the target VM, address: '127.0.0.1:62636', transport: 'socket'
0.999234
Process finished with exit code 0