
Go 限流选 golang.org/x/time/rate.Limiter 还是自己写固定窗口?
直接说结论:90% 的内部微服务接口限流,用 rate.Limiter(令牌桶)更稳;只有极少数需要「整秒/整分钟统计」且允许误差的场景,才考虑固定窗口。
原因很简单:rate.Limiter 是 Go 官方维护、经过压测验证的内存级限流器,支持平滑突发请求,且无时间窗口跳跃问题;而手写固定窗口容易在并发下漏计数、跨窗口重复重置,还得多维护一个 map 或 Redis。
- 固定窗口在每秒切换瞬间会出现“脉冲”——前一窗口末尾 + 新窗口开头可能同时放行两倍请求
-
rate.Limiter的Allow/Reserve方法是原子的,底层用sync/atomic控制令牌,无锁路径快 - 如果你的限流粒度是「每秒 100 次」,但实际请求有波峰(比如 500ms 内打来 80 次),固定窗口会直接拒绝后 20 次;
rate.Limiter可凭剩余令牌缓冲,体验更平滑
为什么 rate.Limiter 的 burst 参数不能设太大?
burst 表示令牌桶最大容量,它不是“并发上限”,而是“可累积的突发请求数”。设得过大,等于变相放宽限流,尤其在服务刚启动或低流量后突增时,会一次性倾泻大量请求到下游。
- 例如
rate.NewLimiter(rate.Every(100*time.Millisecond), 5):每 100ms 补 1 个令牌,桶最多存 5 个。若 burst=1000,那服务空闲 10 秒后,桶里就攒了 1000 个令牌——第一波请求进来全放行,相当于没限流 - 真实微服务中,
burst建议 ≤ 平均 QPS × 0.5 秒(如 QPS=200,burst≤100),既容许合理抖动,又防雪崩传导 - 注意:
burst和limit都是 int 类型,超math.MaxInt32会 panic,但一般不会踩到
固定窗口真要上,怎么避开 map 并发读写 panic?
很多人用 map[string]int 存每个窗口的计数,然后开 goroutine 定时清零——这是典型错误。Go 的 map 默认不支持并发读写,一碰就 fatal error: concurrent map writes。
立即学习“go语言免费学习笔记(深入)”;
- 别用裸
map,改用sync.Map(但注意:它的LoadOrStore不保证原子性累加,仍需额外锁) - 更稳妥的是每个窗口键配一个
sync.Mutex,或者直接上atomic.Int64+ 预分配窗口槽位(比如只保留最近 2 个时间窗口的计数器,用数组 + 原子索引切换) - 如果必须分布式(跨实例),别自己实现——直接走 Redis 的
INCR+EXPIRE,用 Lua 脚本保证原子性,否则本地计数毫无意义
HTTP 中间件里嵌 rate.Limiter,为什么有时限不住?
常见原因是把 rate.Limiter 实例定义在了 handler 函数内,导致每次请求都新建一个 limiter,完全失效。
- 正确做法:limiter 必须是包级变量或依赖注入的单例,例如
var apiLimiter = rate.NewLimiter(rate.Limit(100), 10) - 另一个坑:没区分用户/租户/IP。所有请求共用一个 limiter,会导致大客户挤占小客户额度。应按 key 构建 limiter 池(如用
sync.Map缓存*rate.Limiter),但注意控制池大小,避免内存无限增长 - 测试时用
time.Sleep模拟请求间隔容易误判——Go 的 timer 在高负载下有延迟,建议用runtime.Gosched()或真实压测工具(如 wrk)验证
真正难的不是选算法,而是厘清「谁该被限」「按什么维度限」「失败后怎么降级」。令牌桶和固定窗口只是工具,背后是你的服务契约意识。










