限流:令牌桶与漏桶的实际差别
令牌桶允许突发,漏桶把流量削成恒定速率。选错的典型症状是正常用户被限、或者限流形同虚设,取决于你的下游能不能吃突发。
限流的两个经典模型常被当成同义词,但它们的输出形状完全不同:一个保留突发,一个削平突发。
令牌桶:允许突发
桶里按固定速率累积令牌,容量为 C。请求来了取一个令牌,取不到就限流。
- 速率 R 决定长期平均吞吐
- 容量 C 决定能容忍多大的突发
空闲一段时间后桶会积满,此时可以瞬时放行 C 个请求。这对「页面加载同时发 20 个请求」这类正常行为是必要的 —— 漏桶会把它们摊平,用户看到的是逐条加载。
漏桶:恒定输出
请求先进队列,再以固定速率流出。队列满则丢弃。
输出速率恒定,代价是引入了延迟:请求必须在队列里等待。对下游保护更彻底,但用户体验会退化。
对照
| 维度 | 令牌桶 | 漏桶 |
|---|---|---|
| 输出速率 | 可变,最高到 C | 恒定 |
| 是否允许突发 | 允许 | 不允许 |
| 引入延迟 | 否 | 是 |
| 适合保护 | 自己有弹性的服务 | 脆弱的下游 |
该选哪个
问一个问题:下游能不能消化突发?
| 场景 | 选择 | 理由 |
|---|---|---|
| 用户 API 网关 | 令牌桶 | 正常用法就有突发 |
| 调用第三方付费接口 | 漏桶 | 对方按秒计费,突发无意义 |
| 保护数据库连接池 | 漏桶 | 突发会耗尽连接 |
| 单用户频率限制 | 令牌桶 | 不该惩罚短时密集操作 |
分布式下的坑
单机内存计数在多实例部署下等于把限额乘以实例数。要么用共享存储(Redis 的 INCR + 过期),要么给每个实例分配 限额 / 实例数。
Redis 方案要注意原子性:GET 再 SET 会漏掉并发。用 INCR 配合 EXPIRE,或者一段 Lua 脚本一次完成。
返回什么状态码
被限流时返回 429 Too Many Requests,并带上 Retry-After 告诉客户端等多久。不带 Retry-After 的 429 会让客户端盲目重试,等于自己放大流量。
令牌桶管的是「长期平均 + 突发预算」,漏桶管的是「常量输出」。先确定下游能吃哪种,再选模型。

评论
…