2. 限流基础:常见限流算法(令牌桶、漏桶、滑动窗口)原理与 Spring Boot 实现
限流是分布式系统抵御流量洪峰的第一道防线。在缓存一致性治理场景中,限流不仅保护下游资源,更防止因突发请求导致缓存穿透、雪崩等连锁故障。本节深入剖析三种经典限流算法的数学原理与工程实现,并给出 Spring Boot 中的可落地代码。
2.1 算法核心对比
| 算法 | 核心思想 | 允许突发 | 平滑度 | 内存开销 | 典型场景 |
|---|---|---|---|---|---|
| 令牌桶 | 固定速率生成令牌,请求消耗令牌 | 是(桶容量控制峰值) | 中等 | 低(2个变量) | API 网关、缓存入口 |
| 漏桶 | 请求进入队列,固定速率漏出 | 否(强制削峰) | 高 | 中(队列) | 数据库连接池、消息消费 |
| 滑动窗口 | 将时间窗口细分为小格,统计请求数 | 有限(窗口内) | 低(粒度决定) | 高(数组/环形缓冲区) | 秒杀、热点 Key 防护 |
缓存一致性视角: 令牌桶允许短时突发,适合缓存预热后的正常流量;漏桶强制平滑,适合写缓存场景防止数据库压力;滑动窗口精度高,适合对缓存 Key 级别做细粒度限流。
2.2 令牌桶算法(Token Bucket)
2.2.1 原理与数学模型
令牌桶以恒定速率 r 向容量为 b 的桶中放入令牌。每个请求必须获取一个令牌才能通过。若桶满则令牌丢弃;若桶空则请求被拒绝或等待。
- 关键参数: 速率
r(令牌/秒)、容量b(最大突发量) - 瞬时特性: 允许最大突发流量为
b,长期平均速率不超过r - 数学表达: 当前令牌数 = min(容量, 上次令牌数 + (当前时间 - 上次时间) × r)
2.2.2 Spring Boot 实现(基于 Guava RateLimiter)
import com.google.common.util.concurrent.RateLimiter;
import org.springframework.stereotype.Component;
@Component
public class TokenBucketLimiter {
// 每秒生成 10 个令牌,桶容量 20
private final RateLimiter rateLimiter = RateLimiter.create(10.0);
/**
* 尝试获取令牌,非阻塞
* @return true 表示获取成功,请求允许通过
*/
public boolean tryAcquire() {
return rateLimiter.tryAcquire();
}
/**
* 阻塞获取令牌,最多等待 timeout 毫秒
*/
public boolean tryAcquire(long timeout, java.util.concurrent.TimeUnit unit) {
return rateLimiter.tryAcquire(timeout, unit);
}
}
缓存场景建议: 对热点缓存 Key 设置独立的 RateLimiter 实例,避免全局限流误伤正常 Key。例如:
ConcurrentHashMap<String, RateLimiter> 按 Key 前缀隔离。
2.3 漏桶算法(Leaky Bucket)
2.3.1 原理与数学模型
漏桶将请求放入一个固定容量的队列(桶),以恒定速率从队列底部漏出处理。当桶满时,新请求被直接丢弃。漏桶强制输出速率恒定,是 “削峰填谷” 的典型代表。
- 关键参数: 漏出速率
r(请求/秒)、桶容量b(最大排队数) - 核心特性: 无论入口流量多陡峭,出口流量永远平滑
- 与令牌桶区别: 漏桶不允许突发,令牌桶允许突发
2.3.2 Spring Boot 实现(基于阻塞队列)
import org.springframework.stereotype.Component;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.TimeUnit;
@Component
public class LeakyBucketLimiter {
private final BlockingQueue<Runnable> queue;
private final int capacity = 10;
public LeakyBucketLimiter() {
this.queue = new LinkedBlockingQueue<>(capacity);
// 启动漏出线程,固定速率 2 请求/秒
startLeaking(2);
}
private void startLeaking(int ratePerSecond) {
new Thread(() -> {
while (true) {
try {
Runnable task = queue.take(); // 阻塞直到有任务
task.run();
TimeUnit.MILLISECONDS.sleep(1000 / ratePerSecond);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}, "leaky-bucket-worker").start();
}
public boolean tryAcquire(Runnable task) {
return queue.offer(task); // 队列满时返回 false
}
}
注意: 漏桶的队列实现会占用内存,且任务可能长时间等待。在缓存一致性场景中,建议将超时时间纳入考量,避免请求在队列中等待过久导致缓存数据已失效。
2.4 滑动窗口算法(Sliding Window)
2.4.1 原理与数学模型
滑动窗口将固定时间窗口(如 1 秒)细分为 N 个小格子(如 10 个 100ms 格子)。每个格子独立计数,窗口整体滑动时,丢弃过期格子,加入新格子。相比固定窗口,滑动窗口能有效避免 “窗口边界突发” 问题。
- 关键参数: 窗口大小
W(秒)、格子数N、阈值T(请求数) - 精度: 格子越多,精度越高,内存开销也越大
- 优势: 无临界突变,适合秒杀等瞬时高并发场景
| 对比项 | 固定窗口(1s) | 滑动窗口(10格) |
|---|---|---|
| 边界 0.9s~1.1s 突发 | 可能翻倍通过 | 被精确限制 |
| 内存开销 | 1 个计数器 | N 个计数器 |
| 实现复杂度 | 低 | 中 |
| 适用场景 | 一般 API 限流 | 热点 Key、秒杀 |
2.4.2 Spring Boot 实现(基于环形数组)
import org.springframework.stereotype.Component;
import java.util.concurrent.atomic.AtomicIntegerArray;
@Component
public class SlidingWindowLimiter {
private final int windowSize; // 窗口大小(秒)
private final int slotCount; // 格子数
private final int threshold; // 阈值
private final AtomicIntegerArray slots;
private volatile long startTime; // 窗口起始时间戳(毫秒)
public SlidingWindowLimiter() {
this.windowSize = 1; // 1 秒窗口
this.slotCount = 10; // 10 个格子,每个 100ms
this.threshold = 100; // 每秒最多 100 请求
this.slots = new AtomicIntegerArray(slotCount);
this.startTime = System.currentTimeMillis();
}
public synchronized boolean tryAcquire() {
long now = System.currentTimeMillis();
long elapsed = now - startTime;
// 如果超过窗口大小,重置整个窗口
if (elapsed >= windowSize * 1000L) {
startTime = now;
for (int i = 0; i < slotCount; i++) {
slots.set(i, 0);
}
}
// 计算当前格子索引
int slotIndex = (int) ((elapsed / (windowSize * 1000L / slotCount)) % slotCount);
// 统计窗口内总请求数
int total = 0;
for (int i = 0; i < slotCount; i++) {
total += slots.get(i);
}
if (total >= threshold) {
return false; // 限流
}
slots.incrementAndGet(slotIndex);
return true;
}
}
缓存一致性增强: 滑动窗口可与缓存 Key 绑定,实现 Key 级别限流。例如:
cacheKey:user:12345 在 1 秒内最多访问 50 次,防止单个热点 Key 打爆缓存节点。
2.5 三种算法选型决策树
在实际项目中,应根据流量特征和缓存一致性要求选择算法:
- 需要允许突发 + 缓存读多写少 → 令牌桶(Guava RateLimiter)
- 需要强制平滑 + 缓存写/数据库保护 → 漏桶(阻塞队列)
- 需要高精度 + 热点 Key 防护 → 滑动窗口(环形数组)
- 分布式场景 + 统一配置 → 基于 Redis + Lua 的令牌桶或滑动窗口
最佳实践: 在 Spring Boot 项目中,建议将限流器封装为注解 + AOP 切面,通过
@RateLimit(algorithm = "token-bucket", rate = 10, capacity = 20) 声明式使用,便于统一治理和监控。