应使用 rate.limiter 而非手写令牌桶,因其无锁、高精度、并发安全;多维度限流需用 sync.map 缓存独立实例并定期清理过期项;rate.every 与 burst 参数须正确配置,避免硬编码,动态更新时应平滑切换。

用 rate.Limiter 而不是手写令牌桶
Go 官方 golang.org/x/time/rate 提供的 rate.Limiter 是无锁、高精度、低内存开销的生产级实现,比自己手写或套用第三方包更稳。它内部已处理好并发安全、时间漂移、令牌生成精度等问题。手写容易出错:比如用 time.Now().Unix() 算间隔,在高并发下会漏统计;又比如没加锁导致 tokens 字段竞争,结果限流完全失效。
直接用 rate.NewLimiter 即可,关键不是“怎么造轮子”,而是“怎么用对”。
sync.Map 缓存多维度限流器,但必须带过期清理
按 IP、路径、用户 ID 等维度限流时,不能共用一个 *rate.Limiter 实例——否则 A 用户被限,B 用户也一起 429。必须为每个维度创建独立实例,并用 sync.Map 缓存。
- key 必须统一为
string,例如拼成ip:192.168.1.100或user:123:path:/api/v1/order,避免结构体或指针作 key 导致无法命中 - 不加过期清理,内存只增不减;建议用后台 goroutine 每分钟扫描,清理空闲超 5 分钟的 entry
- 读多写少场景下,
sync.Map比map + sync.RWMutex性能更好,但别在Allow()前对 map 加锁——rate.Limiter本身并发安全,锁只该用在 map 的读写上
rate.Every 和 Burst 参数别填反
常见错误是把 QPS 数字直接当 rate.Every 参数:比如想限 10 QPS,却写成 rate.Every(time.Second),再靠 Burst 补——这是错的。正确写法是 rate.Every(time.Second / 10),表示每 100ms 放一个令牌。
Burst 是桶容量,不是当前剩余令牌数;它决定允许的突发量,建议设为速率的 2–3 倍(如 10/s → Burst=20),否则稍有请求抖动就全拒。
Allow() 是非阻塞判断,适合快速拒绝;ReserveN() 可获取预留时间,适合做排队或延迟响应,但多数接口限流用 Allow() 就够了。
动态配置要外置,别硬编码 rate.NewLimiter(100, 200)
灰度发布、运营活动、API 版本迭代时,硬编码限流参数会拖慢上线节奏。必须把 rate 和 Burst 外置,比如从配置中心拉取,或通过 HTTP 接口热更新。
更新时不要销毁旧 *rate.Limiter 后立刻新建——新实例刚初始化时桶是满的,会导致短时放行激增。稳妥做法是:保留旧实例继续服务,用新参数创建新实例,逐步切换 key 映射,等旧实例自然淘汰。
Header 如 X-RateLimit-Remaining 仅作参考,客户端不可信,别依赖它做逻辑分支;真要暴露剩余数,得调 r.ReserveN(c, 1) 后查 .OK 和 .Delay,但代价略高,慎用。
最易被忽略的是过期清理和 key 拼接一致性——缓存不清理,跑一周后内存涨几 GB;key 在多处硬编码拼接,一处漏加前缀或大小写不一致,就会导致某个维度完全不生效。











