Redis+Lua是分布式限流唯一可靠方案,因INCR与EXPIRE分步执行存在竞态,必须用Lua脚本原子封装计数与过期逻辑,确保全局一致性。

rate.Limiter 必须按请求维度隔离,不能全局复用
很多人把 rate.NewLimiter 实例声明在包级变量里,然后所有请求共用同一个桶——这等于把登录、注册、查询接口全绑在一起限流。结果是注册接口被刷崩时,登录请求也全被拦住。
- 正确做法:用
sync.Map按c.ClientIP()或c.GetString("user_id")动态生成并缓存*rate.Limiter - IPv6 地址含冒号,直接拼接会破坏 key 结构,建议先用
net.ParseIP标准化再转字符串 - burst 值别设太大(如 100),3~5 更合理——它代表允许的瞬时重试次数,不是并发上限
- 别在中间件里统一调
limiter.Wait,先取 path 或 user_id 构造 key,再查 map 获取对应 limiter
Redis+Lua 是分布式限流唯一可靠方案
INCR 和 EXPIRE 分两步走,高并发下必然出现竞态:两个请求同时看到计数为 0,都执行 EXPIRE,但计数已变成 2,实际放行了双倍请求。
- 必须用 Lua 脚本封装原子操作,例如传入
KEYS[1](如rate_limit:ip:192.168.1.1)、ARGV[1](limit)、ARGV[2](expire_time) - 脚本开头加
redis.call("EXISTS", KEYS[1])判断 key 是否存在,避免首次请求因返回 0 被误判为“未超限” - key 命名要带可信 IP(从
X-Forwarded-For提取,并校验代理链)和用户标识(JWT payload 解析),例如rate_limit:user:abc123:ip:2001:db8::1 - 别用 ZSet 做滑动窗口——QPS 超过 5k 后 Redis 延迟飙升,固定窗口 + TTL 更稳
Allow/Reserve/Wait 三个方法选错会直接导致超时或误杀
Wait 在 handler 里裸用 context.Background() 是最常见事故点:没设 timeout,请求永远挂起;Allow 看似简单,但在登录接口里不配合二次校验,可能让爆破工具绕过限流。
-
Allow():只返回 bool,适合快速失败场景,但要用在业务逻辑前,且拒绝后必须立即 return -
Reserve():返回*rate.Reservation,可调Delay()查等待时间,适合前端需提示“请 x ms 后重试”的交互 -
Wait(ctx):必须传带 timeout 的 context,例如ctx, cancel := context.WithTimeout(r.Context(), 500*time.Millisecond),超时自动返回 429 - 批量请求用
AllowN/WaitN,注意它是一次性申请 N 个令牌,不是放行 N 个并发——拿不到就全拒
防刷真正失效的三个隐蔽点
Token 没绑定 IP、IP 提取错误、计数器没绑定请求上下文——这三处任一出错,攻击者换 IP 或复用 Token 就能绕过所有限制。
立即学习“go语言免费学习笔记(深入)”;
- 从
X-Forwarded-For取 IP 时,必须按可信代理列表逐级剥离,否则伪造 header 即可绕过 - JWT 中的 user_id 若未在签发时绑定设备指纹或 IP 段,Token 泄露后就是永久通行证
- 限流器实例若存在 goroutine 泄漏(比如用 time.AfterFunc 清理但没 cancel),长期运行后内存持续增长
- 别忽略 fallback:Redis 不可用时,应降级到本地
rate.Limiter,而不是直接放行——否则熔断失效


















