【数据结构和算法】令牌桶和漏桶的算法区别和实现
转载:https://blog.csdn.net/m0_37477061/article/details/95313062
一、令牌桶和漏桶算法区别
漏桶算法与令牌桶算法在表面看起来类似,很容易将两者混淆。但事实上,这两者具有截然不同的特性,且为不同的目的而使用。
需要说明的是:在某些情况下,漏桶算法不能够有效地使用网络资源。因为漏桶的漏出速率是固定的,所以即使网络中没有发生拥塞,漏桶算法也不能使某一个单独的数据流达到端口速率。因此,漏桶算法对于存在突发特性的流量来说缺乏效率。而令牌桶算法则能够满足这些具有突发特性的流量。通常,漏桶算法与令牌桶算法结合起来为网络流量提供更高效的控制。
1、漏桶算法的示意
2、令牌桶的算法示意
========令牌桶的基本原理===============
【令牌的生成】:令牌以恒定的速率生成,放入令牌桶中,待令牌使用
【令牌的消费】:请求通过令牌桶,要先获取令牌,获取则请求通过,未获取的请求拒绝或阻塞到有令牌生成为至
========令牌生成的原理=================
令牌时间片( slot )= 1S / QPS
令牌生成斜率(rate) = QPS / 1s
当前桶中的令牌数 = Min ( max_token_num , last_token_num + (current_time - last_access_time ) * rate )
当前桶中的令牌数 = Min ( max_token_num , last_token_num + (current_time - last_access_time ) / slot )
令牌桶算法具有两个很重要的特性:流量整形和方便处理突发流量。
流量整形是指令牌桶算法通过阻塞、拒绝等手段使请求以稳定的速度通过限流器,原本不规则的流量在经过限流器后变得平滑且均匀。流量整形效果非常有利于服务端稳定运行,类似我们在高并发系统中常用的基于消息队列实现的“削峰填谷”手段,经过整形后,服务端能够以稳定的状态接收并处理请求。
突发流量是指随机出现的、短时间的流量突刺。如果严格遵循流量整形的限制,那么服务端在遇到突发流量时会突然拒绝一大波请求,在客户端有重试机制的情况下还可能导致情况进一步恶化。因此,在服务端资源充足的条件下,限流器应该具有一些“弹性”,允许服务端临时超频处理一些突发请求。
在令牌桶算法模型中,“弹性”处理突发流量是非常容易实现的,只需要给桶中生成的令牌设置一个有效期即可。有突发流量时,限流器可以使用有效期内的剩余令牌来通过更多请求,从而临时提高服务端处理效率,避免大量请求被拒绝。
二、常用的限流算法
常用的限流算法有两种:漏桶算法和令牌桶算法。
漏桶算法思路很简单,水(请求)先进入到漏桶里,漏桶以一定的速度出水,当水流入速度过大会直接溢出,可以看出漏桶算法能强行限制数据的传输速率。
图1 漏桶算法示意图
对于很多应用场景来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发传输。这时候漏桶算法可能就不合适了,令牌桶算法更为适合。如图2所示,令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。
图2 令牌桶算法示意图
并不能说明令牌桶一定比漏洞好,她们使用场景不一样。
- 令牌桶:可以用来保护自己,主要用来对调用者频率进行限流,为的是让自己不被打垮。所以如果自己本身有处理能力的时候,如果流量突发(实际消费能力强于配置的流量限制),那么实际处理速率可以超过配置的限制
- 漏桶算法:用来保护他人,也就是保护他所调用的系统。主要场景是,当调用的第三方系统本身没有保护机制,或者有流量限制的时候,我们的调用速度不能超过他的限制,由于我们不能更改第三方系统,所以只有在主调方控制。这个时候,即使流量突发,也必须舍弃。因为消费能力是第三方决定的。
- 总结起来:如果要让自己的系统不被打垮,用令牌桶。如果保证被别人的系统不被打垮,用漏桶算法。
二、算法实现
1、漏桶算法实现
/** * 漏桶限流 **/ public class LeakyBucketLimit { //桶的最大水量 private Double capacity; //当前桶中的水量 private Double currentWater; //当前时间 private Double nextLeakyWaterTime; //流水速率 private Double rate; //每滴水的时间片 private Double waterSlot; private Object lock = new Object(); public LeakyBucketLimit(Integer maxBucket) { if (maxBucket == null || maxBucket <= 0) { throw new IllegalArgumentException("参数异常"); } this.capacity = maxBucket.doubleValue(); this.currentWater = 0D; this.nextLeakyWaterTime = Double.valueOf(System.currentTimeMillis()); this.rate = this.capacity / 1000; this.waterSlot = 1000/this.capacity; } public boolean acquireLimit() { long time=0; synchronized (lock) { Long currentTime = System.currentTimeMillis(); //放水 leakyWater(currentTime); //注入水 Double swapWater = this.currentWater + 1; if (swapWater > this.capacity) { return false; } else { this.currentWater = swapWater; //更新一下下次放水时间 this.nextLeakyWaterTime += waterSlot; //这个是计算当前这次请求需要睡眠的时长,保障漏水的速度是均衡的 time = Double.valueOf(this.nextLeakyWaterTime - currentTime).longValue(); } } if(time>0){ //延迟到指定的时间进行漏水 try { Thread.sleep(time); } catch (InterruptedException e) { e.printStackTrace(); } } return true; } /** * 放水 * @param currentTime */ private void leakyWater(Long currentTime) { if (currentTime > nextLeakyWaterTime) { Double leakyWater = (currentTime - nextLeakyWaterTime) * rate; this.currentWater = Math.max(0D, this.currentWater - leakyWater); this.nextLeakyWaterTime = currentWater; } } }
(1)未满加水:通过代码 water +=1进行不停加水的动作。
(2)漏水:通过时间差来计算漏水量。
(3)剩余水量:总水量-漏水量。
(4)下次漏水时间= 下次漏水时间 + 获取入桶的水滴时间片 或者 = 当前时间(当前时间>下次漏水时间)
2、令牌桶算法实现
可以参考guava实现的令牌桶限流器:https://ifeve.com/guava-ratelimiter/
/** * 令牌桶限流 **/ public class TokenBucketLimit { //令牌桶最大令牌 private Double maxToken; //当前令牌桶的令牌数 private Double currentToken; //下一次产生令牌的时间 private Long nextFireTokenTime; //每个令牌的时间片 private Double tokenSlot; //并发锁 private Object lock = new Object(); /** * 初始化令牌桶 * @param maxToken */ public TokenBucketLimit(Integer maxToken){ if(maxToken == null || maxToken<=0){ throw new IllegalArgumentException("参数异常"); } this.maxToken= maxToken.doubleValue(); this.tokenSlot = Double.valueOf(1000 / maxToken); this.currentToken = 0D; this.nextFireTokenTime = System.currentTimeMillis(); } /** * 令牌桶 * 【令牌桶属性】 * 1、最大令牌数 * 2、每个令牌时间片 * 3、下次发放令牌时间 * 4、当前令牌数 * <p> * 【公式】 * 1、每个令牌时间片= 1000 ms / 最大令牌数 * 2、刷新令牌公式:当前令牌 = min( max令牌数 , (当前时间-下次发放时间)/每个令牌时间片 + 当前令牌 ) * 3、扣减令牌公式: * * @return */ public boolean acquireLimit() { //并发锁控制 synchronized (lock){ Long time = System.currentTimeMillis(); //发放令牌 fireBucketForToken(time); //消耗令牌 return consumerBucketToken(time,1); } } /** * 基于时间控制进行令牌发放 * @param currentTime */ private void fireBucketForToken(Long currentTime) { if (currentTime > nextFireTokenTime) { Double addToken = (currentTime - nextFireTokenTime) / tokenSlot; //当前最新令牌 this.currentToken = Math.min(this.maxToken, addToken + this.currentToken); //更新下次发放令牌的时间 this.nextFireTokenTime = currentTime; } } /** * 消费令牌 * * @param currentTime 当前时间 * @param consumerToken 消费令牌数 * @return */ private boolean consumerBucketToken(Long currentTime, int consumerToken) { //找到交换令牌数 Double swapToken = Math.min(this.currentToken,consumerToken); //计算需要超发的令牌数 Double waitFireToken = consumerToken - swapToken; //判定是否在超发令牌范围内: 下次发放令牌时间 + 超发令牌数* 令牌时间片 - 当前时间 > 1s double waiteTime = this.nextFireTokenTime + waitFireToken*tokenSlot - currentTime; if( waiteTime > 1000){ //不允许超发令牌,没有获得限流 return false; }else{ //更新当前令牌数 if(waitFireToken > 0){ //超发令牌,则当前令牌数=0 this.currentToken = 0D; }else{ //未超发令牌,则进行正常的令牌扣减 this.currentToken -= consumerToken; } //更新下一次令牌发放时间 = 当前下一次令牌发送时间 + 超发令牌耗时 this.nextFireTokenTime = this.nextFireTokenTime + Double.valueOf(waitFireToken * tokenSlot).longValue(); return true; } } }