几种常用的限流方式
目前主要使用的限流方式主要是:计数器、滑动窗口、漏桶和令牌桶限流
什么时候使用限流器
为了保证系统能够正常响应部分请求,保证服务的稳定,对于超出服务承载能力的流量进行限制,通过拒绝服务的方式对系统进行保护。
计数器限流
计数器是一种最简单限流算法
实现原理
我们需要维护一个计数器,在一个时间间隔中对计数器进行判断是否超过设定上线,如果允许通过,则放行。到达临界点,将计数器清零。
JAVA实现
这里给出的是一个大概的思路,并不具备生产使用,实际上在高并发的场景下即使最简单的计数器也需要考虑很多因素
/**
* 计数器是一种最简单限流算法
* 在一段时间间隔内,对请求进行计数,与阀值进行比较判断是否需要限流,一旦到了时间临界点,将计数器清零。
* 滑动窗口限流其实就是将计数器细分成多个格子
* @author daify
*/
public class SimpleTimer {
/**
* 最大访问量
*/
private int max = 200;
/**
* 计数器
*/
private AtomicInteger atomicInteger = new AtomicInteger(0);
/**
* 开始时间
*/
private long start = System.currentTimeMillis();
/**
* 计时器间隔
*/
private int interval = 60;
/**
* 计时器是否通过
* @return
*/
public boolean pass () {
long newTime = System.currentTimeMillis();
// 有效期内
if (newTime <= (start + interval)) {
atomicInteger.incrementAndGet();
return atomicInteger.get() <= max;
} else {
// 判断是否是一个周期
start = newTime;
atomicInteger.set(0);
return true;
}
}
}
缺陷
计数器无法解决边缘时间的突发请求,比如如果我们限制了一分钟的请求频率,如果前59秒不存在请求,而最后一秒发送大量请求,在下一秒计时器重置后,再次发送大量请求,就可以能在短时间承受大量恶意请求。
滑动窗口
滑动窗口是针对计数器存在的临界点缺陷,对时间进行细化
实现原理
滑动窗口就是把固定时间片进行细分,随着时间流失抛弃较早的格子统计计数,每个格子都是一个单独的计数器。
缺陷
其实滑动窗口就是细化了计数器的时间区间,或者说计数器就是一个只有单一格子的滑动窗口。如果看的更细会发现在每个格子边缘依然存在的突发请求虽然因为时间细化后影响会小很多,但依然无法解决。
漏桶
漏桶将限流比作一个固定容量的漏桶,按照固定速率流出水滴。
实现原理
漏桶的几个特性
- 固定的容量和固定的出水(控制留出)
- 如果超过容量,则溢出(拒绝请求)
漏桶需要包含的属性
- 用来保存速率的属性
- 最大时间的限制属性,用来拒绝超过上限的请求
- 记录队列中最后需要被处理时间
处理的逻辑
- 计算最后等待时间是否小于当前时间,如果小于则表示可以处理
- 最后等待时间如果大于当前时间,则表示当前任务需要等待到指定时间再处理
JAVA实现
这里给出的是一个大概的思路,并不具备生产使用
/**
* 漏桶逻辑
* @author daify
*/
public class LeakyBucket {
/**
* 该值代表多久处理一个请求。实际上就是指处理完该请求后,要等待多久才能处理下一个请求。
*/
private int rate = 50;
/**
* 桶数量
* 该值代表我们最多允许多少个请求排队,超过该值,就直接返回,不用等待了
*/
private int maxWait = 2000;
/**
* 桶中最后一个排队请求被处理的时间last
*/
private final AtomicLong latestPassedTime = new AtomicLong(-1);
public boolean pass() {
// 当前时间
long currentTime = System.currentTimeMillis();
// 计算下一次处理
long expectedTime = rate + latestPassedTime.get();
// 花费时间小于当前时间,pass,直接将最后处理时间设置为当前时间
if (expectedTime <= currentTime) {
latestPassedTime.set(currentTime);
return true;
} else {
// 此时需要控制返回时间,获取最新时间差额
long waitTime = rate + latestPassedTime.get() - System.currentTimeMillis();
// 等待时间超过最大等待时间,丢弃
if (waitTime > maxWait) {
return false;
} else {
// 反之,可以更新最后一次通过时间了
long nowLastTime = latestPassedTime.addAndGet(rate);
try {
//在时间范围之内的话,就等待
waitTime = nowLastTime - System.currentTimeMillis();
if (waitTime > 0) {
Thread.sleep(waitTime);
}
return true;
} catch (InterruptedException e) {
}
}
}
return false;
}
}
缺陷
可以看到,漏桶对出水的速度进行了控制,虽然解决的边缘时间的请求,但是无法应对突然流量,面对短时间的大量请求,即使这些请求服务器可以承受,但依旧被缓慢处理。
令牌桶
令牌桶和漏桶相反的思维,令牌桶认为有一个固定容量的桶,我们以固定速度向桶中生产令牌。当有请求的时候去桶中获取令牌,如果拿到令牌则允许通过
实现原理
令牌桶的特性
- 令牌按照固定速度放入桶中
- 最多存指定数量令牌
当桶中令牌足够的时候面对突发流量可以分发足够的令牌,如果令牌空了,则会正常进行限流。这样即兼容了突发请求又保证了速率。
JAVA实现
/**
* 过让请求被处理前先行获取令牌,只有获取到令牌的请求才能被放行处理的一种限流方式。
* 令牌桶控制的是令牌产生的速率。即当有请求的时候,先从令牌桶中获取令牌,
* 只要能获取到令牌就能立即通过被处理,不限制请求被处理的速度,所以也就可以应对一定程度的突发流量。
* @author daify
*/
public class TokenBucket {
/**
* 桶容量
*/
private long capacity = 200;
/**
* 刷新令牌时间
*/
private double refreshInterval = 10;
/**
* 存在的令牌数量
*/
private double tokenNum = 200;
/**
* 上次更新令牌时间
*/
private long lastRefresh = System.currentTimeMillis();
/**
* 获取令牌
* @param numberTokens
* @return
*/
public synchronized boolean pass(int numberTokens) {
syncToken();
if (tokenNum < numberTokens) {
return false;
} else {
tokenNum -= numberTokens;
return true;
}
}
private void syncToken() {
long now = System.currentTimeMillis();
if (now > lastRefresh) {
long interval = now - lastRefresh;
double refill = interval / refreshInterval;
this.tokenNum = Math.min(capacity, tokenNum + refill);
this.lastRefresh = now;
}
}
}
Redis + Lua 分布式限流
在实际中环境中很多时候我们要面临多节点的服务,这个时候我们要控制集群的处理流量需要使用Redis进行限流。可以使用Redis+Lua方式。
执行原子化的Lua脚本。
实现原理
使用Redis+Lua可以实现多种限流算法。这里因为个人实际使用中没有相关实操经验,所以没有详细写实现逻辑了。
限流模式优缺点
限流器 | 优点 | 缺点 |
---|---|---|
计数器 | 固定时间段计数,实现简单,适用不太精准的场景 | 对边界没有很好处理,导致限流不能精准控制 |
滑动窗口 | 将固定时间段分块,时间比“计数器”复杂,适用于稍微精准的场景 | 不能彻底解决“计数器”存在的边界问题 |
漏桶 | 很好的控制消费频率 | 无法应对突发流量 |
令牌桶 | 可以解决“漏桶”不能灵活消费的问题,又能避免过渡消费 | |
Redis + Lua | 支持分布式限流,有效保护下游依赖的服务资源 | 依赖 Redis |