单机限流用 rate.Limiter 需全局复用或按 key 缓存(如 sync.Map),避免每次请求新建实例;HTTP 中间件中应使用带超时的 Wait(ctx) 并跳过健康检查;多实例必须用 Redis+Lua 实现分布式限流,注意 key 精确提取与故障降级。

单机限流用 golang.org/x/time/rate.Limiter 就够,但直接 new 实例、硬编码路径、忽略并发竞争,这三类写法会让限流形同虚设。
为什么每次请求都 new rate.Limiter 会失效
每个 rate.Limiter 实例维护独立的令牌桶状态。如果在 HTTP handler 里写 limiter := rate.NewLimiter(10, 5),那每来一个请求就新建一个桶——100 个并发请求等于 100 个桶各自放行 10 QPS,实际总流量是 1000 QPS,完全失去控制。
- 正确做法是全局复用(如全局限流)或按 key 缓存(如 per-user)
- 缓存必须用
sync.Map或带读写锁的 map,避免高并发下 panic - key 命名要带区分维度,例如
"user:" + userID、"ip:" + realIP、"path:" + r.URL.Path - 别忘了清理闲置 key:对每个新 key 启动
time.AfterFunc(30 * time.Minute, func() { syncMap.Delete(key) })
HTTP 中间件里怎么安全调用 Allow() 或 Wait()
Allow() 是非阻塞的,返回 false 后必须立刻中断后续逻辑,否则请求仍会执行;Wait(ctx) 虽能阻塞等待,但没设超时就会永久挂起。
- 别写
if !limiter.Allow() { http.Error(...) }然后继续往下走——这是常见误操作 - 推荐用
Wait(ctx),且 ctx 必须来自r.Context()并套一层context.WithTimeout(..., 100*time.Millisecond) - 若需返回标准限流头(
X-RateLimit-Limit等),得用limiter.ReserveN(now, 1)预占再算剩余,直接减法在并发下不准 - 健康检查接口(如
/health)务必跳过限流,否则 k8s probe 会反复失败
多实例部署时为什么 rate.Limiter 不管用
它只在当前进程内存中生效,5 个 Pod 各自跑一个 rate.Limiter(100, 200),入口总流量就是 500 QPS —— 这不是 bug,是设计使然。
立即学习“go语言免费学习笔记(深入)”;
- 跨实例限流必须依赖共享存储,Redis 是事实标准,etcd 次之
- Redis 方案必须用 Lua 脚本封装
INCR+EXPIRE+GET,拆成多次网络调用会因竞态导致计数错误 - key 命名要细粒度,比如
"limit:svc:user-get:12345",而不是笼统的"user-get" - 别指望 “本地缓存 + 定时同步” 来模拟分布式效果,时钟漂移和网络延迟会让配额错乱
真正难的不是写通限流逻辑,而是让 key 提取不漏(比如 X-Real-IP 没配 TrustedProxies 就拿不到真实 IP),让配置热加载不 panic(reload 时 map 正在被读,又同时被写),还有 Redis 故障时的降级策略——这些细节不处理好,限流反而会成为故障放大器。


















