當系統面臨高併發、大流量的請求時,爲保障服務的穩定運行,可採取限流算法。限流,顧名思義就是當請求超過一定數量時,就限制新的流量對系統的訪問。目前限流算法主要有計數器法、漏桶算法和令牌桶算法。
最簡單的計數器限流算法只需要一個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;
}
}
}
}