限流
限流已经是目前互联网项目的常规手段了,主要作用是为了在系统遇到大量突发的请求时,通过拒绝或丢弃超出承载能力以外的请求,从而达到保护系统的目的。我们下面主要讨论几种主要的限流算法。
计数算法
顾名思义,计数就是简单的计算单位时间内的请求总数,超过上限的请求直接拒绝
示例代码:
public class SimpleCountLimiter {
private int limit = 10;
private int window = 1000;
private int reqCount = 0;
private volatile long start = System.currentTimeMillis();
public SimpleCountLimiter(int limit, int window) {
this.limit = limit;
this.window = window;
}
public synchronized boolean acquire() {
int cur = reqCount++;
long ts = System.currentTimeMillis();
if (cur < limit) {
return true;
}
if (ts - start > window) {
start = ts;
reqCount = 1;
return true;
}
return false;
}
}
直接计数有啥问题呢? 假如我的突发请求集中在两个时间窗口的交汇处,那么就可能在一个时间窗口内接收两倍的请求,超出了我们的系统承受范围。
滑动窗口
滑动窗口可以解决上面简单计数所面临的问题,它将整个窗口W划分为N段,每个请求通过计算落在其中某个段内,每经过W/N时间则窗口向右滑动一格。 这样的话,段划分的越多,则请求限流的越平滑。如果只划分一段,其实就是我们上面简单计数算法了。
示例代码:
public class SlidingWindowLimiter {
//窗口大小 单位 ms
private int window = 1000;
//窗口划分格数
private int slot = 5;
//时间窗口内请求上限
private int limit = 50;
//窗口内每格请求数
private final int[] reqCount = new int[slot];
//每格时间
private final long timePerSlot = window / slot;
//当前指向slot
private int index = 0;
//时间窗口开始时间
private long start = System.currentTimeMillis();
public SlidingWindowLimiter() {
}
public SlidingWindowLimiter(int window, int slot, int limit) {
this.window = window;
this.slot = slot;
this.limit = limit;
}
public synchronized boolean acquire() {
long cur = System.currentTimeMillis();
//需要滑动的slot数量,如果当前时间减去滑动窗口开始时间小于滑动窗口,证明不需要移动。
long slideSlot = Math.max(cur - start - window, 0) / this.timePerSlot;
//等于0不需要滑动
if (slideSlot > 0) {
//如果需要滑动的slot数量大于划分的slot,则取默认的slot大小
long move = Math.min(slideSlot, slot);
//每滑动一格,将当前指向的格内请求数清空
for (int i = 0; i < move; i++) {
index = (index + 1) % slot;
reqCount[index] = 0;
}
//更新滑动窗口开始时间
start = start + move * timePerSlot;
}
int sum = 0;
for (int j : reqCount) {
sum += j;
}
if (sum >= limit) {
return false;
}
reqCount[index]++;
return true;
}
}
漏斗算法
漏斗算法和消息队列相似,想象一下漏斗是怎么工作的,上面一个巨大的开口,不管放多少沙子进去,下面的出口永远是按指定的速率流出.漏洞算法也是这样的:维护一个指定大小的队列,按指定的速率消费队列来执行请求,当请求大于队列大小时,则拒绝请求。
示例代码:
public class FunnelLimiter {
private final int limit;
private final int rate;
private final ArrayBlockingQueue<Request> taskQueue;
public FunnelLimiter(int limit, int rate) {
this.limit = limit;
this.rate = rate;
taskQueue = new ArrayBlockingQueue<>(this.limit);
new Thread(() -> {
while (true) {
try {
handleRequest(taskQueue.take());
Thread.sleep(1000 / this.rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
private void handleRequest(Request request) {
//do request
System.out.println("do request:" + request);
}
public boolean acquire(Request request) {
return taskQueue.offer(request);
}
@Data
@AllArgsConstructor
public static class Request {
private long id;
}
}
令牌桶算法
令牌桶算法和漏斗类似,漏斗是按一定的速率处理请求,而令牌桶是按一定的速率生成令牌,请求只有获取到令牌才可以被执行,否则丢弃。
示例代码:
public class RateLimiter {
private final int limit;
private final int rate;
private final int init;
private final AtomicInteger bucket;
public RateLimiter(int limit, int rate, int init) {
this.limit = limit;
this.rate = rate;
this.init = init;
bucket = new AtomicInteger(this.init);
new Thread(() -> {
while (true) {
int count = bucket.get();
if (count < this.limit) {
bucket.incrementAndGet();
}
try {
Thread.sleep(1000 / this.rate);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
public synchronized boolean acquire() {
int remained = bucket.get();
if (remained <= 0) {
return false;
}
bucket.decrementAndGet();
return true;
}
}
令牌桶与漏斗算法的区别在于:令牌桶算法只要获取到令牌,则可以执行请求,意味着在一定时间内,是允许处理部分突发流量的。而漏斗算法是按指定的速率来消费请求,所以它主要是用来平滑流量,将突发流量转换为稳定的流量,上面说到他和消费队列类似,这其实就是MQ的流量削峰。