rate.Limiter 不能直接控制并发数,因其限制的是单位时间请求数(QPS)而非瞬时并发;要控并发需设填充速率为0(如 rate.NewLimiter(0, 5)),此时桶容量即最大并发数,令牌永不恢复。

rate.Limiter 为什么不能直接控并发数
它控制的是单位时间内的请求数(QPS),不是瞬时并发数。默认初始化 rate.NewLimiter(10, 5) 表示“每秒补 10 个 token,桶最多存 5 个”,这只能约束平均速率,无法阻止 5 个 goroutine 同时拿到 token 并发执行。
要真正限制并发,请把填充速率设为 0:rate.NewLimiter(0, 5)。此时桶容量(第二个参数)就是最大并发数,且令牌永不恢复——没被拿走的 token 不会自动归还,后续请求必须等前面的完成才可能获得许可。
- 别用
rate.Every(time.Second)初始化来控并发,那只是设置填充间隔,和并发无关 - 高并发下
Wait()会排队阻塞,天然形成 FIFO 队列,但不带超时;务必套context.WithTimeout(r.Context(), 100*time.Millisecond) - 若需主动拒绝而非等待,改用
ReserveN(now, 1)判断res.OK(),再根据res.Delay()决定是否返回Retry-After
按 key 复用 Limiter 实例的正确姿势
全局单例只适合全局限流;多数场景需 per-user、per-ip 或 per-path 控制,这时必须缓存实例,但不能用普通 map —— 高并发读写会 panic。
推荐用 sync.Map 存储:var limiters sync.Map,key 命名为 "user:" + userID 或 "ip:" + realIP,避免冲突。
立即学习“go语言免费学习笔记(深入)”;
在 Go 中使用 google/wire 实现编译时依赖注入——wire.NewSet、wire.Build、wire.Bind(接口→实现)、wire.Struct、wire.Value、wire.Interface
- 每次取 key 前先
load, ok := limiters.Load(key),未命中则新建并Store - 新建时用
rate.NewLimiter(rate.Limit(100), 20),burst 设为 20 而非 1,否则突发流量全被拒 - 闲置 key 要清理:对每个新 key 启动
time.AfterFunc(30 * time.Minute, func() { limiters.Delete(key) }) - 健康检查路径(如
/health)必须跳过限流,否则 k8s probe 会反复失败
分布式部署时 rate.Limiter 失效的根本原因
它只在当前进程内存里维护状态。5 个 Pod 各跑一个 rate.Limiter(100, 200),入口总流量就是 500 QPS —— 这不是 bug,是设计使然。
跨实例限流必须引入外部存储,最常用的是 Redis + Lua 原子脚本。关键点不在“用 Redis”,而在“如何提取唯一 key”和“如何降级”:
- key 必须精确到业务维度,比如
"rate:user:123",而不是笼统的"rate:api" - Lua 脚本要实现原子的“读+增+过期”三步,避免竞态;Redis 连接超时或返回 error 时,应 fallback 到本地
rate.Limiter或直接放行(视业务容忍度而定) - 别依赖
Allow()的返回值做决策后继续执行 handler —— 常见错误是if !limiter.Allow() { http.Error(...) }然后不管不顾往下走
HTTP 中间件里嵌入限流的易错细节
限流逻辑必须在业务 handler 执行前拦截,且拒绝后立即终止流程。用 Gin 或 net/http 时,常见误操作是忘了 c.Abort() 或漏了 context 传递。
正确写法示例(Gin):
func RateLimitMiddleware(limiter *rate.Limiter) gin.HandlerFunc {
return func(c *gin.Context) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 100*time.Millisecond)
defer cancel()
if err := limiter.Wait(ctx); err != nil {
c.JSON(429, gin.H{"error": "too many requests"})
c.Abort() // 必须调用,否则后续 handler 仍会执行
return
}
c.Next()
}
}-
Wait()传入的 ctx 必须来自c.Request.Context(),否则 cancel 和 timeout 不生效 - 别在
defer里调用res.Cancel()—— 如果 handler panic 且已执行部分 DB 操作,取消 reservation 会导致 token 白白浪费 - 若需返回标准限流头(
X-RateLimit-Limit等),得用limiter.ReserveN(time.Now(), 1)预占再算剩余,直接减法在并发下不准
实际部署时,最常被忽略的是时钟漂移问题:Kubernetes Pod 在 CPU 资源受限时,time.Now() 可能滞后,导致限流变松。这不是代码能解决的,得靠监控 + 主动告警。

















