八股文解析
限流算法:令牌桶和漏桶有什么区别?
一句话结论
漏桶是恒定速率输出(强制平滑),令牌桶是允许突发流量(按需取令牌),前者重“整形”,后者重“限流”。
面试标准答法
1. 核心区别:整形 vs 限流
漏桶(Leaky Bucket):请求先进入一个固定容量的桶,桶底部以恒定速率漏水(即处理请求)。如果桶已满,新请求直接丢弃或排队。无论上游流量多猛,下游看到的永远是恒定速率。
令牌桶(Token Bucket):系统以固定速率向桶中放入令牌(Token),每个请求必须消耗一个令牌才能通过。桶有最大容量(burst size),允许瞬间积攒一批令牌,从而支持突发流量。
一句话记忆:漏桶管“出口速率”,令牌桶管“入口速率+突发上限”。
2. 机制细节拆解
漏桶(Leaky Bucket)
- 数据结构:FIFO 队列 + 定时器
- 核心参数:
capacity(桶容量)、rate(漏水速率,即每秒处理请求数) - 工作流程:
- 请求到达,尝试放入队列尾部
- 若队列已满(长度 ≥ capacity),拒绝(丢弃/返回 503)
- 定时器以
rate频率从队列头部取出请求处理
- 关键特性:
- 输出绝对平滑:无论输入多么不均匀,输出始终是匀速
- 无突发能力:即使桶是空的,也不能加速处理
- 实现简单:一个队列 + 一个定时器即可
令牌桶(Token Bucket)
- 数据结构:计数器(当前令牌数)+ 时间戳(上次补充时间)
- 核心参数:
capacity(桶容量,即最大突发量)、refill_rate(令牌补充速率) - 工作流程:
- 系统以
refill_rate向桶中添加令牌,直到桶满(容量为capacity) - 请求到达时,检查桶中是否有令牌:
- 有 → 取走一个令牌,请求通过
- 无 → 拒绝或等待
- 懒更新(Lazy Refill):不依赖定时器,而是在请求到达时计算
(当前时间 - 上次补充时间) × refill_rate,一次性补充令牌
- 关键特性:
- 支持突发:如果一段时间没有请求,桶会积满令牌,后续突发请求可一次性全部通过
- 平均限流:长期来看,平均速率被限制在
refill_rate,但短期允许峰值 - 实现高效:无定时器,纯计算,适合高并发场景
3. 对比表格
| 维度 | 漏桶(Leaky Bucket) | 令牌桶(Token Bucket) |
|---|---|---|
| 输出速率 | 恒定,完全平滑 | 允许突发,平均受限 |
| 突发处理 | 不支持,桶满即拒 | 支持,最多可突发 capacity 个请求 |
| 实现复杂度 | 低(队列+定时器) | 中(计数器+时间戳计算) |
| 适用场景 | 网络流量整形(Traffic Shaping)、保护下游脆弱系统 | API 网关限流、突发业务场景(如秒杀) |
| 典型参数 | capacity=1000, rate=100 req/s | capacity=200, refill_rate=100 req/s |
| 拒绝策略 | 桶满直接丢弃 | 令牌耗尽时等待或丢弃 |
| 内存占用 | O(capacity) 队列 | O(1) 计数器 |
| 代表实现 | Nginx limit_req(近似) | Guava RateLimiter(SmoothBursty)、Redis+Lua |
常见追问
| 追问 | 回答要点 |
|---|---|
| 为什么很多生产系统用令牌桶而不是漏桶? | ① 支持突发流量,用户体验更好;② 无需定时器,纯计算实现,性能更高;③ 参数语义更直观(容量=突发量,速率=平均速率);④ 漏桶的恒定输出对某些场景(如数据库写入)虽然友好,但会浪费上游空闲期的处理能力 |
| 令牌桶如何实现“预热”(Warm-up)? | 使用 SmoothWarmUp 模式:初始令牌速率慢,逐步加速到目标速率。实现上通过“冷因子”(coldFactor)控制斜率,Guava 中默认 coldFactor=3,即从 1/3 速率开始爬升 |
| Redis 实现令牌桶的原子性问题怎么解决? | 用 Lua 脚本 保证“取令牌+扣减”的原子性,或者用 Redis 4.0+ 的 RedLock 方案。核心是避免并发下超卖令牌。生产级方案:Redis + Lua(如 INCR + EXPIRE 实现滑动窗口限流) |
| 漏桶的“恒定速率”真的恒定吗? | 在分布式环境下,如果多个实例各自实现漏桶,全局速率不恒定。需要引入 集中式协调(如 Redis + Lua 保证全局速率),但会引入网络延迟。这也是漏桶在分布式场景下不如令牌桶常用的原因之一 |
面试回答模板(30 秒版)
延伸准备
1. 滑动窗口 vs 令牌桶(加分点)
- 滑动窗口(Sliding Window):按时间窗口计数,窗口内请求数超限则拒绝。优点是精确控制“每秒 N 次”,缺点是无法处理窗口边界突刺(如 59.9s 和 60.1s 各放 100 个)。
- 令牌桶:天然平滑突发,但“平均速率”不够精确(如 100 req/s 的桶,可能在 1s 内放 200 个,随后 1s 内全拒)。
- 加分表述:实际生产常组合使用——外层滑动窗口卡死峰值,内层令牌桶平滑流量。
2. 分布式限流的“一致性”问题
- 单机令牌桶是纯内存操作,但分布式场景需要全局计数器。方案有:Redis + Lua(推荐,原子性)、Sentinel 集群流控(Token Server 模式)、Nginx + etcd 配置同步。
- 关键挑战:网络开销(每次请求多一次 Redis RTT)、时钟漂移(如果依赖本地时间戳)、容灾(Redis 挂了怎么办——降级为本地限流或直接放行)。
3. 拒绝策略的工程化设计
- 令牌桶/漏桶只解决“是否放行”,不解决“拒绝后怎么办”。生产级方案:
- 排队等待(如 Kafka 削峰填谷)
- 快速失败(返回 429 + Retry-After 头)
- 降级兜底(返回默认值或走缓存)
- 优先级队列(高优先级请求先取令牌——可扩展为多桶方案)
想系统备战大厂大模型/Agent 开发?NiceOffer 提供 SDE+LLM 双轨 1v1 陪跑,合同保底 40w 年薪,文末扫码咨询。