滑动时间窗算法
基本知识
限流算法最简单的实现就是使用一个计数器法。比如对于A接口来说,我要求一分钟之内访问量不能超过100,那么我们就可以这样来实现:
- 最开始的时候就设置一个count值,每当一个请求过来我就count++
- 如果count的值大于了100,并且与第一个请求的时间间隔小于1分钟,那么就表示请求数过多
- 如果该请求与第一个请求的间隔时间大于1分钟,且count的值还在限流范围内,那么就重置count。
/**
* @Description: 限流算法之计数器法,伪代码的实现
* @Author 胡尚
* @Date: 2024/7/12 8:50
*/
public class Count {
// 当前时间
private Long timeStamp = System.currentTimeMillis();
// 请求数量
private int count = 0;
// 时间窗口的最大请求数
private final int limit = 100;
// 时间窗口ms
private final long interval = 1000 * 60;
/**
* 限流校验,校验是否成功
* @return true表示允许请求,false表示校验未通过
*/
public boolean check(){
long now = System.currentTimeMillis();
// 还在一分钟 时间窗口之内
if (now <= timeStamp + interval){
count++;
return count <= limit;
} else {
timeStamp = System.currentTimeMillis();
// 超时后重置请求数
count = 1;
}
return true;
}
}
计数法的实现有一个缺点,那就是限流的时间精度不准确。比如第一个时间窗口的前半分钟只有十个请求,后半分钟有90个请求;第二个时间窗口的前半分钟有90个请求,后半分钟只有十个请求。这种情况下,限流器是不会出现限流的,但是我在在一个时间段中的请求却有180个了。
为了解决计算器法统计时间精度不够的问题,进而引入了滑动时间窗
滑动时间窗算法的基本原理是维护一个固定大小的时间窗口,窗口内的数据被认为是当前分析的有效数据。随着时间的推移,新的数据点进入窗口,而旧的数据点则被移出窗口,从而形成了一个滑动的时间窗口。
在上图中,一个红色窗格就是一个时间窗口,我们把一个时间窗口分为了6个小格子。就拿时间窗口为一分钟举例,上面每一个小格子就是10秒;每过10秒,我们的时间窗口就会往右移动一格。每一个格子都有自己的计数器count,比如当一个请求 在0:35秒的时候到达,那么0:30~0:39对应的counter就会加1。
在具体实现上,滑动时间窗算法可以通过多种数据结构来实现,例如使用环形数组、哈希表等。例如,可以使用一个环形数组来存储时间窗口内的数据点,数组的大小等于时间窗口的大小(以时间单位为单位)。每当有新的数据点进入时,旧的对应时间点的数据将被覆盖,从而实现滑动时间窗的效果。此外,还可以使用哈希表来记录每个时间点上的数据变化情况,其中键为时间点,值为该时间点的数据值或变化量。
/**
* @Description: 限流算法之滑动时间窗算法的伪代码实现
* 假设每秒的请求不能超过100,我们设置一个1s的时间窗口,时间窗口中共有10个小格子,
* 每个格子记录100ms的请求数,每100毫秒移动一次,每次移动都需要记录当前服务请求数
* 我这里就简单实现,只有一个计数器,最新的小窗口永远存储最新访问请求总数。当然也可以每个小窗口都有自己的计数器。
* @Author 胡尚
* @Date: 2024/7/12 9:23
*/
public class SlidingTimeWindow {
/**
* 服务器访问次数,可以放在redis中实现分布式系统访问
*/
private Long count = 0L;
/**
* 滑动时间窗,使用linkedList来记录滑动窗口的10个格子
*/
private LinkedList<Long> slots = new LinkedList<>();
public static void main(String[] args) throws InterruptedException {
SlidingTimeWindow timeWindow = new SlidingTimeWindow();
new Thread(new Runnable() {
@Override
public void run() {
try {
timeWindow.onCheck();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
// 模拟一直都有请求数,随机休眠几毫秒
while(true){
// TODO 限流校验
timeWindow.count++;
Thread.sleep(new Random().nextInt(15));
}
}
private void onCheck() throws InterruptedException {
while (true){
// 把当前访问总数存入时间小窗口中
slots.add(count);
// 时间窗口的剔除操作
if (slots.size() > 10){
slots.removeFirst();
}
// 最新的时间小窗口的数和最老的时间小窗口数进行比较,是否要限流
if (slots.peekLast() - slots.peekFirst() > 100 ){
// TODO 限流标识
} else{
// TODO 解除限流标识
}
Thread.sleep(100);
}
}
}
源码算法分析
我们接下来看看Sentinel它的滑动时间窗是怎么实现的。我们从StatisticSlot
类的entry()
方法开始看,因为它每次都会记录请求通过/请求拒绝相关的计数
public void entry (.