单机限流用 rate.Limiter 足够,但需按请求维度分桶、调整认证顺序、避免锁和内存泄漏;time.Sleep 和全局 sync.Mutex 无效且有害,map 计数必须带时间戳并定期清理;Redis+Lua 实现分布式限流需原子操作与合理过期策略。

单机限流用 rate.Limiter 就够,但直接套在所有路由上等于没限——关键得按请求维度分桶、错开认证顺序、避开锁和内存泄漏陷阱。
为什么 time.Sleep 和全局 sync.Mutex 不能当限流器
前者只阻塞当前 goroutine,100 个并发请求进来,每个都 time.Sleep(100 * time.Millisecond),实际 QPS 还是爆表;后者在高并发下锁争用严重,sync.Mutex 成为性能瓶颈,QPS 上不去还容易超时。更隐蔽的问题是:用 map[string]int 存计数但不清理过期 key,跑几天内存就涨到几个 GB。
- 别在 handler 里写
time.Sleep模拟限流,它不是控制频率,只是加延迟 - 避免用
sync.Mutex包裹整个计数逻辑,改用sync.Map或官方rate.Limiter - 如果自己实现计数器,必须带时间戳 + 定期清理协程,或直接用 Redis
按 IP 或用户 ID 分桶的 rate.Limiter 实操要点
rate.Limiter 本身线程安全,但多个路径/用户共用一个实例会互相干扰——比如把登录接口和公开文档页绑在同一个限流器上,用户刷文档就会导致自己登不上录。
- key 设计要带区分字段:
rate:ip:<code>ctx.ClientIP():v1/login 或rate:uid:<code>userID:v2/report - 用
sync.Map缓存不同 key 对应的*rate.Limiter,避免重复初始化 - burst 值别设太小(如 1),否则重试或前端并发请求容易被误杀;建议设成平均 QPS 的 2–3 倍
- 注意
ctx.ClientIP()可能被伪造,务必提前调用gin.SetTrustedProxies()或 Nginx 设置X-Real-IP
Redis + Lua 实现分布式限流的不可绕过细节
多 Pod 部署时,rate.Limiter 失效是必然的。但直接用 INCR + EXPIRE 两步走会竞态:两个请求同时读到 0,都执行 INCR,结果计数变成 2,限流阈值翻倍。
立即学习“go语言免费学习笔记(深入)”;
- Lua 脚本必须原子执行:先
GET当前值,判断是否>= limit,再INCR并EXPIRE,三步合一 - key 过期时间要略大于窗口时长(比如 60 秒窗口设 65 秒过期),防止刚好过期瞬间出现计数归零漏洞
- 不要在中间件里解析 JWT 或查 DB 获取用户 ID——认证中间件必须前置,只从
ctx.Get("user_id")读,否则拖慢所有请求 - 错误响应别用默认
{"error":"rate limit exceeded"},统一返回429+Retry-Afterheader,前端才好自动退避
gin-contrib/rate 为什么不该直接用
它底层虽用 rate.Limiter,但存储层固定在内存,没提供 Redis 替换入口;错误响应体硬编码,没法适配公司统一返回格式;而且它的 NewRateLimiter 初始化后无法动态调整 limit/burst,上线后想调参只能重启服务。
- 如果你用的是 Gin,别 import
gin-contrib/rate就往中间件链里塞 - 它的 “支持 Redis” 是 demo 级 stub,没做 Lua 原子性封装,不能用于生产
- 真要省事,不如直接抄一段 20 行以内的 Redis+Lua 中间件,可控性高得多
真正难的不是写限流逻辑,而是 key 怎么设计、认证和限速谁先谁后、burst 值怎么跟业务重试策略对齐——这些地方一错,限流要么形同虚设,要么把正常用户全拦在外面。


















