本文共 4868 字,大约阅读时间需要 16 分钟。
在互联网高并发场景下,限流是用来保证系统稳定性的一种手段,当系统遭遇瞬时流量激增时,可能会由于系统资源耗尽导致宕机。而限流可以把一小部分流量拒绝掉,保证大部分流量可以正常访问,从而保证系统只接收承受范围以内的请求,多余的请求给拒绝掉。
举个例子,节假日很多人都会出去玩,我们知道每个地铁站单位时间内可承受的运输能力是有限的,也就是每趟车承载的人数是有上限的,当达到这个上限以后,上不去的人就只能排队等待,当排队等待的人已经到地铁站口了,此时保安就会限制再进入地铁站了。此时的保安就是起到限流的作用
常用的限流算法有:漏桶算法、令牌桶算法
漏桶算法
漏桶算法很形象,我们可以想像有一个大桶,大桶底部有一个固定大小的洞,Web请求就像水一样,先进入大桶,然后以固定的速率从底部漏出来,无论进入桶中的水多么迅猛,漏桶算法始终以固定的速度来漏水
对应到Web请求就是
令牌桶算法
令牌桶跟漏桶算法有点不一样,令牌桶算法也有一个大桶,桶中装的都是令牌,有一个固定的“人”在不停的往桶中放令牌,每个请求来的时候都要从桶中拿到令牌,要不然就无法进行请求操作
漏桶算法 VS 令牌桶算法
Guava中的限流实现RateLimiter
Guava中的限流使用的是令牌桶算法,RateLimiter提供了两种限流实现:
什么是平滑突发限流?
每秒以固定的速率输出令牌,以达到平滑输出的效果
//每秒5个令牌 RateLimiter rateLimiter = RateLimiter.create(5); while(true){ System.out.println("time:" + rateLimiter.acquire() + "s"); }
输出结果:
time:0.0stime:0.196802stime:0.186141stime:0.199164stime:0.199221stime:0.199338stime:0.199493s
平均每个0.2秒左右,很均匀
当产生令牌的速率大于取令牌的速率时,是不需要等待令牌时间的
//每秒5个令牌 RateLimiter rateLimiter = RateLimiter.create(5); while(true){ System.out.println("time:" + rateLimiter.acquire() + "s"); //线程休眠,给足够的时间产生令牌 Thread.sleep(1000); }
输出结果:
time:0.0stime:0.0stime:0.0stime:0.0stime:0.0s
由于令牌可以积累,所以我一次可以取多个令牌,只要令牌充足,可以快速响应
//每秒5个令牌 RateLimiter rateLimiter = RateLimiter.create(5); while(true){ //一次取出5个令牌也可以快速响应 System.out.println("______________________________________"); System.out.println("time:" + rateLimiter.acquire(15) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); }
输出结果:
______________________________________time:0.0stime:2.997305stime:0.193227stime:0.195574s______________________________________time:0.194769stime:2.995363stime:0.195592stime:0.195186s______________________________________time:0.196013stime:2.9972stime:0.19612stime:0.195975s______________________________________time:0.193164s
平滑预热限流?
平滑预热限流带有预热期的平滑限流,它启动后会有一段预热期,逐步将令牌产生的频率提升到配置的速率
这种方式适用于系统启动后需要一段时间来进行预热的场景
比如,我设置的是每秒5个令牌,预热期为5秒,那么它就不会是0.2左右产生一个令牌。在前5秒钟它不是一个均匀的速率,5秒后恢复均匀的速率
//每秒5个令牌,预热期为5秒 RateLimiter rateLimiter = RateLimiter.create(5,5, TimeUnit.SECONDS); while(true){ //一次取出5个令牌也可以快速响应 System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("time:" + rateLimiter.acquire(1) + "s"); System.out.println("-----------"); }
输出结果:
time:0.0stime:0.57874stime:0.539854stime:0.520102stime:0.485491s-----------time:0.455969stime:0.423133stime:0.391189stime:0.359336stime:0.327898s-----------time:0.295409stime:0.263682stime:0.231618stime:0.202781stime:0.198914s-----------time:0.198955stime:0.199052stime:0.199183stime:0.199296stime:0.199468s-----------time:0.199493stime:0.199687stime:0.198785stime:0.198893stime:0.199373s-----------
从输出结果可以看出来,前面的速率不均匀,到后面就逐渐稳定在0.2秒左右了
当然,Guava的RateLimiter只适用于单机的限流,在互联网分布式项目中可以借助其他中间件来实现限流的功能
设定业务场景:限制单个JVM进程10 qps的下单能力;RateLimiter原理
RateLimiter基于时间轴变化在每次请求时计算是否有可用Permit。总体思路:记录start、next两个时间点,每次申请Permit时,若current-next>stable interval可以申请Permit。
注:图中一个刻度表示100ms
名字解释:
start:计时器启动时间
next:下次可生成Permit时间(current>next时,默认为上次申请Permit时间)
current:当前时间
stable interval:生成一个Permit时间间隔
步骤:
1、记录启动时间,记start=System.currentTimeMillis()
2、计算生成一个Permit的时间间隔,继上面例子1秒10qps,1000/10=100,即100ms生成一个Permit,记stable interval 为100ms
3、request1:申请一个Permit,next=0,current=start+1
a. current>next设置next=current
b. 计算request1等待时间,next<=current,request1不需要等待
c. 计算可用Permit,(current-next)/statble interval=0,没有可用Permit。怎么办?向未来借用Permit,next需要往后推迟一个stable interval。即:next=next+stable interval=start+2
request2:申请一个Permit,next=start+2,current=start+1.2
a. current<next,什么也不做
b. 计算request2等待时间,sleep time = next-current=0.8,request2需要等待sleep time(还request1借的时间)
c. 计算可用Permit,current<next,说明时间上次请求借用的额度还没有还完,所以本次请求只能再次向未来借用Permit,next需要往后推迟一个stable interval。即:next=next+stable interval=start+3
request3:申请一个Permit,next=start+3,current=start+8
a. current>next设置next=current
b. 计算request3等待时间,next<=current,request3不需要等待
c. 计算可用Permit,(current-next)/statble interval=5,所以本次请求不需要借用额度,注意额度最多只能存储用户设置的qps数量,这个例子是10
参考:
https://yq.aliyun.com/articles/696317转载地址:http://efdoi.baihongyu.com/