一、限流的定义
- 核心目的:控制请求速率,防止系统因高并发或大流量过载,保障稳定性。
- 应用场景:
- 防止 DoS 攻击、限制 Web 爬虫。
- 保护服务器、数据库等资源,避免因突发流量崩溃。
- 平衡挑战:限流可能拒绝部分请求,影响用户体验,需在系统稳定与用户体验间权衡。
二、四种经典限流算法
1. 固定窗口算法
- 原理:
将时间划分为固定窗口(如 1 秒),统计窗口内请求次数,超过阈值则拒绝后续请求,窗口结束后计数器清零。 - 问题:
临界突刺:窗口切换时可能出现双倍流量(如 0.9 秒和 1.1 秒各触发一次阈值请求,实际 1 秒内处理双倍请求)。 - Java 实现:
java
class FixedWindowRateLimiter { // 窗口内允许的最大请求数 private final int limit; // 窗口大小(毫秒) private final long windowSizeMs; // 当前窗口内的请求计数器(原子操作保证线程安全) private AtomicInteger count = new AtomicInteger(0); // 当前窗口的开始时间(原子操作保证线程安全) private AtomicLong windowStart = new AtomicLong(System.currentTimeMillis()); /** * 构造函数 * @param limit 窗口内允许的最大请求数 * @param windowSizeMs 窗口大小(毫秒) */ public FixedWindowRateLimiter(int limit, long windowSizeMs) { this.limit = limit; this.windowSizeMs = windowSizeMs; } /** * 尝试获取一个请求许可 * @return true表示允许请求,false表示拒绝请求 */ public boolean tryAcquire() { long now = System.currentTimeMillis(); // 检查是否需要重置窗口(当前时间已超过窗口结束时间) if(now - windowStart.get() > windowSizeMs) { // 使用CAS操作原子性地重置窗口开始时间和计数器 if(windowStart.compareAndSet(windowStart.get(), now)) { count.set(0); } } // 原子性地增加计数并检查是否超过限制 return count.incrementAndGet() <= limit; } }
- 特点:
- 实现简单,内存占用低。
- 精度低,无法应对临界突刺。
2. 滑动窗口算法
- 原理:
将固定窗口划分为多个子窗口(如 1 秒分为 5 个 0.2 秒的子窗口),每个子窗口独立计数,窗口随时间滑动时累加有效子窗口的请求数。 - 优势:
解决固定窗口的临界突刺问题,子窗口越多,统计越精确。 - Java 实现:
java
class SlidingWindowRateLimiter { // 窗口内允许的最大请求数 private final int limit; // 子窗口数量 private final int windowCount; // 每个子窗口的大小(毫秒) private final long windowSizeMs; // 子窗口计数器数组(原子操作保证线程安全) private final AtomicInteger[] windows; // 当前窗口开始时间(原子操作保证线程安全) private AtomicLong currentWindowStart = new AtomicLong(System.currentTimeMillis()); // 当前窗口索引 private int currentWindowIndex = 0; /** * 构造函数 * @param limit 窗口内允许的最大请求数 * @param windowSizeMs 总窗口大小(毫秒) * @param windowCount 子窗口数量 */ public SlidingWindowRateLimiter(int limit, long windowSizeMs, int windowCount) { this.limit = limit; this.windowCount = windowCount; this.windowSizeMs = windowSizeMs / windowCount; // 计算每个子窗口大小 this.windows = new AtomicInteger[windowCount]; for (int i = 0; i < windowCount; i++) { windows[i] = new AtomicInteger(0); // 初始化所有子窗口计数器 } } /** * 尝试获取一个请求许可 * @return true表示允许请求,false表示拒绝请求 */ public boolean tryAcquire() { long now = System.currentTimeMillis(); // 计算当前窗口索引 long elapsed = now - currentWindowStart.get(); int windowIndex = (int) ((elapsed / windowSizeMs) % windowCount); // 重置过期窗口 if (elapsed > windowSizeMs * windowCount) { resetAllWindows(); // 如果超过整个窗口周期,重置所有窗口 currentWindowStart.set(now); } else if (elapsed > windowSizeMs) { // 重置已过期的子窗口 int skipCount = (int) (elapsed / windowSizeMs); for (int i = 0; i < skipCount; i++) { int idx = (currentWindowIndex + i + 1) % windowCount; windows[idx].set(0); } currentWindowStart.addAndGet(skipCount * windowSizeMs); } currentWindowIndex = windowIndex; // 统计所有子窗口的请求总数 int total = 0; for (AtomicInteger window : windows) { total += window.get(); } // 检查是否超过限制 if (total >= limit) { return false; } // 原子性地增加当前子窗口计数 return windows[windowIndex].incrementAndGet() <= limit; } /** * 重置所有子窗口计数器 */ private void resetAllWindows() { for (AtomicInteger window : windows) { window.set(0); } } }
- 特点:
- 精度高,临界流量控制更严格。
- 内存占用较高(需维护多个子窗口计数器)。
3. 漏桶算法
- 原理:
请求如 “水流” 进入漏桶,漏桶以固定速率(如每秒处理 100 请求)“漏水”(处理请求),桶满时拒绝新请求。 - 特点:
- 平滑处理请求,无突发流量,但无法应对瞬时高峰。
- 适合需要严格控制平均速率的场景(如消息队列削峰)。
- Java 实现:
java
class LeakyBucketRateLimiter { // 桶的总容量(最大水量) private final int capacity; // 漏出速率(每秒漏出多少单位水) private final int rate; // 当前桶中的水量 private int water; // 上次漏水的时间戳(毫秒) private long lastLeakTime; /** * 构造函数 * @param capacity 桶的总容量 * @param rate 漏出速率(单位/秒) */ public LeakyBucketRateLimiter(int capacity, int rate) { this.capacity = capacity; this.rate = rate; this.water = 0; // 初始时桶是空的 this.lastLeakTime = System.currentTimeMillis(); } /** * 尝试获取一个请求许可(线程安全) * @return true表示允许请求,false表示拒绝请求 */ public synchronized boolean tryAcquire() { long now = System.currentTimeMillis(); // 计算自上次漏水后经过的时间(毫秒) long elapsedMillis = now - lastLeakTime; // 计算这段时间应该漏出的水量 int leakedWater = (int) (elapsedMillis * rate / 1000); // 漏水(水量不能为负) water = Math.max(0, water - leakedWater); lastLeakTime = now; // 更新最后漏水时间 if (water < capacity) { water++; // 增加水量 return true; // 允许请求通过 } return false; // 拒绝请求 } }
4. 令牌桶算法
- 原理:
系统以固定速率(如每秒生成 100 令牌)向桶中放令牌,桶满则丢弃多余令牌。请求需获取令牌才能处理,无令牌则拒绝。 - 优势:
- 允许突发流量(桶中预存令牌可应对瞬时高峰)。
- 长期维持平均速率,兼顾稳定性与灵活性。
- Java 实现:
java
class TokenBucketRateLimiter{ // 桶的总容量(最大令牌数) private final int capacity; // 令牌填充速率(每秒填充多少个令牌) private final double rate; // 当前桶中的令牌数量 private double tokens; // 上次补充令牌的时间戳(毫秒) private long lastRefillTime; /** * 构造函数 * @param capacity 桶的总容量 * @param rate 令牌填充速率(令牌/秒) */ public TokenBucketRateLimiter(int capacity,double rate){ this.capacity = capacity; this.rate = rate; this.tokens = capacity; // 初始化时桶是满的 this.lastRefillTime = System.currentTimeMillis(); // 记录初始时间 } /** * 尝试获取一个令牌 * @return true表示获取成功,false表示被限流 */ public synchronized boolean tryAcquire(){ long now = System.currentTimeMillis(); // 计算自上次补充后经过的时间(秒) double elapsedSeconds = (now - lastRefillTime) / 1000.0; // elapsed 时间过去 // 计算这段时间应该补充的令牌数 double newTokens = elapsedSeconds * rate; // 补充令牌(不超过桶容量) tokens = Math.min(capacity, tokens + newTokens); lastRefillTime = now; // 更新最后补充时间 // 检查是否有足够令牌 if(tokens >= 1){ tokens--; // 消耗一个令牌 return true; // 允许请求通过 } return false; // 拒绝请求 } }
- 应用:
Guava 的RateLimiter
组件即基于此算法实现。
三、算法对比
算法 | 是否允许突发流量 | 速率控制 | 临界问题 | 典型场景 |
---|---|---|---|---|
固定窗口 | 否 | 粗略控制平均速率 | 存在突刺 | 简单限流(如测试环境) |
滑动窗口 | 否 | 精确控制平均速率 | 无 | 金融交易、API 网关 |
漏桶算法 | 否 | 严格固定速率 | 无 | 消息队列、实时数据处理 |
令牌桶算法 | 是(依赖桶容量) | 平均速率 + 突发容忍 | 无 | 电商秒杀、广告投放系统 |
四、总结
- 固定窗口和滑动窗口属于时间窗口类算法,核心差异在于统计精度。
- 漏桶和令牌桶属于流量整形算法,前者限制流出速率,后者允许突发但控制平均速率。
- 实际选择需结合业务场景:
- 若需严格控制瞬时流量,选滑动窗口或漏桶。
- 若需兼顾突发流量和稳定性,选令牌桶(如 Guava 组件)