令牌桶原理:


令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,而如果请求需要被处理,则需要先从桶里获取一个令牌,当桶里没有令牌可取时,则拒绝服务。




算法描述:


  • 假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中(每秒会有r个令牌放入桶中);
  • 假设桶中最多可以存放b个令牌。如果令牌到达时令牌桶已经满了,那么这个令牌会被丢弃;
  • 当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌(不同大小的数据包,消耗的令牌数量不一样),并且数据包被发送到网络;
  • 如果令牌桶中少于n个令牌,那么不会删除令牌,并且认为这个数据包在流量限制之外(n个字节,需要n个令牌。该数据包将被缓存或丢弃);
  • 算法允许最长b个字节的突发,但从长期运行结果看,数据包的速率被限制成常量r。对于在流量限制外的数据包可以以不同的方式处理:(1)它们可以被丢弃;(2)它们可以排放在队列中以便当令牌桶中累积了足够多的令牌时再传输;(3)它们可以继续发送,但需要做特殊标记,网络过载的时候将这些特殊标记的包丢弃。

场景:


通常可应用于抢购限流防止冲垮系统;限制某接口、服务单位时间内的访问量,譬如一些第三方服务会对用户访问量进行限制;限制网速,单位时间内只允许上传下载多少字节等。 平滑过渡方案


Google开源工具包Guava提供了限流工具类RateLimiter,该类基于令牌桶算法(Token Bucket)来完成限流,非常易于使用.RateLimiter经常用于限制对一些物理资源或者逻辑资源的访问速率.它支持两种获取permits接口,一种是如果拿不到立刻返回false,一种会阻塞等待一段时间看能不能拿到.


RateLimiter和Java中的信号量(java.util.concurrent.Semaphore)类似,Semaphore通常用于限制并发量.


RateLimiter 允许某次请求拿走超出剩余令牌数的令牌,但是下一次请求将为此付出代价,一直等到令牌亏空补上,并且桶中有足够本次请求使用的令牌为止 2 。这里面就涉及到一个权衡,是让前一次请求干等到令牌够用才走掉呢,还是让它先走掉后面的请求等一等呢?Guava 的设计者选择的是后者,先把眼前的活干了,后面的事后面再说.


源码注释中的一个例子,比如我们有很多任务需要执行,但是我们不希望每秒超过两个任务执行,那么我们就可以使用RateLimiter:


final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
    for (Runnable task : tasks) {
        rateLimiter.acquire(); // may wait
        executor.execute(task);
    }
}


另外一个例子,假如我们会产生一个数据流,然后我们想以每秒5kb的速度发送出去.我们可以每获取一个令牌(permit)就发送一个byte的数据,这样我们就可以通过一个每秒5000个令牌的RateLimiter来实现:


final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
    rateLimiter.acquire(packet.length);
    networkService.send(packet);
}


另外,我们也可以使用非阻塞的形式达到降级运行的目的,即使用非阻塞的tryAcquire()方法:


if(limiter.tryAcquire()) { //未请求到limiter则立即返回false
  doSomething();
}else{
  doSomethingElse();
}



Guava RateLimiter提供了令牌桶算法实现:平滑突发限流( SmoothBursty )和平滑预热限流( SmoothWarmingUp )实现



public class  
 RateLimiterDemo {
 
  
 public static void  
 main 
 (String[] args)  
 throws  
 InterruptedException {
 
// smooth();
 
// bursty1();
 
// bursty2();
 
  
 warmingUp 
 () 
 ;
 
  
 }
 
  
 /*
 
 * 有很多个任务,但希望每秒不超过X个,可用此类
 
 * */
 
  
 public static void  
 test1 
 () {
 
  
 //1代表一秒最多多少个
 
  
 RateLimiter rateLimiter = RateLimiter. 
 create 
 ( 
 0.5 
 ) 
 ;
 
  
 List<Runnable> tasks =  
 new  
 ArrayList<Runnable>() 
 ;
 
  
 for  
 ( 
 int  
 i =  
 0 
 ;  
 i <  
 20 
 ;  
 i++) {
 
 tasks.add( 
 new  
 UserRequest(i)) 
 ;
 
  
 }
 
 ExecutorService threadPool = Executors. 
 newCachedThreadPool 
 () 
 ;
 
  
 for  
 (Runnable runnable : tasks) {
 
  
 // 请求RateLimiter, 每秒超过permits会被阻塞
 
  
 System. 
 out 
 .println( 
 "等待时间:"  
 + rateLimiter.acquire()) 
 ;
 
  
 threadPool.execute(runnable) 
 ;
 
  
 }
 
 }
 
 
 
 
  
 public static void  
 smooth 
 () {  
 //SmoothBursty 平滑限流
 
  
 RateLimiter limiter = RateLimiter. 
 create 
 ( 
 5 
 ) 
 ; 
 //表示桶容量为5且每秒新增5个令牌,即每隔200毫秒新增一个令牌
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 }
 
 
 
 
  
 //突发了10个请求,令牌桶算法也允许了这种突发(允许消费未来的令牌),但接下来的limiter.acquire(1)将等待差不多2秒桶中才能有令牌,且接下来的请求也整形为固定速率了
 
  
 public static void  
 bursty1 
 () {  
 //SmoothBursty应对突发限流,突发过来避免空等,然后再平滑
 
  
 RateLimiter limiter = RateLimiter. 
 create 
 ( 
 5 
 ) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire( 
 1 
 )) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire( 
 10 
 )) 
 ; 
 //应对突发流量,提前消耗后面令牌,后面请求要通过时间补偿
 
  
 System. 
 out 
 .println(limiter.acquire( 
 1 
 )) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire( 
 10 
 )) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire( 
 1 
 )) 
 ;
 
  
 }
 
 
 
 
  
 /*


1、创建了一个桶容量为2且每秒新增2个令牌;


2、首先调用limiter.acquire()消费一个令牌,此时令牌桶可以满足(返回值为0);


3、然后线程暂停2秒,接下来的两个limiter.acquire()都能消费到令牌,第三个limiter.acquire()也同样消费到了令牌,到第四个时就需要等待500毫秒了。


此处可以看到我们设置的桶容量为2(即允许的突发量),这是因为SmoothBursty中有一个参数:最大突发秒数(maxBurstSeconds)默认值是1s,突发量/桶容量=速率*maxBurstSeconds,所以本示例桶容量/突发量为2,


例子中前两个是消费了之前积攒的突发量,而第三个开始就是正常计算的了。令牌桶算法允许将一段时间内没有消费的令牌暂存到令牌桶中,留待未来使用,并允许未来请求的这种突发。


*/
 
  
 public static void  
 bursty2 
 ()  
 throws  
 InterruptedException {  
 //SmoothBursty应对突发限流,突发过来避免空等,然后再平滑
 
  
 RateLimiter limiter = RateLimiter. 
 create 
 ( 
 2 
 ) 
 ;
 
  
 System. 
 out 
 .println(limiter.acquire( 
 20 
 )) 
 ; 
 //0.0 第一次请求消费一个令牌
 
  
 Thread. 
 sleep 
 ( 
 10000L 
 ) 
 ; 
 //默认允许最多积攒1s的剩余令牌,即最大并发量2,积攒2个令牌
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ; 
 //0.0 第二次请求获取上面积攒令牌
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ; 
 //0.0 第三次请求获取上面积攒令牌
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ; 
 //0.0 第三次请求获取当前令牌
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ; 
 //0.5 第三次请求获取需等待500s
 
  
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 }
 
 
 
 
  
 /*
 
 *因为SmoothBursty允许一定程度的突发,会有人担心如果允许这种突发,假设突然间来了很大的流量,那么系统很可能扛不住这种突发。
 
 * 因此需要一种平滑速率的限流工具,从而系统冷启动后慢慢的趋于平均固定速率(即刚开始速率小一些,然后慢慢趋于我们设置的固定速率)。
 
 * Guava也提供了SmoothWarmingUp来实现这种需求,其可以认为是漏桶算法,但是在某些特殊场景又不太一样
 
 * */
 
  
 public static void  
 warmingUp 
 ()  
 throws  
 InterruptedException {  
 //SmoothWarmingUp
 
  
 RateLimiter limiter = RateLimiter. 
 create 
 ( 
 5 
 , 
 1000 
 ,  
 TimeUnit. 
 MILLISECONDS 
 ) 
 ;
 
  
 //RateLimiter.create(doublepermitsPerSecond, long warmupPeriod, TimeUnit unit)
 
 //permitsPerSecond表示每秒新增的令牌数,warmupPeriod表示在从冷启动速率过渡到平均速率的时间间隔
 
  
 for 
 ( 
 int  
 i = 
 1 
 ;  
 i <  
 5 
 ; 
 i++) {
 
 System. 
 out 
 .println(limiter.acquire( 
 10 
 )) 
 ;
 
  
 }
 
 Thread. 
 sleep 
 ( 
 1000L 
 ) 
 ;
 
  
 for 
 ( 
 int  
 i = 
 1 
 ;  
 i <  
 5 
 ; 
 i++) {
 
 System. 
 out 
 .println(limiter.acquire()) 
 ;
 
  
 }
 
  
 //maxPermits等于热身(warmup)期间能产生的令牌数,比如QPS=4,warmup为2秒,则maxPermits=8.halfPermits为maxPermits的一半.
 
 //速率是梯形上升速率的,也就是说冷启动时会以一个比较大的速率慢慢到平均速率;然后趋于平均速率(梯形下降到平均速率)。可以通过调节warmupPeriod参数实现一开始就是平滑固定速率。
 
  
 }
 
}
 
 
 
 
class  
 UserRequest  
 implements  
 Runnable {
 
  
 private int  
 id 
 ;
 
 
 
 
  
 public  
 UserRequest( 
 int  
 id) {
 
  
 this 
 . 
 id  
 = id 
 ;
 
  
 }
 
 
 
 
  
 public void  
 run 
 () {
 
 System. 
 out 
 .println( 
 "第"  
 + ( 
 id  
 +  
 1 
 ) +  
 "个请求" 
 ) 
 ;
 
  
 }