前不久在项目中使用到了限流算法,今天就总结一下常见的限流算法,常见的四种限流算法:固定窗口、滑动窗口、漏桶算法和令牌桶算法,并比较它们的优缺点和适用场景。
1、固定窗口算法
固定窗口算法是最简单的限流算法之一,其核心思想是将时间划分为固定大小的窗口,并在每个窗口内限制请求的数量。具体来说,系统维护一个大小为 N 的计数器,每当有请求到来时,计数器加一。当一个窗口结束时,如果计数器的值超过了预设的阈值,则拒绝后续的请求。固定窗口算法的实现简单,但容易受到突发流量的影响,因为请求可能会在同一窗口内集中到达。
假设单位时间1s(窗口大小),限流阈值是4。也就是说每次请求来计数器都会+1,如果单位时间内,计数器的数量超过阈值,那么就会拒绝所有请求,等到单位时间结束后,计数器清0,重新计数
固定窗口限流算法代码如下:
public class FixedWindowRateLimiter {
private final AtomicInteger counter = new AtomicInteger(0);
private volatile long lastRequestTime;
private long windowUnit = 1000L;
private int threshold = 4;
public FixedWindowRateLimiter(){}
// 这块可以通过配置文件获取
public FixedWindowRateLimiter(long windowUnit, int threshold) {
this.windowUnit = windowUnit;
this.threshold = threshold;
}
public synchronized boolean fixedWindowsTryAcquire() {
long currentTime = System.currentTimeMillis();
if (currentTime - lastRequestTime > windowUnit) {
counter.set(0);
lastRequestTime = currentTime;
}
if (counter.get() < threshold) {
counter.incrementAndGet();
return true;
}
return false;
}
}
固定窗口限流算法的优缺点
优点:
1、实现简单易理解
缺点:
1、限流不均匀: 固定窗口算法可能会导致请求的限流不均匀。在某些时刻,请求量可能远远低于限流阈值,而在其他时刻则可能超过限流阈值。如下图所示:
2、临界问题:假设窗口大小是1s,阈值是4,那么,0.8s-1s来4个请求,在1s-1.2s来4个请求,虽然在每个独立的窗口内都没有超过阈值,但是在连续的窗口中,请求量却超过了阈值。即在窗口边界处出现突发的高并发请求,导致整体的请求量超过了阈值。如图所示。
2、滑动窗口限流算法
为了解决固定窗口算法的突发流量问题,滑动窗口算法应运而生。滑动窗口算法与固定窗口算法类似,但是它不是按照固定的窗口大小来计数,而是采用一个固定大小的滑动窗口在时间轴上滑动。每当有请求到达时,算法会检查滑动窗口内的请求数是否超过了阈值。通过滑动窗口的机制,滑动窗口算法能够更加平滑地限制流量,减少了对突发流量的敏感度。
滑动窗口限流算法代码如下:
public class SlidingWindowRateLimiter {
private long unitOfTime = 60000;
private int subCycle = 10;
private int thresholdPerMin = 100;
private final ConcurrentHashMap<Long, Integer> counters = new ConcurrentHashMap<>();
/**
* 滑动窗口时间算法实现
*/
public boolean allowRequest() {
// 将当前时间戳(以 UTC 时区为准)进行舍去操作,使其对齐到最近的子周期的起始时间
// 例如,如果子周期长度为 10 秒,并且当前时间戳是 1632345678,那么上述表达式的结果将是 1632345670,即对齐到最近的以 0 结尾的子周期起始时间。
long currentWindowTime = LocalDateTime.now().toEpochSecond(ZoneOffset.UTC) / subCycle * subCycle; //获取当前时间在哪个小周期窗口
int currentWindowNum = countCurrentWindow(currentWindowTime); //当前窗口总请求数
//超过阀值限流
if (currentWindowNum >= thresholdPerMin) {
return false;
}
//计数器+1
counters.put(currentWindowTime, counters.getOrDefault(currentWindowTime, 0) + 1);
return true;
}
/**
* 统计当前窗口的请求数
*/
private int countCurrentWindow(long currentWindowTime) {
//计算窗口开始位置
long startTime = currentWindowTime - subCycle * (unitOfTime / 1000L / subCycle - 1);
int count = 0;
//遍历存储的计数器
Iterator<Map.Entry<Long, Integer>> iterator = counters.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Long, Integer> entry = iterator.next();
// 删除无效过期的子窗口计数器
if (entry.getKey() < startTime) {
iterator.remove();
} else {
//累加当前窗口的所有计数器之和
count = count + entry.getValue();
}
}
return count;
}
}
从代码的角度简简单单举个例子来说明一下滑动窗口的限流,假设单位时间1min(窗口大小),把这个窗口划分为6个小窗口,每个小窗口是10s,每过10s,窗口就会往右滑动,限流阈值是100。也就是说每次请求来都会计算窗口计数器总和,如果总和小于设置的阈值,那么对应的小窗口计数器+1,否则就拒绝请求。
通过下图它是如何解决临界问题的
在当前窗口结束的位置有100个请求,时间超过了1min后,又来100个请求,此时由于窗口向前滑动了一小格,所以此时窗口内的请求是200,大于100,那么他会把第二个蓝色小框的请求都会拒绝
滑动窗口限流算法的优缺点
优点:简单易懂、统计精度高
缺点:无法处理突发流量
3、漏桶限流算法
漏桶限流算法是一种简单有效的限流算法,通俗点来说,其原理类似于一个漏桶,请求先进入漏桶,然后以固定的速率被处理或丢弃。漏桶限流算法主要包括两个核心参数:漏桶的容量和漏水的速率。
漏桶限流算法代码如下:
public class LeakyBucketRateLimiter {
private long bucketSize = 10; // 漏桶容量,默认为 10
private long leakyRate = 1; // 漏水速率,默认为 1 每毫秒
private long currentSize = 0; // 漏桶当前水量
private long lastTime = System.currentTimeMillis(); // 上次处理请求的时间,默认为当前时间
/**
* 无参构造函数,使用默认的漏桶容量、漏水速率、当前水量和上次处理请求的时间。
*/
public LeakyBucketRateLimiter() {
}
/**
* 带参构造函数,使用指定的漏桶容量、漏水速率、当前水量和上次处理请求的时间。
* @param bucketSize 漏桶容量
* @param leakyRate 漏水速率
* @param currentSize 当前水量
* @param lastTime 上次处理请求的时间
*/
public LeakyBucketRateLimiter(long bucketSize, long leakyRate, long currentSize, long lastTime) {
this.bucketSize = bucketSize;
this.leakyRate = leakyRate;
this.currentSize = currentSize;
this.lastTime = lastTime;
}
/**
* 允许处理请求的方法。
* @return 如果漏桶中有足够的水量处理请求,则返回 true;否则返回 false。
*/
public synchronized boolean allowRequest() {
long currentTime = System.currentTimeMillis(); // 获取当前时间
long deltaTime = currentTime - lastTime; // 计算距离上次处理请求的时间间隔
currentSize = Math.max(0, currentSize - deltaTime * leakyRate); // 漏水,更新当前水量
if (currentSize < bucketSize) { // 如果漏桶中有足够的空间
currentSize++; // 处理请求,放入漏桶
lastTime = currentTime; // 更新上次处理请求的时间
return true; // 允许处理请求
}
return false; // 漏桶已满,拒绝处理请求
}
}
漏桶限流算法的优缺点
优点:
1、简单有效:漏桶限流算法简单易懂,易于实现和部署。
2、平滑限流:漏桶算法以固定速率处理请求,可以实现平滑的限流效果,防止突发请求对系统造成影响。
3、控制请求速率:漏桶限流算法可以有效地控制请求的处理速率,保护后端系统免受突发请求的影响。
缺点:
1、不适用于突发流量:漏桶限流算法无法应对突发流量,因为漏桶的处理速率是固定的,无法根据实际流量动态调整。
2、延迟较大:漏桶限流算法会对请求进行排队,可能会增加请求的处理延迟,不适用于对延迟敏感的场景。
4、令牌桶限流算法
令牌桶限流算法是一种常用的限流算法,用于控制单位时间内请求的数量,保护系统免受突发流量的影响。其基本原理是系统维护一个令牌桶,每个令牌表示一个允许通过的请求。在单位时间内,令牌桶以固定速率生成令牌,请求到达时需要从令牌桶中获取令牌,如果桶中有足够的令牌,则允许请求通过;否则拒绝请求。
令牌桶限流算法代码如下:
public class TokenBucketRateLimiter {
private long capacity = 10; // 令牌桶容量
private long rate = 10; // 令牌生成速率,单位:毫秒
private AtomicLong tokens = new AtomicLong(10); // 当前令牌数量
private long lastRefillTime; // 上次令牌补充时间
public TokenBucketRateLimiter() {
}
public TokenBucketRateLimiter(long capacity, long rate) {
this.capacity = capacity;
this.rate = rate;
this.tokens = new AtomicLong(capacity); // 初始时,令牌桶满
this.lastRefillTime = System.currentTimeMillis(); // 初始化时记录上次补充时间
}
// 尝试获取令牌,成功返回true,失败返回false
public synchronized boolean allowRequest() {
refillTokens(); // 补充令牌
long currentTokens = tokens.get(); // 获取当前令牌数量
if (currentTokens > 0) { // 令牌桶中有令牌
tokens.decrementAndGet(); // 消费一个令牌
return true; // 获取令牌成功
}
return false; // 令牌桶中无令牌,获取失败
}
// 补充令牌
private synchronized void refillTokens() {
long now = System.currentTimeMillis(); // 当前时间
long elapsedTime = now - lastRefillTime; // 距离上次补充令牌的时间间隔
long tokensToAdd = elapsedTime * rate / 1000; // 计算需要补充的令牌数量
if (tokensToAdd > 0) { // 需要补充令牌
tokens.set(Math.min(capacity, tokens.get() + tokensToAdd)); // 补充令牌,并确保不超过容量
lastRefillTime = now; // 更新上次补充时间
}
}
}
令牌桶算法的优缺点
优点:
1、稳定性高:令牌桶算法能够控制请求的处理速度,从而使系统的负载变得稳定。
2、精确度高。算法可以根据实际情况动态调整生成令牌的速率,实现较高精度的限流。
缺点:
1、对短时请求难以处理。在短时间内有大量请求到来时,可能会导致令牌快速消耗完,从而限流。
2、时间精确要求高。算法需要在固定的时间间隔内生成令牌,对系统时间的准确性要求较高。