基於循環數組實現的帶滑動窗口的計數器限流算法

當系統面臨高併發、大流量的請求時,爲保障服務的穩定運行,可採取限流算法。限流,顧名思義就是當請求超過一定數量時,就限制新的流量對系統的訪問。目前限流算法主要有計數器法、漏桶算法和令牌桶算法。

最簡單的計數器限流算法只需要一個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;
            }
        }
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章