ratelimiter实例不能在gin中间件中每次新建,否则每个请求独占桶导致限流失效;必须复用——全局单例用于网关总限流,或用sync.map按ip/用户id等维度缓存,key需标准化并配定时清理,同时健康接口须跳过、redis多实例需lua原子脚本保障一致性。

RateLimiter 实例不能在中间件里每次 new
直接在 Gin 中间件函数里写 rate.NewLimiter(rate.Every(100*time.Millisecond), 10),等于每个请求都新建一个桶——100 并发进来,就创建 100 个独立桶,每个桶自己放行 10 QPS,实际吞吐变成 1000 QPS,完全失效。
必须复用:要么全局一个(适合网关总限流),要么按维度缓存(推荐)。缓存用 sync.Map,key 建议是 "ip:" + ip 或 "uid:" + userID,避免 IPv6 冒号干扰,先 strings.ReplaceAll(ip, ":", "-") 标准化。
- 认证接口要取
c.GetString("user_id"),但认证中间件必须注册在限流中间件之前,否则取不到值 - 未认证时 fallback 到
c.ClientIP(),且需提前配好engine.TrustedProxies,不然拿到的是 Nginx 内网地址 - 每个新 key 必须配清理:
time.AfterFunc(5 * time.Minute, func() { limiterMap.Delete(key) }),否则内存持续上涨
Allow / ReserveN / WaitN 怎么选
三个方法都消耗令牌,但语义和副作用完全不同,错用会导致前端卡死、重试失败或剩余数不准。
-
Allow():非阻塞,返回bool。适合登录防爆破这类“快速失败”场景,没令牌立刻c.AbortWithStatusJSON(http.StatusTooManyRequests, ...) -
ReserveN():返回*rate.Reservation,可调OK()和Delay()。适合需要提示“请等待 X 毫秒”的表单提交 -
WaitN():阻塞直到拿到令牌或 context 超时。绝不能裸用WaitN(context.Background(), n),必须传带 timeout 的 context,例如:ctx, cancel := context.WithTimeout(c.Request.Context(), 100*time.Millisecond) - 批量操作(如一次上传 3 个文件)要用
WaitN(ctx, 3),注意这不是允许 3 并发,而是“这 3 个请求必须同时放行或同时拒绝”
健康检查和内部路径必须跳过
不跳过 /health 或 /metrics 这类路径,K8s liveness probe 会因 429 频繁失败,触发滚动重启;监控系统轮询也会被限流误伤。
- 中间件开头加判断:
if c.Request.URL.Path == "/health" || c.Request.URL.Path == "/readyz" { c.Next(); return } - 别用
strings.Contains匹配 referer 或 path,容易被绕过;用精确匹配或正则预编译好的*regexp.Regexp - 敏感管理后台(如
/admin/*)建议单独配更严的限流规则,和用户 API 隔离,避免互相干扰
多实例部署必须上 Redis + Lua
单机 rate.Limiter 在 Kubernetes 多 Pod 下彻底失效——5 个 Pod 各跑一个 rate.Limiter(100, 200),入口总流量就是 500 QPS,阈值翻倍。
- Key 命名必须含双维度:
"rate:ip:" + realIP + ":api/v1/submit"或"rate:uid:" + uid + ":api/v1/submit" - 必须用 Lua 脚本一次性完成
INCR、判断是否超限、EXPIRE,避免分步执行导致 key 永久残留 - 别信 pipeline 包装的“原子性”,它只是批量发命令;
EVAL才是真原子 - Lua 脚本里不能依赖 Redis 的
TIME,必须由客户端传入当前时间戳(ARGV[3]),否则时钟漂移会导致桶填充异常
真实限流不是加个中间件就完事——key 隔离没做对,全局限流;Wait 不设超时,goroutine 挂起;Redis 脚本没原子,计数错乱;健康接口没跳过,服务反复重启。这些点任何一个漏掉,防护就形同虚设。











