基于循环数组实现的带滑动窗口的计数器限流算法

当系统面临高并发、大流量的请求时,为保障服务的稳定运行,可采取限流算法。限流,顾名思义就是当请求超过一定数量时,就限制新的流量对系统的访问。目前限流算法主要有计数器法、漏桶算法和令牌桶算法。

最简单的计数器限流算法只需要一个int型变量(可使用AtomicInteger变量,保证操作的原子性)count。保存一个初始的时间戳。每当有请求到来时,先判断和时间戳之间的差是否在一个统计周期内,如果在的话,就计算count是否小于阈值,如果小于则将count加1,同时返回不限流。如果count大于等于阈值,则返回限流。若超过了一个统计周期,则将时间戳更新到当前时间,同时将count置为1,并且返回不限流。

这种简单的实现存在的一个问题,就是在两个周期的临界点的位置,可能会存在请求超过阈值的情况。比如有恶意攻击的人在一个周期即将结束的时刻,发起了等于阈值的请求(假设之前的请求数为0),并且在下一个周期开始的时刻也发起等于阈值个请求。则相当于在这接近一秒的时间内系统受到了2倍阈值的冲击,有可能导致系统挂掉。

因此,可以采用滑动窗口的方式,就是将每一个周期分割为多个窗口,当一个周期结束时,只将整个周期的开始时刻移动一个窗口的位置,这样就可以防止上面那种临界点瞬间大流量的冲击。

我采用循环数组实现了一个简单的带滑动窗口的计数器限流算法。(因为时间关系,下列代码还未充分测试过,不能保证一定正确,先发出来免得自己忘了,后续再补测试)

public class CounterLimiter {
    /** 时间戳 **/
    private long timestamp;
    /** 滑动窗口数组,每个窗口统计本窗口的请求数 **/
    private long[] windows;
    /** 滑动窗口个数 **/
    private int windowCount;
    /** 窗口的size 用于计算总的流量上限 **/
    private long windowSize;
    /** 周期起始的窗口下标 **/
    private int start;
    /** 统计周期内总请求数 **/
    private long count;
    /** 流量限制 **/
    private long limit;

    public CounterLimiter(int windowCount, int windowSize, long limit) {
        this.windowCount = windowCount;
        this.windowSize = windowSize;
        this.windows = new long[windowSize];
        this.timestamp = System.currentTimeMillis();
        this.start = 0;
        this.limit = windowCount * windowCount;
    }

    public synchronized boolean tryAcquire() {
        long now = System.currentTimeMillis();
        long time = now - timestamp;
        if (time <= limit) {
            if (count < limit) {
                count++;
                int offset = start + ((int) (time / windowSize)) % windowCount;
                windows[offset]++;
                return true;
            } else {
                return false;
            }
        } else {
            long diffWindow = time / windowSize;
            timestamp = now;
            if (diffWindow < windowCount * 2) {
                int i;
                for (i = 0; i < diffWindow - windowCount; i++) {
                    int index = start + i;
                    if (index > windowCount) {
                        index %= windowCount;
                    }
                    count += ((-1) * windows[index]);
                    windows[index] = 0L;
                }
                if (i >= windowCount) {
                    i = i % windowCount;
                }
                windows[i]++;
                return true;
            } else {
                for (int i = 0; i < windows.length; i++) {
                    windows[i] = 0L;
                }
                count = 0L;
                return true;
            }
        }
    }
}
点赞