关于限流算法有许多种,有简单的计数限流阀,固定窗口限流法,滑动窗口限流法,漏桶算法,令牌桶算法。今天我们就来聊聊滑动窗口版的限流算法。
说起滑动窗口之前,我们先来说说固定窗口限流。固定窗口限流,我们可以这样子来理解,我们假设规定1s可以放入3个请求,那么窗口的大小就是1s,然后在1s内对请求计数,如果超过3那么就限流,过了这一秒后计数清空。
可以用下图来理解
这边放一下我写的固定窗口的限流算法线程安全版给大家参考一下
主要是利用juc包下的原子类和cas的操作以及一些状态的判断
/**
* @Author: chencc
*/
public class FixedWindowRateLimit {
/**
* 窗口开始的时间 -1代表还没被初始化
*/
AtomicLong startTime = new AtomicLong(-1);
/**
* 计数
*/
AtomicInteger count = new AtomicInteger(0);
/**
* 最大请求数
*/
private final int MAX_REQUEST_NUM = 100;
/**
* 判断当前是否能够继续往下进行
* @return
*/
public boolean entry() {
long time = System.currentTimeMillis();
for (;;) {
// 如果第一次没有初始化
if (startTime.get() == -1) {
if (lockStartTime(-1)) {
count.set(1);
unlockStartTime(time);
return true;
}
}
// 如果是-2 证明窗口处于锁定状态 让出cpu
if (startTime.get() == -2) {
Thread.yield();
continue;
}
// 如果这个请求的时间已经小于当前这个窗口的时间 更新时间
if (time < startTime.get()) {
time = System.currentTimeMillis();
}
// 判断当前时间和窗口已经相差了一秒
long expect = startTime.get();
if (time - expect >= 1000) {
if (lockStartTime(expect)) {
count.set(1);
unlockStartTime(time);
return true;
}
}
// 如果已经超过的最大请求数
if (count.get() >= MAX_REQUEST_NUM && startTime.get() > 0) {
return false;
}
int num;
for (;(num = count.get()) < MAX_REQUEST_NUM && startTime.get() > 0 && time >= startTime.get();) {
if (count.compareAndSet(num, num + 1)) {
return true;
}
}
}
}
/**
* -2代表当前被另外一个线程再使用
* @param expect
* @return
*/
private boolean lockStartTime(long expect) {
return startTime.compareAndSet(expect, -2);
}
/**
* 解除锁定
* @param time
*/
private void unlockStartTime(long time) {
startTime.set(time);
}
}
在spingboot的环境下可以通过拦截器这样子来测试
/**
* @author chencc
* @date 2022/8/2 21:42
*/
@Configuration
@Component
public class RateLimitFilter implements Filter {
@Autowired
private FixedWindowRateLimit fixedWindowRateLimit;
@Bean
public FixedWindowRateLimit getFixedWindowRateLimit() {
return new FixedWindowRateLimit();
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
if (!fixedWindowRateLimit.entry()) {
response.getWriter().write("rate limit");
} else {
chain.doFilter(request, response);
}
}
}
通过以上的了解了固定窗口版的限流算法,是不是看上去没什么毛病?其实不然,我们想一想,如果一秒钟允许500个请求,如果这500个请求都在这1s钟的最后100ms过来那么是可以通过的,然后下一秒的前100ms又过来了500个请求,那么这个算法也是允许通过的,那么问题就来了,再这短短的200ms就已经过去了1000个请求,这也是这个算法的一个缺陷所在,面对突增的流量以及刚好又在上一秒和下一秒的切换的时候,可能没有起到很好的防护
所以下面就引出我们的滑动窗口版的限流算法
滑动窗口版的算法我们可以这么来理解
我们可以把1s 以 200ms 分割成了5个小窗口,然后每个小窗口会记录这段时间内的请求数量,如果总和已经超过了最大请求量那么就拒绝请求,然后每隔200ms 我们往右边滑动,顺便减去左边被淘汰出去的窗口的请求数,我们可以用下图来理解(1s三个请求)
每个大窗口会记载当前这1s内有几个请求了,然后每隔200ms往左滑动,然后减去最左边被淘汰的小窗口的请求数量,可以发现,如果小窗口的越小那么越平滑,如果小窗口的大小也是1s,那么就退化成固定窗口了。。如果使用滑动窗口的话,小窗口大小为200ms,那么就不会发生上面固定窗口的那种情况了
下一篇文章我们再来实现这样子的一个限流算法