一、为什么需要限流?
限流的核心目标是控制系统单位时间内的请求处理量,防止系统过载。它通常用于以下场景:
- 保护后端服务:避免数据库、缓存等底层组件因过载而崩溃;
- 防止恶意攻击:如高频刷接口、DDoS攻击等;
- 资源公平分配:对不同用户或租户实施配额管理;
- 保障SLA(服务等级协议):确保关键业务路径的资源优先级。
限流策略可以作用于多个层级:客户端、API网关、微服务内部等。无论在哪一层,其核心逻辑都依赖于某种限流算法。
二、主流限流算法详解
1. 固定窗口(Fixed Window)
固定窗口是最简单的限流方式。它将时间划分为固定长度的窗口(如 1秒),在每个窗口内统计请求数,若超过阈值则拒绝后续请求。
例如:限制每秒最多100次请求。从 00:00:00 到 00:00:01为第一个窗口,若在此期间收到 100 个请求,则第 101 个请求被拒绝;下一窗口从 00:00:01 开始重新计数。
优点:实现简单,性能高。
缺点:存在“边界问题”。假设在 00:00:00.9接收了 100 个请求,又在 00:00:01.1 接收了100 个请求,虽然总时间仅0.2秒,但两个窗口各自未超限,实际却在短时间内承受了 200 QPS 的压力,可能压垮系统。
因此,固定窗口适用于对精度要求不高的场景,但在生产环境中往往不够稳健。
2.滑动窗口(Sliding Window)
为解决固定窗口的边界问题,滑动窗口通过更细粒度的时间划分来平滑限流效果。
滑动窗口有两种常见实现方式:
- 基于时间戳的滑动窗口:记录每个请求的时间戳,每次判断时剔除超出时间窗口的旧请求,再统计剩余数量。
- 分片滑动窗口(Sliding Log或 Sliding WindowCounter):将大窗口划分为多个小格子(buckets),每个格子记录该时间段内的请求数。例如,1秒窗口划分为 10 个100ms的格子。每次只保留最近 10 个格子的数据,动态滑动。
以分片滑动窗口为例:设窗口大小为 1秒,划分为 10 个100ms 的桶。当前时间为t,我们只考虑[t-1s, t]范围内的请求。每个新请求到来时,更新对应时间桶的计数,并丢弃过期桶。最终限流判断基于所有有效桶的总和。
优点:比固定窗口更精确,能有效避免边界突刺。
缺点:实现复杂度较高,需维护多个时间桶;内存占用随窗口粒度增加而上升。
3.漏桶算法(Leaky Bucket)
漏桶算法将请求想象成水流,进入一个固定容量的“桶”中,桶以恒定速率“漏水”(即处理请求)。如果桶满了,新来的请求就会被丢弃。
关键参数:
- 桶容量(burst capacity):允许突发的最大请求数;
- 漏水速率(rate):单位时间处理的请求数。
漏桶的特点是输出速率恒定,即使有突发流量,也会被平滑处理。例如,设置速率为 100 QPS,桶容量为 50。那么即使瞬间涌入 200 个请求,前50 个可暂存,后续150个被拒绝;之后系统以 100 QPS 的速度匀速处理。
优点:输出平滑,适合对处理速率有严格要求的场景(如支付系统)。
缺点:无法应对合理突发流量。比如用户正常操作产生短时高峰,也可能被限流。
4. 令牌桶算法(Token Bucket)
令牌桶是漏桶的“反向”模型。系统以固定速率向桶中添加令牌,每个请求需要消耗一个令牌才能被处理。若桶中无令牌,则请求被拒绝。
关键参数:
- 令牌生成速率(rate);
- 桶容量(burstsize):最大可积攒的令牌数。
例如:速率100 QPS,桶容量 50。若系统空闲 1 秒,桶中会积攒 100 个令牌(但最多 50),此时可一次性处理 50 个突发请求。
优点:
- 允许一定程度的突发流量;
- 实现灵活,广泛应用于网络流量控制(如 TCP)、API 网关(如 Nginx、SpringCloud Gateway)。
缺点:若桶容量设置过大,可能在突发时仍造成后端压力。
三、算法对比总结
| 算法 | 是否允许突发 | 输出是否平滑 | 实现复杂度 | 适用场景 |
|---|---|---|---|---|
| 固定窗口 | 是 | 否 | 低 | 简单限流,容忍边界问题 |
| 滑动窗口 | 是 | 较平滑 | 中 | 高精度限流,如 API 调用频率控制 |
| 漏桶 | 否 | 是 | 中 | 需严格平滑输出的场景 |
| 令牌桶 | 是 | 否(输入可突发) | 中 | 通用限流,兼顾突发与稳态 |
在实际工程中,令牌桶因其灵活性和良好平衡性,成为最常用的限流算法。
四、单机限流的Java 实现
下面我们用 Java 实现几种限流器,并结合 LeetCode 风格的问题进行演示。
LeetCode 风格题目:设计一个限流器,支持每秒最多处理 N 个请求。若请求超过限制,则返回 false;否则返回 true。
解法一:固定窗口(简单但有缺陷)
public class FixedWindowRateLimiter {
private final int maxRequests;
private final long windowSizeMs;
private int currentRequests;
private long windowStart;
public FixedWindowRateLimiter(int maxRequests, long windowSizeMs) {
this.maxRequests= maxRequests;
this.windowSizeMs = windowSizeMs;
this.currentRequests= 0;
this.windowStart = System.currentTimeMillis();
}
public synchronized boolean allow() {
long now = System.currentTimeMillis();
if (now - windowStart > windowSizeMs) {
//进入新窗口
currentRequests =0;
windowStart = now;
}
if (currentRequests < maxRequests) {
currentRequests++;
return true;
}
return false;
}
}
该实现线程安全,但存在边界问题。
解法二:滑动窗口(基于时间戳)
import java.util.LinkedList;
import java.util.Queue;
public class SlidingWindowRateLimiter {
private final int maxRequests;
private final long windowSizeMs;
private final Queue<Long> timestamps;
public SlidingWindowRateLimiter(int maxRequests, long windowSizeMs) {
this.maxRequests = maxRequests;
this.windowSizeMs = windowSizeMs;
this.timestamps = new LinkedList<>();
}
public synchronized boolean allow() {
long now = System.currentTimeMillis();
//清理过期请求
while (!timestamps.isEmpty() && now - timestamps.peek() > windowSizeMs){
timestamps.poll();
}
if (timestamps.size() < maxRequests) {
timestamps.offer(now);
return true;
}
return false;
}
}
此方法精度高,但队列可能积累大量数据(尤其在高并发下),影响性能。
解法三:令牌桶(推荐)
我们可以借助 Guava 的 RateLimiter,但为了展示原理,手写一个简化版:
public class TokenBucketRateLimiter {
private final double rate; // tokens per second
private final int capacity;
private double tokens;
private long lastRefillTimestamp;
public TokenBucketRateLimiter(double rate, intcapacity) {
this.rate = rate;
this.capacity = capacity;
this.tokens = capacity;
this.lastRefillTimestamp = System.currentTimeMillis();
}
public synchronized boolean allow() {
refill();
if(tokens >= 1) {
tokens -=1;
return true;
}
return false;
}
private void refill() {
long now = System.currentTimeMillis();
long elapsedMs = now - lastRefillTimestamp;
if (elapsedMs > 0) {
double newTokens = elapsedMs /1000.0 * rate;
tokens = Math.min(capacity, tokens + newTokens);
lastRefillTimestamp = now;
}
}
}
该实现支持突发,且性能较好。注意:实际生产中建议使用原子变量或更高效的并发结构(如 LongAdder)替代synchronized,以提升吞吐量。
五、分布式限流的挑战与方案
单机限流在单体架构中足够使用,但在微服务或集群环境下,各节点独立限流会导致整体流量失控。例如,10 个实例,每个限流 100 QPS,理论上系统可承受 1000 QPS,但如果所有流量打到其中一个实例,仍可能过载。
因此,分布式限流需要全局统一的计数器。核心挑战包括:
- 一致性:多个节点需共享同一限流状态;
- 性能:频繁读写中心化存储可能成为瓶颈;
- 容错性:中心节点故障不应导致全局限流失效。
常见解决方案如下:
- 基于 Redis 的分布式令牌桶
Redis提供了原子操作(如 INCR、EXPIRE、Lua 脚本),非常适合实现分布式限流。
以滑动窗口为例,可使用 Redis 的 ZSET(有序集合)存储请求时间戳:
-- Lua 脚本:滑动窗口限流
local key = KEYS[1]
local window = tonumber(ARGV[1]) --窗口大小(毫秒)
local limit = tonumber(ARGV[2]) -- 最大请求数
local now = tonumber(ARGV[3])
--移除过期元素
redis.call('ZREMRANGEBYSCORE', key, 0, now - window)
-- 获取当前请求数
local count = redis.call('ZCARD', key)
if count < limit then
redis.call('ZADD', key, now, now, math.random())
redis.call('EXPIRE',key, math.ceil(window /1000))
return 1
else
return 0
end
Java调用示例(使用 Jedis):
public boolean tryAcquire(String key, int limit, long windowMs) {
long now = System.currentTimeMillis();
String script =" ... "; // 上述Lua 脚本
Object result = jedis.eval(script, Collections.singletonList(key),
Arrays.asList(String.valueOf(windowMs), String.valueOf(limit), String.valueOf(now)));
return ((Long) result) == 1;
}
优点:精度高,支持滑动窗口; 缺点:ZSET 内存占用较大,高并发下可能影响 Redis性能。
2.Redis + 固定窗口(高性能方案)
若对精度要求不高,可使用 INCR + EXPIRE 实现固定窗口:
public boolean tryAcquire(String key, int limit, int windowSec) {
String redisKey = "rate_limit:" + key;
Long count = jedis.incr(redisKey);
if (count == 1) {
jedis.expire(redisKey, windowSec);
}
return count <= limit;
}
该方法简单高效,但存在边界问题。可通过“双窗口”技巧缓解:同时维护当前窗口和上一窗口的计数,按比例估算真实流量。
3. 本地缓存 +异步同步(混合限流)
为减少对 Redis 的依赖,可采用“本地令牌桶 + 中央协调”的混合模式:
- 每个节点维护本地令牌桶;
- 定期从Redis 获取全局配额,并按节点数均分;
- 使用“预分配”或“租约”机制,避免频繁交互。
例如:总限流 1000 QPS,10 个节点,则每个节点初始获得100 QPS配额。当本地配额耗尽,向 Redis 申请新配额(如一次申请 10个)。这种方式在保证一定精度的同时,大幅降低 Redis压力。
4.使用 Sentinel 或 Resilience4j
在微服务生态中,可直接集成成熟框架:
- Sentinel(阿里开源):支持集群流控,通过 TokenServer 统一管理限流规则;
- Resilience4j:轻量级容错库,支持基于环形缓冲区的滑动窗口限流;
- Spring Cloud Gateway:内置 Redis+ Lua 的分布式限流过滤器。
六、实际工程建议
- 选择合适的算法:一般优先选用令牌桶,兼顾突发与稳态;对输出平滑性要求极高时选漏桶。
- 避免过度限流:限流应作为兜底手段,配合降级、熔断、排队等策略。
- 监控与告警:记录限流日志,监控限流触发频率,及时调整阈值。
- 测试边界场景:模拟突发流量、时钟漂移、节点扩容等异常情况。
- 考虑多维度限流:不仅限于 QPS,还可按用户 ID、IP、API路径等维度组合限流。
七、结语
限流是系统稳定性建设的基石之一。从单机到分布式,从固定窗口到令牌桶,每种算法都有其适用边界。理解其原理,结合业务场景选择合适方案,才能在高并发浪潮中稳如磐石。随着云原生和Service Mesh 的发展,限流能力正逐步下沉至基础设施层(如 Istio、Envoy),但开发者仍需掌握底层逻辑,方能在复杂系统中做出正确决策。
限流不是目的,而是手段。真正的目标,是在保障用户体验的同时,让系统在极限压力下依然可靠运行。