单个 rate.limiter 无法按用户 id 区分限流,因其无 key 设计,所有请求共用同一令牌桶,导致用户间相互干扰;需用 sync.map 按 user_id 懒加载独立限流器,并配 ttl 清理防泄漏。

不能只用一个 rate.Limiter 实例做用户 ID 限流,否则所有用户共享同一桶,完全失效。
为什么单个 rate.Limiter 无法按用户 ID 区分限流
标准库的 rate.Limiter 是无 key 的单实例设计:它只认“现在能不能放行”,不认“这是哪个用户”。你在中间件里直接调 limiter.Allow(),不管请求头里是 user_id=1001 还是 user_id=9999,都往同一个桶里扣令牌——结果就是高权限用户被低频用户拖累,或恶意用户挤占他人额度。
常见错误现象包括:
- 登录接口被刷爆,但其他用户也突然 429(因为共用桶)
- 后台管理员调接口频繁失败(和游客混在同一个限流器里)
- 日志显示“用户 A 被限流”,实际查发现 A 根本没发起请求(桶状态被 B/C/D 污染)
用 sync.Map + 懒加载构建用户维度限流池
核心是为每个 user_id 维护独立的 *rate.Limiter 实例,并保证并发安全、不重复创建、不内存泄漏。
实操要点:
- key 使用标准化字符串,如
"u_" + userID(避免空值、特殊字符冲突) - 用
limiters.LoadOrStore(key, newLimiter)替代先Load再Store,防止两个 goroutine 同时新建实例 - burst 值按接口容忍度设:登录接口建议
burst=5,查询类可设burst=50 - 未登录请求统一用
"anonymous"作为 user_id,而不是空字符串或"null"
示例片段:
Colly 是一个用于 Go 语言的快速开源爬取和爬虫框架。它适用于从简单的页面提取到异步爬虫处理大量页面集合,支持请求回调和结构化解析。
var userLimiters sync.Map
func getLimiterForUser(userID string) *rate.Limiter {
if userID == "" {
userID = "anonymous"
}
key := "u_" + userID
if lim, ok := userLimiters.Load(key); ok {
return lim.(*rate.Limiter)
}
// 每分钟最多 60 次,突发允许 10 次
lim := rate.NewLimiter(rate.Every(time.Minute/60), 10)
userLimiters.Store(key, lim)
return lim
}
必须加 TTL 清理,否则内存持续增长
攻击者构造大量随机 user_id(如 u_abc123、u_def456)会不断写入 sync.Map,没有清理机制就等于内存泄漏。
推荐做法:
- 每次访问时更新最后使用时间(例如用
time.Now()存到另一个sync.Map) - 启动一个后台 goroutine,每 30 秒扫描一次,删除超过 5 分钟未访问的 key
- 别依赖 GC ——
*rate.Limiter本身不持有大对象,但 map 的键值对不会自动回收
注意:不要用 time.AfterFunc 为每个 key 单独设过期,开销太大;集中扫描更可控。
Allow() vs Wait() 在用户限流中的实际选择
两者语义差异直接影响用户体验和系统稳定性:
-
Allow()立即返回 bool,适合 Web 接口快速失败(返回429 Too Many Requests) -
Wait()会阻塞,**必须传带 timeout 的 context**,例如context.WithTimeout(r.Context(), 100*time.Millisecond);裸用Wait(context.Background())可能导致请求永久挂起 - 批量操作(如一次提交 10 条记录)要用
AllowN()或WaitN(),但注意:这不是并发控制,而是“这 10 次必须一起通过或一起拒绝”
真实场景中,绝大多数 API 应优先用 Allow() —— 用户不想等,系统也不该卡住。
真正难的不是写对一个 Allow(),而是让成千上万个 user_id 各自拥有稳定、隔离、可回收的限流器。key 的拼法、map 的清理节奏、burst 的取值粒度,三者稍有偏差,就会在流量高峰时暴露为偶发超限或大面积误杀。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










