限流算法
漏桶算法
漏桶算法的思路很简单,请求先进入到漏桶里,漏桶以固定速度出水,也就是处理请求当水加的过快,则会直接溢出,也就是拒绝请求,可以看出漏桶算法执行强行限制数据的传输速度
但是对于很多常见来说,除了要求能够限制数据的平均传输速率外,还要求允许某种程度的突发.这时候漏桶算法可能就不合适了,令牌桶算法更合适
令牌桶算法
令牌桶算法的原理是系统会以一个恒定的速度往桶里放入令牌,如果请求需要被处理,则需要从桶里获取出一个令牌,当桶里没有令牌时,则拒绝服务
代码
public class TaskRunnable implements Runnable {
private Integer reqCount;//已使用令牌数量
public TaskRunnable(int reqCount) {
this.reqCount = reqCount;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "已经执行...,目前已使用令牌数:" + reqCount);
}
}
代码
public class TokenTask {
//关键:时间控制周期+令牌桶容量
static volatile long timeStamp = getNowTime();//令牌桶计时开始
static AtomicInteger reqCount = new AtomicInteger(0);//统计调用数
static final int maxReqCount = 5;//时间周期内最大请求数
static final long effectiveDuration = 1000;//时间控制周期(秒)
/**
* 限流逻辑
* @return
*/
public static boolean passControl() {
long now = getNowTime();//程序执行时间
if (now < (timeStamp + effectiveDuration)) {//在时间控制范围内
reqCount.getAndIncrement();
return reqCount.get() <= maxReqCount;//当前时间范围内超过最大请求控制数
} else {
timeStamp = now;//超时后重置
reqCount = new AtomicInteger(1);//占用一个令牌
return true;
}
}
//获取执行时间
public static long getNowTime() {
return System.currentTimeMillis();
}
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < 10; i++) {
//Thread.sleep(200); //用来模拟令牌周期
if (passControl()) {//放行
executor.execute(new TaskRunnable(reqCount.get()));
} else {
System.out.println("被限流,请稍后访问!");
}
}
}
}
限流工具
用法
google开源工具包guava提供了限流工具类,该类基于令牌桶算法,非常方便使用.rateLimiter经常用于限制对一些物理资源或者逻辑资源的的访问速率.它支持俩种获取Permits接口,一种是拿不到令牌立刻返回,一种是阻塞一段时间看看能不能拿到
//多任务执行,但每秒执行不超过2个任务
final RateLimiter rateLimiter = RateLimiter.create(2.0);
void submitTasks(List<Runnable> tasks, Executor executor) {
for (Runnable task : tasks) {
rateLimiter.acquire(); //默认获取1个令牌
executor.execute(task);
}
}
//以每秒5kb内的速度发送-限制数据包大小
final RateLimiter rateLimiter = RateLimiter.create(5000.0);
void submitPacket(byte[] packet) {
rateLimiter.acquire(packet.length);//获取指定数量的令牌
networkService.send(packet);
}
//以非阻塞的形式达到降级
if(limiter.tryAcquire()) { //未请求到limiter则立即返回false
doSomething();
}else{
doSomethingElse();
}
主要接口
RateLimiter其实是一个abstract类,但是它提供了几个static方法用于创建RateLimiter:
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 当请求到来的速度超过了permitsPerSecond,保证每秒只处理permitsPerSecond个请求
* 当这个RateLimiter使用不足(即请求到来速度小于permitsPerSecond),会囤积最多permitsPerSecond个请求
*/
public static RateLimiter create(double permitsPerSecond);
/**
* 创建一个稳定输出令牌的RateLimiter,保证了平均每秒不超过permitsPerSecond个请求
* 还包含一个热身期(warmup period),热身期内,RateLimiter会平滑的将其释放令牌的速率加大,直到起达到最大速率
* 同样,如果RateLimiter在热身期没有足够的请求(unused),则起速率会逐渐降低到冷却状态
*
* 设计这个的意图是为了满足那种资源提供方需要热身时间,而不是每次访问都能提供稳定速率的服务的情况(比如带缓存服务,需要定期刷新缓存的)
* 参数warmupPeriod和unit决定了其从冷却状态到达最大速率的时间
*/
public static RateLimiter create(double permitsPerSecond, long warmupPeriod, TimeUnit unit);
提供了两个获取令牌的方法,不带参数表示获取一个令牌。如果没有令牌则一直等待,返回等待的时间(单位为秒),没有被限流则直接返回0.0
public double acquire();//默认获取一个令牌
public double acquire(int permits);//获取指定令牌数
尝试获取令牌分为超市和不带超时俩种
//尝试获取一个令牌,立即返回
public boolean tryAcquire();
public boolean tryAcquire(int permits);
//尝试获取permits个令牌,带超时时间
public boolean tryAcquire(long timeout, TimeUnit unit);
public boolean tryAcquire(int permits, long timeout, TimeUnit unit);
使用
平滑突发限流
使用 RateLimiter
的静态方法创建一个限流器,设置每秒放置的令牌数为5个。返回的RateLimiter对象可以保证1秒内不会给超过5个令牌,并且以固定速率进行放置,达到平滑输出的效果。
public void testSmoothBursty() {
RateLimiter r = RateLimiter.create(5);
while (true) {
System.out.println("get 1 tokens: " + r.acquire() + "s");
}
/**
* output: 基本上都是0.2s执行一次,符合一秒发放5个令牌的设定。
* get 1 tokens: 0.0s
* get 1 tokens: 0.182014s
* get 1 tokens: 0.188464s
* get 1 tokens: 0.198072s
* get 1 tokens: 0.196048s
* get 1 tokens: 0.197538s
* get 1 tokens: 0.196049s
*/
}
RateLimiter
使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
public void testSmoothBursty2() {
RateLimiter r = RateLimiter.create(2);
while (true)
{
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
try {
Thread.sleep(2000);
} catch (Exception e) {}
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* end
* get 1 tokens: 0.499796s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
*/
}
}
RateLimiter
由于会累积令牌,所以可以应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。 RateLimiter
在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。
public void testSmoothBursty3() {
RateLimiter r = RateLimiter.create(5);
while (true)
{
System.out.println("get 5 tokens: " + r.acquire(5) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 5 tokens: 0.0s
* get 1 tokens: 0.996766s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194007s
* get 1 tokens: 0.196267s
* end
* get 5 tokens: 0.195756s
* get 1 tokens: 0.995625s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194603s
* get 1 tokens: 0.196866s
*/
}
}
RateLimiter
使用令牌桶算法,会进行令牌的累积,如果获取令牌的频率比较低,则不会导致等待,直接获取令牌。
public void testSmoothBursty2() {
RateLimiter r = RateLimiter.create(2);
while (true)
{
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
try {
Thread.sleep(2000);
} catch (Exception e) {}
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* end
* get 1 tokens: 0.499796s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
* get 1 tokens: 0.0s
*/
}
}
RateLimiter
由于会累积令牌,所以可以应对突发流量。在下面代码中,有一个请求会直接请求5个令牌,但是由于此时令牌桶中有累积的令牌,足以快速响应。 RateLimiter
在没有足够令牌发放时,采用滞后处理的方式,也就是前一个请求获取令牌所需等待的时间由下一次请求来承受,也就是代替前一个请求进行等待。
public void testSmoothBursty3() {
RateLimiter r = RateLimiter.create(5);
while (true)
{
System.out.println("get 5 tokens: " + r.acquire(5) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 5 tokens: 0.0s
* get 1 tokens: 0.996766s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194007s
* get 1 tokens: 0.196267s
* end
* get 5 tokens: 0.195756s
* get 1 tokens: 0.995625s 滞后效应,需要替前一个请求进行等待
* get 1 tokens: 0.194603s
* get 1 tokens: 0.196866s
*/
}
}
平滑预热流
RateLimiter
的 SmoothWarmingUp
是带有预热期的平滑限流,它启动后会有一段预热期,逐步将分发频率提升到配置的速率。 比如下面代码中的例子,创建一个平均分发令牌速率为2,预热期为3分钟。由于设置了预热时间是3秒,令牌桶一开始并不会0.5秒发一个令牌,而是形成一个平滑线性下降的坡度,频率越来越高,在3秒钟之内达到原本设置的频率,以后就以固定的频率输出。这种功能适合系统刚启动需要一点时间来“热身”的场景。
public void testSmoothwarmingUp() {
RateLimiter r = RateLimiter.create(2, 3, TimeUnit.SECONDS);
while (true)
{
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("get 1 tokens: " + r.acquire(1) + "s");
System.out.println("end");
/**
* output:
* get 1 tokens: 0.0s
* get 1 tokens: 1.329289s
* get 1 tokens: 0.994375s
* get 1 tokens: 0.662888s 上边三次获取的时间相加正好为3秒
* end
* get 1 tokens: 0.49764s 正常速率0.5秒一个令牌
* get 1 tokens: 0.497828s
* get 1 tokens: 0.49449s
* get 1 tokens: 0.497522s
*/
}
}
超详细的Guava RateLimiter限流原理解析 - 知乎 (zhihu.com)
流量控制算法——漏桶算法和令牌桶算法 - 知乎 (zhihu.com)
俩种算法区别
漏桶
原理
请求放入桶(消息队列等),业务处理单元(线程/进程/服务)从桶里拿请求处理,桶满则丢弃新的请求
本质:总量控制,桶的大小是关键
优缺点
桶大小调整比较困难,例如java BlockkingQueue
无法精确控制流出速度(处理速度)
突发流量时丢弃的请求较少
应用场景:瞬时高并发,例如0点签到,整点秒杀
令牌桶
某个处理单元按照指定的速率将令牌放入桶(消息队列等),业务处理单元收到请求后需要获取令牌,获取不到就丢弃请求
技术本质:速率控制,令牌产生的速度是设计的关键
优点
可以动态调整处理的速度
突发流量的时候可能丢弃很多请求
实现相对复杂
场景:控制访问第三方服务的速度,防止把下游压垮,控制自己的处理速度防止过载
常见问题
漏桶和令牌桶的区别是保护自己还是保护别人吗
很显然不是,令牌桶保护自己和保护下游都可以,而不是说保护自己用令牌桶,保护别人用漏桶
原因很简单,令牌桶就是一个速率控制,你可以用来控制自己的处理速度,也可以控制别人的处理速度,都可以起到保护作用
其实漏桶也可以既保护自己又保护下游,因为请求太多的时候,把请求先缓存到漏桶里了,漏桶放不下就丢弃新的请求,下游按照自己的速度来桶里拿任务就可以了,这也是保护机制
其实就是
漏桶的本质是总量控制,令牌桶的本质是速率控制
至于这个控制,可以保护自己,也可以用来保护别人
有的技术人员对于“保护别人”可能不太理解,这个主要应用于访问第三方,最典型的例子就是双十一的支付宝,支付的时候要访问银行的接口,银行的接口实际上处理高并发的能力并不强(例如澳门某银行对外提供的移动支付接口,峰值 TPS 是 30 次/s),这个时候支付宝自己处理性能再高都没用,而且越高越容易把下游的银行给压垮,所以支付宝不得不针对下游接口请求做限流。
网上的文章都说令牌桶更适合“突发流量”,为何你这里说漏桶更适合“瞬时(业务)高并发”?
其实,令牌桶的“突发流量”跟我们通常理解的“业务高并发”并不一样。令牌桶的算法原本是用于网络设备控制传输速度的,而且它控制的目的是保证一段时间内的平均速率控制,之所以说令牌桶适合突发流量,是指在网络传输的时候,可以允许某段时间内(一般就几秒)超过平均传输速率,这在网络环境下常见的情况就是“网络抖动”,但这个短时间的突发流量是不会导致雪崩效应,网络设备也能够处理得过来。对应到令牌桶应用到业务处理的场景,就要求即使有突发流量来了,系统自己或者下游系统要真的能够处理的过来,否则令牌桶允许突发流量进来,结果系统或者下游处理不了,那还是会被压垮。
而我说漏桶算法更适合“瞬时高并发”,是指秒杀、抢购、整点打卡签到、微博热点事件这种业务高并发场景,它不是由于“XX抖动”引起的,而是由业务场景引起的,并且持续的事件可能是几分钟甚至几十分钟,这种业务场景为了用户体验和业务尽量少受损,优先采取的不是丢弃大量请求,而是缓存请求,避免系统出现雪崩效应。因此我们会看到,漏桶和令牌桶都有保护作用,但漏桶的保护是尽量缓存请求(缓存不下才丢),令牌桶的保护主要是丢弃请求(即使系统还能处理,只要超过指定的速率就丢弃,除非此时动态提高速率)。
所以如果在秒杀、抢购、整点打卡签到、微博热点事件这些业务场景用令牌桶的话,会出现大量用户访问出错,因为请求被直接丢弃了;而用漏桶的话,处理可能只是会慢一些,用户体验会更好一些,所以我认为漏桶更适合“突发流量”。