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 适用场景对比

把四种算法摆在一起看,差别就清楚了。我直接画个图,你再对照下面的表格理解。

四种限流算法核心对比 计数器 固定窗口,简单易行 存在临界问题 滑动窗口 细分时间片,精度高 内存占用随精度上升 漏桶 恒定出水,平滑流量 不能处理突发 令牌桶 允许突发,可控速率 需合理配置桶容量 适用场景速览 ✅ 计数器:适合简单统计,对窗口边界不敏感的场景 ✅ 滑动窗口:适合需要精确限流的防盗刷场景 ✅ 漏桶:适合下游处理能力固定、不允许突发的场景(如写入磁盘) ✅ 令牌桶:适合需要允许突发、同时限制平均速率的场景(如API网关)

看明白了吗?我再给你一张表,把关键差异列清楚。

对比维度计数器滑动窗口漏桶令牌桶
流量整形无无强制平滑允许突发
精度低(有临界问题)高(可调)中中
内存消耗低中~高低低
对突发响应无法处理边界突发能控制突发总量丢弃超出桶容量的所有请求允许一定程度的突发
典型实现AtomicLong + 时间戳环形数组 + 槽位队列 + 固定消费速率令牌桶 + 定时补充
推荐框架自行实现Sentinel滑动窗口Guava SmoothWarmingUp(近似)Guava RateLimiter

实际选型时,我一般这样判断:如果下游能力稳定,选漏桶;如果有突发但要求平滑,选令牌桶;如果只是简单防刷,滑动窗口或计数器够用。别一上来就上复杂算法,先想清楚你的业务到底需要什么。

小结:四种算法没有银弹。计数器最轻量,滑动窗口最精确,漏桶最平滑,令牌桶最灵活。你根据流量模型、下游能力、允许的突发程度来选。我在多家公司做架构评审时,发现80%的场景用令牌桶或滑动窗口都能覆盖。