golang.org/x/time/rate.Limiter不是经典令牌桶,而是滑动窗口漏桶;其burst参数表示最大允许延迟时间(burst÷rate),而非物理桶容量,底层不维护令牌计数器,而是通过时间戳推算最早可放行时间。

golang.org/x/time/rate 不是令牌桶,是滑动窗口漏桶;真要按 RFC 2697 实现经典令牌桶,得手写或换库。
为什么 rate.Limiter 的 burst 不是桶容量
调用 rate.NewLimiter(10, 5) 时,很多人默认 5 是“最多攒 5 个令牌”,但实际它代表“最多允许延迟 500ms(即 5 ÷ 10)”。底层不维护 tokens 计数器,而是每次调用 Allow() 或 ReserveN() 时,计算“从现在往前推 500ms 内能放行多少请求”。这导致:
- 无法跨 goroutine 安全读取剩余令牌数 ——
limiter.Tokens()不存在 - 无法动态调整速率(
SetLimit()只改 rate,burst 不可变) - 时间戳若被截断(如用
time.Now().Unix()),会因精度丢失造成窗口错位、误放行
漏桶(uber-go/ratelimit)为何输出更稳
uber-go/ratelimit 是阻塞式漏桶,核心逻辑是 Take() 返回下一个允许执行的时间点。它不缓存请求,也不排队,而是让调用方主动 sleep 到该时刻。这意味着:
- 无论并发多高,实际处理节奏严格由
rate控制,输出 jitter 极小 - 没有“桶满溢出”概念,也无所谓 burst,天然适合保护下游脆弱服务(如 MySQL 连接池)
- 不支持突发 —— 即使前 10ms 没请求,第 11ms 来 10 个,它仍按固定间隔逐个放行
- 若业务不能接受阻塞(比如 HTTP handler 不想卡住协程),就得包装成非阻塞检查 + 立即拒绝
什么时候必须自己实现令牌桶
当需要以下任一能力时,rate.Limiter 和 ratelimit 都不够用:
立即学习“go语言免费学习笔记(深入)”;
- 实时暴露剩余令牌数给监控或前端(如响应头
X-RateLimit-Remaining) - 支持透支(borrowing):允许短期超限,后续用更慢速率偿还
- 与 Redis 联动水位(例如本地桶 + Redis 全局计数双校验)
- 纳秒级单调时钟保障(
runtime.nanotime()),避免 NTP 回拨导致 token 暴增
手写关键不是加锁,而是用 atomic.CompareAndSwapInt64 原子更新 tokens 和 lastRefill,并统一用纳秒做时间差计算 —— 浮点累加或秒级时间戳在高 QPS 下几小时内就会漂移几十个 token。
选型真正取决于流量模式,不是库名气
电商查询接口适合 rate.Limiter:能扛零点突发,且多数场景只需“平均限速 + 小爆发容忍”;支付回调接口更适合 ratelimit:必须确保每秒只打 3 次 DB,不能因上游抖动就突然连发 5 次;而秒杀库存扣减若需精确水位感知和跨节点协同,就得上 Redis + Lua 的滑动窗口,或者自研带原子状态同步的令牌桶。别被“标准库”三个字带偏 —— 它只是够用,不是万能。


















