限流器
概念
在当前的微服务或分布式系统下,需要保证整个系统的高可用,限流就是高可用的实现手段之一,限流的意思是流量限速,当请求到来的速度大于系统处理的速度时,如果积压的请求数量超过阈值,会触发限流策略,后续的请求会被拒绝或排队。限流是出于安全性考虑,避免流量过大或恶意流量将系统打崩。
限流方式
限流的方式大致可以分为本地限流和分布式限流:
- 本地限流:每个业务接口内部单独设置限流逻辑,每个接口独立控制流量,适合节点粒度的流量控制
- 分布式限流:分布式限流不同于单机限流,是控制整个服务的流量。限流逻辑一般位于网关或者共享中间件(Redis)中,适合全局流量控制
| 单机限流 | 分布式限流 |
|---|---|
![]() |
![]() |
一般是通过网关做分布式限流,网关本身起到请求转发的作用,是流量的统一入口,所以将限流或负载均衡这类共享的非业务逻辑放在网关最为合适。
限流算法
固定窗口限流
| 固定窗口限流 |
|---|
![]() |
固定窗口限流算法将时间划分为固定的窗口大小(1s),在一个时间窗口内,每到来一个请求,计数器+1,当超过限流阈值后,窗口内后续的请求全部丢弃,直到下一个窗口计数器重置为0。
固定窗口限流算法的优点是实现非常简单,且内存占用极小,只需要存储当前的时间窗口标识以及计数器,缺点是限流不够平滑,并且存在临界限流失效问题:窗口切换时可能会产生两倍于阈值流量的请求(突发流量)。
| 临界限流失效 |
|---|
![]() |
固定窗口限流器的代码如下:
public class CounterRateLimiter {
private long windowSize;
private int permitPerWindow;
private int counter;
private long latestWindowStartTime;
public CounterRateLimiter(long _windowSize, int _permitPerWindow) {
windowSize = _windowSize;
permitPerWindow = _permitPerWindow;
counter = 0;
latestWindowStartTime = System.currentTimeMillis();
}
/**
高并发下使用阻塞锁
*/
public synchronized boolean tryAcquire() {
long currentTime = System.currentTimeMillis();
if(currentTime - latestWindowStartTime < windowSize) {
if(counter < permitPerWindow) {
counter++;
return true;
}
return false;
}
counter = 1;
latestWindowStartTime = currentTime;
return true;
}
}
滑动窗口限流
滑动窗口限流算法是固定窗口限流算法的升级版,解决了固定窗口限流临界两倍阈值流量的问题,但是滑动窗口的实现和维护比较麻烦,并且限流不够平滑,无法适应突增流量,滑动窗口限流算法实现如下:
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
public class SlideWindowRateLimiter implements RateLimiter{
private final int windowSecond;
private final int subWindowSecond;
private final int subWindowCount;
private final int permitPerWindow;
private final int[] counter;
private int currentSubWindowIndex;
private long latestWindowStartTime;
public SlideWindowRateLimiter(int _windowSecond, int _subWindowSecond, int _permitPerWindow) {
windowSecond = _windowSecond;
subWindowSecond = _subWindowSecond;
subWindowCount = windowSecond / _subWindowSecond;
permitPerWindow = _permitPerWindow;
counter = new int[subWindowCount];
latestWindowStartTime = System.currentTimeMillis();
currentSubWindowIndex = 0;
}
public synchronized boolean tryAcquire() {
long elapsedMillis = System.currentTimeMillis() - latestWindowStartTime;
int elapsedTime = (int) Math.ceil(elapsedMillis / (subWindowSecond * 1000.0));
if(elapsedTime > 0) {
for(int i = 0; i < elapsedTime; i++) {
currentSubWindowIndex = (currentSubWindowIndex + 1) % subWindowCount;
counter[currentSubWindowIndex] = 0;
latestWindowStartTime += subWindowSecond * 1000L;
}
}
counter[currentSubWindowIndex]++;
int currentRequest = Arrays.stream(counter).sum();
return currentRequest <= permitPerWindow;
}
}
漏桶算法
滑动窗口限流解决了固定窗口的流量超过限流阈值的问题,但是窗口限流固有的一个缺陷没有解决,即限流不够平滑,同时无法应对突增流量,水桶算法的思想是,请求按照任意速度进入系统排队,而系统以一个固定的速率处理这些请求。虽然实现了了流量的平滑限流,但是在高并发情况下,请求的处理速率配置不当容易导致请求积压。
| 漏桶算法 |
|---|
![]() |
令牌桶算法
令牌桶算法是水桶算法的进一步改进,水桶算法无法应对高并发以及突增流量,那令牌桶怎么解决这个问题呢?
- 按照固定速率向桶中投放令牌
- 请求到来时,首先查看桶中是否有令牌,有的话,允许请求通过,并且令牌数-1,否则,触发限流策略
| 令牌桶算法 |
|---|
![]() |
| 令牌桶能够做到流量的平滑限流,同时能够处理突增流量(令牌会持续增加)。代码如下: |
public class TokenBucketRateLimiter {
private int capacity;
private int speed;
private int currentTokens;
private long latestSupplyTime;
public TokenBucketRateLimiter(int _capacity, int _speed) {
capacity = _capacity;
speed = _speed;
currentTokens = 0;
latestSupplyTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
supply();
if(currentTokens == 0) return false;
latestSupplyTime = System.currentTimeMillis();
currentTokens--;
return true;
}
private void supply() {
long current = System.currentTimeMillis();
int suppliedTokens = (int)((current - latestSupplyTime) * speed / 1000.0);
currentTokens = Math.min(currentTokens + suppliedTokens, capacity);
latestSupplyTime = current;
}
}
并不存在完美的限流算法,需要根据场景选择最合适的限流算法
| 限流算法 | 适用场景 |
|---|---|
| 固定窗口 | 对限流精度要求极低的场景,比如管理后台接口限流、日志上报限流等场景 |
| 滑动窗口 | 适合高精度限流(M秒内N次),但不允许有突增流量 |
| 漏桶 | 适合后端处理能力恒定的场景,比如音视频流控,要求严格匀速 |
| 令牌桶 | 适用于需要兼顾平滑性与突发能力,非常适合分布式系统下的高并发限流 |







浙公网安备 33010602011771号