一、简介
百度百科中的定义:
令牌桶算法是网络流量(Traffic Shaping)整形(Traffic Shaping)和速率限制(Rate Limiting)中最常使用的一种算法。典型情况下,令牌桶算法用来控制发送到网络上的数据的数目,并允许突发数据的发送。
大小固定的令牌桶可自行以恒定的速率源源不断地产生令牌。如果令牌不被消耗,或者被消耗的速度小于产生的速度,令牌就会不断地增多,直到把桶填满。后面再产生的令牌就会从桶中溢出。最后桶中可以保存的最大令牌数永远不会超过桶的大小。
传送到令牌桶的数据包需要消耗令牌。不同大小的数据包,消耗的令牌数量不一样。令牌桶这种控制机制基于令牌桶中是否存在令牌来指示什么时候可以发送流量。令牌桶中的每一个令牌都代表一个字节。如果令牌桶中存在令牌,则允许发送流量;而如果令牌桶中不存在令牌,则不允许发送流量。因此,如果突发门限被合理地配置并且令牌桶中有足够的令牌,那么流量就可以以峰值速率发送。
二、限流算法:漏桶算法和令牌桶算法
- 漏桶算法(Leaky Bucket):基本原理是水(请求)先进入到漏桶里,漏桶以一定的速度出水(接口有响应速率),当水流入速度过大超出了桶的容量,会直接溢出而被丢弃(访问频率超过接口响应速率),就拒绝请求。
- 令牌桶算法:它是一个存放固定容量令牌的桶,按照固定速率往桶里添加令牌。
假设限制2r/s,则按照500毫秒的固定速率往桶中添加令牌;
桶中最多存放b个令牌,当桶满时,新添加的令牌被丢弃或拒绝;
当一个n个字节大小的数据包到达,将从桶中删除n个令牌,接着数据包被发送到网络上;
如果桶中的令牌不足n个,则不会删除令牌,且该数据包将被限流(要么丢弃,要么缓冲区等待)。
令牌桶算法的基本过程:
假如用户配置的平均发送速率为r,则每隔1/r秒一个令牌被加入到桶中;桶最多可以存发b个令牌。如果令牌到达时令牌桶已满,则这个令牌会被丢弃;
当一个n个字节的数据包到达时,就从令牌桶中删除n个令牌,并且数据包被发送到网络;
如果令牌桶中少于n个令牌,则不会删除令牌,并且认为这个数据包在流量限制之外;
算法允许最长b个字节的突发,数据包的速率被限制成常量r。对于在流量限制外的数据包可以以不同的方式处理:
1)被丢弃;
2)放在队列中当令牌桶中累积了足够多的令牌时再传输;
3)继续发送,但需要做特殊标记,网络过载时将这些特殊标记的包丢弃。
令牌桶算法与漏桶算法(Leaky Bucket)的主要区别:
1)漏桶算法能够强行限制数据的传输速率,而令牌桶算法在能够限制数据的平均传输速率外,还允许某种程度的突发传输。
2)令牌桶算法中,只要令牌桶中存在令牌,就允许突发地传输数据直到达到用户配置的上限,它适合于具有突发特性的流量。
三、Guava-RateLimiter
RateLimiter是Guava中开源的一个令牌桶算法工具类,可以轻松实现限流工作。
RateLimiter有两个实现类:SmoothBursty和SmoothWarmingUp;
两者区别:
1)都是令牌桶算法的变种实现
2)SmoothBursty加令牌的速度是恒定的,SmoothWarmingUp会有个预热期,在预热期内加令牌的速度是慢慢增加的,直到达到固定速度为止。其适用场景是,对于有的系统而言刚启动时能承受的QPS较小,需要预热一段时间后才能达到最佳状态。
测试示例:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>19.0</version>
</dependency>
示例1:创建一个令牌桶,每秒生成一个令牌,申请失败立即返回。使用CountdownLatch计数器模拟多线程并发,调用await()方法阻塞当前线程,当计数完成后,唤醒所有线程并发执行。
//每秒生成一个令牌,返回SmoothBursty实现类
RateLimiter rateLimiter = RateLimiter.create(1D);
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executor = Executors.newCachedThreadPool();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 10; i++) {
Runnable runnable = () -> {
try {
countDownLatch.await();
//尝试获取令牌桶中一个令牌,失败立即返回
if (rateLimiter.tryAcquire()) {
System.out.println(Thread.currentThread().getName() + ": " + sdf.format(new Date()) + "获取到令牌");
} else System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {e.printStackTrace();}
};
executor.submit(runnable);
}
countDownLatch.countDown();
executor.shutdown();
示例2:创建一个令牌桶,每秒生成0.1个令牌,即每10s才会有一个令牌,超时时间设置成20s,20s内获取不到令牌返回失败,20s内可以生成2个令牌,加上创建时桶里会有一个令牌,超时前最终会有3条线程拿到令牌,并且每个令牌获取时间相隔10s。使用CountdownLatch计数器模拟多线程并发:调用await()方法阻塞当前线程,当计数完成后,唤醒所有线程并发执行。
//每秒生成0.1个令牌,返回SmoothBursty实现类
RateLimiter rateLimiter = RateLimiter.create(0.1D);
CountDownLatch countDownLatch = new CountDownLatch(1);
ExecutorService executor = Executors.newCachedThreadPool();
SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
for (int i = 0; i < 10; i++) {
Runnable runnable = () -> {
try {
countDownLatch.await();
//尝试获取令牌桶中一个令牌,最多等待20秒
if (rateLimiter.tryAcquire(20, TimeUnit.SECONDS)) {
System.out.println(Thread.currentThread().getName() + ": " + sdf.format(new Date()) + "获取到令牌");
} else System.out.println(Thread.currentThread().getName());
} catch (InterruptedException e) {e.printStackTrace();}
};
executor.submit(runnable);
}
countDownLatch.countDown();
executor.shutdown();
参考文档:
https://baike.baidu.com/item/%E4%BB%A4%E7%89%8C%E6%A1%B6%E7%AE%97%E6%B3%95/6597000?fr=aladdin
https://blog.csdn.net/unclecoco/article/details/99583154