不能每次请求都新建rate.Limiter,因其会重置令牌桶状态导致限流失效,且高频分配加剧GC压力;必须按IP或userID分桶,用sync.Map等线程安全结构缓存复用实例,多实例部署时需Redis+Lua实现跨节点原子限流。

直接用 rate.Limiter 做全局单桶限流,在高并发下会成为瓶颈,且无法隔离恶意 IP;必须按请求维度(如 IP 或 user_id)分桶,并用线程安全结构缓存各桶实例,否则限流形同虚设。
为什么不能每次请求都 new 一个 rate.Limiter
因为 rate.Limiter 内部依赖时间戳和原子计数器,新建实例等于重置状态——每个请求都拿到一个“满桶”,完全失去限流意义。更严重的是,高频请求下频繁分配对象会加剧 GC 压力,实测 QPS 超过 5k 后内存分配速率翻倍。
-
rate.NewLimiter(rate.Every(100*time.Millisecond), 5)必须复用,不能在中间件闭包里每次调用时创建 - 所有 goroutine 共享同一个
*rate.Limiter实例才有效,但共享又导致不同 IP 相互干扰 - 正确做法是:用键(如 IP)查缓存 → 命中则复用,未命中则新建并存入 → 缓存本身需线程安全
IP 分桶限流必须用 sync.Map 而非 map + mutex
高并发场景下,用普通 map 加 sync.RWMutex 容易因读写锁争用拖慢整体吞吐。Echo 默认每秒可处理数万请求,而 mutex 在热点 key(如爬虫集中打某个接口)下会退化成串行执行。
Echo框架 5.1.0 版本源码包下载,适合关注 RealIP 行为变化、StartConfig.Listener、NewDefaultFS 和观测性中间件入口的开发团队。
-
sync.Map对读多写少场景做了优化,Load是无锁操作,Store也只在首次写入时加锁 - 键建议用
c.RealIP()而非c.Request.RemoteAddr,前者自动解析X-Forwarded-For,更适配 Nginx/ELB 等反向代理环境 - 注意:不要对 IP 做字符串拼接或正则清洗后再当 key,避免额外分配,直接用原始字符串即可
多实例部署时 Redis + Lua 是唯一可靠方案
单机 sync.Map 只能保障本进程内一致性,K8s 水平扩缩容或机器重启后桶状态丢失,会导致限流失效甚至突增流量击穿下游。
立即学习“go语言免费学习笔记(深入)”;
- 必须用 Redis 的
EVAL执行 Lua 脚本,保证“读桶、判断、更新”三步原子性 - 推荐脚本逻辑:用
INCR计数 +EXPIRE设置窗口期,比用PTTL+GETSET更省网络往返 - Go 侧不要自己拼 Lua,直接用
redis.UniversalClient.Eval(ctx, script, keys, args...),key 为"rate:ip:" + ip,args 包含窗口秒数与最大请求数
真正难的不是写限流逻辑,而是决定「桶的粒度」和「窗口的边界」:按 IP?按 JWT 中的 user_id?还是业务 ID(如订单号前缀)?这些选择直接影响攻击面和误伤率,上线前必须用真实流量压测验证,而不是只看本地 ab 或 hey 结果。

















