固定窗口限流&滑动窗口限流Java实现
我们知道常用的限流算法有固定窗口、滑动窗口、漏通、令牌桶算法,这里给出前面两种的算法Java实现
固定窗口
固定窗口算法
package org.example.限流算法;
import org.example.utils.Util;
import java.util.concurrent.TimeUnit;
public class FixedWindow {
private final int windowSize; // 窗口大小(毫秒)
private int count; // 当前窗口内的计数
private final int threshold; // 限流阈值
private long lastWindowStart; // 上一个窗口的开始时间
public FixedWindow(int windowSize, int threshold) {
this.windowSize = windowSize;
this.threshold = threshold;
this.lastWindowStart = System.currentTimeMillis();
}
public synchronized boolean addRequest() {
tryRestWindow();
if (count >= threshold) {
return false;
}
count++;
return true;
}
public int getCount() {
return count;
}
/**
* 尝试重置窗口
*/
public void tryRestWindow() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastWindowStart >= windowSize) {
count = 0;
lastWindowStart = currentTime;
}
}
public static void main(String[] args) {
FixedWindow fixedWindow = new FixedWindow(1000, 20); // 1秒窗口,阈值20次请求
for (int i = 0; i < 5; i++) {
new Thread(() -> {
long end = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(3);
// 模拟请求
int j = 0;
while (System.currentTimeMillis() < end) {
if (fixedWindow.addRequest()) {
System.out.println("currentTime:" + Util.getNowStr() + " thread" + Thread.currentThread().getName() + " 第" + j + "次请求通过 当前窗口请求数:" + fixedWindow.getCount());
} else {
System.err.println("currentTime:" + Util.getNowStr() + " thread" + Thread.currentThread().getName() + " 第" + j + "次请求被限流了 当前窗口请求数" + fixedWindow.getCount());
}
j++;
Util.sleep(Util.random(150, 250));
}
}).start();
}
}
}
滑动窗口
package org.example.限流算法;
import org.example.utils.Util;
import java.util.concurrent.TimeUnit;
public class SlidingWindow {
private final int windowSize; // 窗口大小(毫秒)
private final int granularity; // 粒度(毫秒)
private final int threshold; // 限流阈值
private final int[] buckets; // 每个桶存储的请求数
private int currentBucket; // 当前桶的索引
private long lastBucketTime; // 处于末尾的通的开始时间
public SlidingWindow(int windowSize, int granularity, int threshold) {
this.windowSize = windowSize;
this.granularity = granularity;
this.threshold = threshold;
this.buckets = new int[windowSize / granularity];
this.currentBucket = 0;
this.lastBucketTime = System.currentTimeMillis();
}
public synchronized boolean addRequest() {
trySliderWindow();
if (getCount() >= threshold) {
return false;
}
buckets[currentBucket]++;
return true;
}
public synchronized int getCount() {
int total = 0;
for (int count : buckets) {
total += count;
}
return total;
}
/**
* 尝试滑动滑动窗口
*/
private void trySliderWindow() {
long currentTime = System.currentTimeMillis();
long elapsed = currentTime - lastBucketTime;
if (elapsed >= granularity) {
int bucketsToAdvance = (int) (elapsed / granularity);
for (int i = 0; i < bucketsToAdvance && i < buckets.length; i++) {
currentBucket = (currentBucket + 1) % buckets.length;
buckets[currentBucket] = 0;
}
lastBucketTime = currentTime - (elapsed % granularity);
}
}
public static void main(String[] args) {
SlidingWindow slidingWindow = new SlidingWindow(1000, 100, 20); // 1秒窗口,100ms秒粒度,阈值20次请求
for (int i = 0; i < 5; i++) {
new Thread(() -> {
long end = System.currentTimeMillis() + TimeUnit.SECONDS.toMillis(3);
// 模拟请求
int j = 0;
while (System.currentTimeMillis() < end) {
if (slidingWindow.addRequest()) {
System.out.println("currentTime:" + Util.getNowStr() + " thread" + Thread.currentThread().getName() + " 第" + j + "次请求通过 当前窗口请求数:" + slidingWindow.getCount());
} else {
System.err.println("currentTime:" + Util.getNowStr() + " thread" + Thread.currentThread().getName() + " 第" + j + "次请求被限流了 当前窗口请求数" + slidingWindow.getCount());
}
j++;
Util.sleep(Util.random(150, 250));
}
}).start();
}
}
}
lastBucketTime 的理解
这里的 lastBucketTime 可能会有点难以理解,第一感觉是这个值为什么不设置为请求的当前时间,实际上这里是一个巧妙的模拟。
我们知道滑动窗口每隔一段时间,就会滚动即发生一次更新,但我们这里没有使用类似异步线程来做这种更新,而是在实际的请求到来时,去计算过去的时间并且触发更新,覆盖应该被遗忘的数组,这里的计算出lastBucketTime实际上就是被模拟的上一次发生滑动的时间
举一个具体的例子:
如果窗口设置为1000ms,滑动间隔是100ms
第一个请求在2001ms到来,此时如果窗口会自己滚动,那么最后一个发生滚动的时间应该是2000ms,即:20001 - (2001% 1000) = 2000
第二个请求在2999ms到来 ,那么最后一个发生滚动的时间仍然应该是2000ms(下一次发生滚动的时间在3000ms)
如果这里的不这样做,而是设置为请求到来的时的当前时间,那么加入每1ms都来一个请求,将导致elapsed总是小于1个滑动时间间隔,以至于无法触发模拟的滚动行为