4. 限流算法:令牌桶、漏桶、滑动窗口、计数器
限流这个话题,说白了就是控制流量别把系统冲垮。我在生产环境摸爬滚打这些年,踩过不少坑,今天把这四种主流算法掰开揉碎讲清楚。
4.1 计数器算法
计数器是最朴素的限流方式。想象一个计数器,每来一个请求就加1,到时间窗口末尾清零。如果计数超过阈值,直接拒绝。
个人经验:我当年接手过一个秒杀系统,用的是固定窗口计数器。结果发现流量在窗口边界集中爆发——比如0分59秒999毫秒来了一波,1分0秒又来一波,瞬间打穿数据库。这就是著名的“临界问题”。
// 伪代码:固定窗口计数器
val count = atomicIncrement()
if (count > THRESHOLD) {
if (currentTime - windowStart > WINDOW_SIZE) {
// 重置窗口
windowStart = currentTime
count = 1
} else {
return REJECT
}
}
return ACCEPT
适用场景:对精度要求不高、流量均匀的场景,比如简单的API调用频率控制。
4.2 滑动窗口算法
滑动窗口把时间窗口切成多个小格子,每个格子独立计数。移动窗口时,丢弃旧格子,累加当前格子和之前几个格子的计数。这样就解决了计数器“临界问题”。
注意:格子数量影响精度。格子越多,内存消耗越大。我曾经把一个窗口切成1000个格子,结果内存炸了。后来改成200格,精度够用,内存也稳。
| 格子数 | 误差率 | 内存占用(1k QPS) |
|---|---|---|
| 10 | ~10% | ~4KB |
| 100 | ~1% | ~40KB |
| 1000 | ~0.1% | ~400KB |
适用场景:需要较精确限流,能接受一定内存开销的场景,比如接口防刷。
4.3 漏桶算法
漏桶像一个有底部小洞的水桶。请求像水一样倒进去,桶容量有限,多出来的溢出丢弃。桶底以恒定速率流出请求。说白了,漏桶强制“出水”速率恒定,不管进水有多猛。
核心特性:漏桶把突发流量“削平”成均匀的流。适合下游服务处理能力固定的场景,比如写入数据库的请求。
我见过一个案例:某团队用漏桶保护Redis写入,结果业务高峰期大量请求被丢弃,用户反馈数据丢失。原因是漏桶不能处理突发——即使用户短时间内发10个请求,只要超过桶容量,直接丢掉9个。这可能是业务无法接受的。
适用场景:对流量整形要求严格、不允许突发的场景。
4.4 令牌桶算法
令牌桶是另一种思路。系统以恒定速率往桶里放令牌,每个请求需要拿走一个令牌才能通过。桶里可以积累一些令牌,允许一定程度的突发。我最初选型时就偏爱令牌桶,因为它既能平滑流量,又能应对短期峰值。
// 令牌桶关键参数(Guava RateLimiter风格)
rate = 10 // 每秒产生令牌数
burst = 20 // 桶容量,最多允许20个突发请求
tokens = 0 // 当前令牌数
lastRefillTime = now()
def acquire():
refill()
if tokens >= 1:
tokens -= 1
return true
else:
return false
def refill():
now = currentTime()
elapsed = now - lastRefillTime
tokens = min(burst, tokens + elapsed * rate)
lastRefillTime = now
避坑指南:我曾经把令牌桶的突发容量设得太大,结果一个促销活动直接把下游压死。记住:突发虽好,但要确保下游能扛得住这个“突发”。
适用场景:需要允许短时间突发,又能平滑整体流量的场景,如API网关、秒杀系统。
4.5 适用场景对比
把四种算法摆在一起看,差别就清楚了。我直接画个图,你再对照下面的表格理解。
看明白了吗?我再给你一张表,把关键差异列清楚。
| 对比维度 | 计数器 | 滑动窗口 | 漏桶 | 令牌桶 |
|---|---|---|---|---|
| 流量整形 | 无 | 无 | 强制平滑 | 允许突发 |
| 精度 | 低(有临界问题) | 高(可调) | 中 | 中 |
| 内存消耗 | 低 | 中~高 | 低 | 低 |
| 对突发响应 | 无法处理边界突发 | 能控制突发总量 | 丢弃超出桶容量的所有请求 | 允许一定程度的突发 |
| 典型实现 | AtomicLong + 时间戳 | 环形数组 + 槽位 | 队列 + 固定消费速率 | 令牌桶 + 定时补充 |
| 推荐框架 | 自行实现 | Sentinel滑动窗口 | Guava SmoothWarmingUp(近似) | Guava RateLimiter |
实际选型时,我一般这样判断:如果下游能力稳定,选漏桶;如果有突发但要求平滑,选令牌桶;如果只是简单防刷,滑动窗口或计数器够用。别一上来就上复杂算法,先想清楚你的业务到底需要什么。
小结:四种算法没有银弹。计数器最轻量,滑动窗口最精确,漏桶最平滑,令牌桶最灵活。你根据流量模型、下游能力、允许的突发程度来选。我在多家公司做架构评审时,发现80%的场景用令牌桶或滑动窗口都能覆盖。