当系统面临高并发、大流量的请求时,为保障服务的稳定运行,可采取限流算法。限流,顾名思义就是当请求超过一定数量时,就限制新的流量对系统的访问。目前限流算法主要有计数器法、漏桶算法和令牌桶算法。
最简单的计数器限流算法只需要一个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;
}
}
}
}