计时器限流
计数器是一种比较简单的限流算法,用途比较广泛,在接口层面,很多地方使用这种方式限流。在一段时间内,进行计数,与阈值进行比较,到了时间临界点,将计数器清0 。
优点:实现简单
缺点:存在临界问题,如在12:01:00到12:01:58这段时间内没有用户请求,然后在12:01:59这一瞬时发出100个请求,OK,然后在12:02:00这一瞬时又发出了100个请求,那这某个1秒区间内,超过了每秒100次的请求阈值。
代码实现
public class CounterLimiter {
private static long timeStamp = System.currentTimeMillis();
// 1秒内允许请求数
private static long limitCount = 100;
// 请求间隔数
private static long interval = 1000;
// 请求限流数
private static AtomicLong reqCount = new AtomicLong(0);
public static boolean grant() {
long now = System.currentTimeMillis();
if (now < timeStamp + interval) {
if (reqCount.get() < limitCount) {
reqCount.incrementAndGet();
return true;
}
return false;
} else {
timeStamp = System.currentTimeMillis();
reqCount.set(0);
return false;
}
}
}
滑动窗口限流
将时间划分为多个区间,在每个区间内每有一次请求就将计数器加一维持一个时间窗口,占据多个区间,每经过一个区间的时间,则抛弃最老的一个区间,并纳入最新的一个区间,如果当前窗口内区间的请求总和超过了限制数量,则本窗口内所有的请求都被丢弃。
优点:滑动窗口计数器是通过将窗口再细分,并且按照时间“滑动”,这种算法避免了固定窗口计数器带来的双倍突发请求。
缺点:时间区间的精度越高,算法所需的空间容量就越大。
代码实现
public class SlidingWindowLimiter {
/**
* 循环队列,就是装多个窗口用,该数量是windowSize的2倍
*/
private AtomicInteger[] timeSlices;
/**
* 队列的总长度
*/
private int timeSliceSize;
/**
* 每个时间片的时长,以毫秒为单位
*/
private int timeMillisPerSlice;
/**
* 共有多少个时间片(即窗口长度)
*/
private int windowSize;
/**
* 在一个完整窗口期内允许通过的最大阈值
*/
private int threshold;
/**
* 该滑窗的起始创建时间,也就是第一个数据
*/
private long beginTimestamp;
/**
* 最后一个数据的时间戳
*/
private long lastAddTimestamp;
public SlidingWindowLimiter(int timeMillisPerSlice, int windowSize, int threshold) {
this.timeMillisPerSlice = timeMillisPerSlice;
this.windowSize = windowSize;
this.threshold = threshold;
this.timeSliceSize = windowSize * 2;
reset();
}
/**
* 初始化队列
*/
private void reset() {
beginTimestamp = System.currentTimeMillis();
if (timeSlices != null) {
return;
}
AtomicInteger[] localTimeSlices = new AtomicInteger[timeSliceSize];
for (int i = 0; i < timeSliceSize; i++) {
localTimeSlices[i] = new AtomicInteger(0);
}
timeSlices = localTimeSlices;
}
/**
* 添加元素,模拟操作
* @param count
* @return
*/
public boolean addCount(int count) {
int index = locationIndex();
System.out.println("index:" + index);
clearFromIndex(index);
int sum = 0;
// 在当前时间片里继续+1
sum += timeSlices[index].addAndGet(count);
// 加上前面几个时间片里的数量
for (int i= 1; i < windowSize; i++) {
sum += timeSlices[(index - i + timeSliceSize) % timeSliceSize].get();
}
System.out.println(sum + "---" + threshold);
lastAddTimestamp = System.currentTimeMillis();
return sum >= threshold;
}
/**
* 计算当前所在的时间片的位置
* @return
*/
private int locationIndex() {
long now = System.currentTimeMillis();
if ((now - lastAddTimestamp) > timeMillisPerSlice * windowSize) {
reset();
}
return (int) (((now - beginTimestamp) / timeMillisPerSlice) % timeSliceSize);
}
/**
* 然后清空自己前面windowSize到2*windowSize之间的数据格的数据
* 譬如1秒分4个窗口,那么数组共计8个窗口
* 当前index为5时,就清空6、7、8、1。然后把2、3、4、5的加起来就是该窗口内的总和
* @param index
*/
private void clearFromIndex(int index) {
for (int i = 1; i <= windowSize; i++) {
int j = index + i;
if (j >= windowSize * 2) {
j -= windowSize * 2;
}
timeSlices[j].set(0);
}
}
}
漏桶限流
将每个请求视作“水滴”放入“漏桶”进行存储,“漏桶”以固定速率向外“漏”出请求来执行如果“漏桶”空了则停止“漏水”,如果“漏桶”满了则多余的“水滴”会被直接丢弃。
优点:漏桶算法多使用队列实现,服务的请求会存到队列中,服务的提供方则按照固定的速率从过往队列中取出请求并执行,过多的请求则放在队列中排队或直接拒绝。
缺点:漏桶算法的缺陷也很明显,当短时间内有大量的突发请求时,即便此时服务器没有任何负载,每个请求也都得在队列中等待一段时间才被响应。
代码实现
public class LeakyBucketLimiter {
// 时间刻度
private static long timeStamp = System.currentTimeMillis();
// 桶大小
private static int bucketSize = 10;
// 每ms流出的请求
private static int rate = 1;
// 当前的水量
private static long count = 0;
public static boolean grant() {
long now = System.currentTimeMillis();
// 计算出水量
long out = (now - timeStamp) * rate;
// 先执行漏水,计算剩余水量
count = Math.max(0, count - out);
timeStamp = now;
if ((count + 1) < bucketSize) {
// 桶未满,可以执行
count ++;
return true;
}
// 水满拒绝执行
return false;
}
}
令牌桶算法
令牌以固定速率生成,生成的令牌放入令牌桶中存放,如果令牌桶满了则多余的令牌会直接丢弃,当请求到达时,会尝试从令牌桶中取令牌,取到了令牌的请求可以执行,如果桶空了,那么尝试取令牌的请求会被直接丢弃
优点:令牌桶算法既能够将所有的请求平均分布到时间区间内,又能接受服务器能够承受范围内的突发请求,因此是目前使用较为广泛的一种限流算法
缺点:牺牲小部分流量
代码实现
public class TokenBucketLimiter {
public static long timeStamp = System.currentTimeMillis();
// 桶的容量
public static int bucketSize = 10;
// 令牌放入速度
public static int rate = 3;
// 当前令牌数量
public static long tokens = 0;
public static boolean grant() {
long now = System.currentTimeMillis();
// 计算需要放桶中数量
long in = (now - timeStamp) * rate;
tokens = Math.min(bucketSize, tokens + in);
// 判断桶中是否还有令牌
if (tokens > 1) {
tokens--;
return true;
}
return false;
}
}