gin接口防刷需分层限流:单机用sync.map缓存独立rate.limiter实例,防连坐;分布式优先用redis固定窗口lua脚本,慎用zset;验证码接口须ip+手机号双校验并绑定设备指纹;中间件中关键路径用allow(),容忍延迟场景用带超时的wait()。

直接用 Gin 做接口防刷,不能只靠一个中间件打天下——限流必须分层、按场景、带 fallback,否则要么拦不住爬虫,要么误杀正常用户。
rate.Limiter 单机限流怎么避免共享桶陷阱
很多人一上来就写 var limiter = rate.NewLimiter(10, 5) 然后全局复用,结果所有请求共用一个桶,IP A 刷爆了,IP B 也跟着 429。这不是防刷,是“连坐”。
- 每个客户端必须有独立的
*rate.Limiter实例,key 推荐用c.ClientIP()或c.GetString("user_id")(登录态存在时) - 用
sync.Map缓存,不要用普通map + sync.RWMutex,高并发下锁争抢严重 - key 构造要防注入:IPv6 地址含冒号,建议先调用
net.ParseIP().To16()转标准格式再 hex 编码 - 不主动清理旧 key 会导致内存泄漏,可配合定时 goroutine 扫描过期项,或用 LRU 库如
github.com/hashicorp/golang-lru
Redis 分布式限流为什么慎用 ZSet 滑动窗口
很多教程推荐用 Redis ZSet 实现滑动窗口,但线上压测发现:QPS 超过 5k 后,ZRemRangeByScore + ZCard 组合操作延迟飙升,Redis CPU 直接拉满。
- 生产环境优先选固定窗口 +
INCR/EXPIRE原子 Lua 脚本,简单、快、压测稳定 - ZSet 方案只适合低频敏感接口(如短信发送),且需严格控制时间窗口粒度(别设 1s 窗口)
- 脚本里别用
KEYS,改用SCAN配合业务 key 前缀做清理,否则集群模式下报错 - 注意 Redis 连接池配置:超时设
ReadTimeout: 50ms,失败时降级为单机rate.Limiter,别让限流器变成单点故障
验证码类接口(如 /send-code)必须加双校验
只校验 IP 限流?攻击者换代理就绕过。只校验手机号?批量注册小号照样刷。必须组合验证。
- 第一层:IP + 手机号联合 key,例如
limit:ip_phone:192.168.1.1:138****1234,每分钟最多 1 次 - 第二层:手机号维度单独限频(防换 IP 狂刷),
limit:phone:138****1234,24 小时最多 5 次 - 触发阈值后(如 3 次失败),强制返回
{"captcha_required": true},前端弹滑块,后端校验geetest_validate或自研 token - 验证码 token 必须绑定设备指纹(UA + screen + canvas hash),防止脚本复用
Gin 中间件里 Wait() 和 Allow() 别混用
limiter.Wait(ctx) 会阻塞,limiter.Allow() 立即返回 bool——选错就等于把限流逻辑从“控速”变成“随机丢包”。
- 登录、发短信等关键路径,用
Allow():没令牌立刻c.AbortWithStatusJSON(429, ...),不卡住连接 - 文件下载、日志上报等容忍延迟的接口,可用
Wait(ctx),但ctx必须带 timeout(如context.WithTimeout(c.Request.Context(), 200*time.Millisecond)) - 绝对不要在中间件里写
limiter.Wait(context.Background()),超时不可控,goroutine 泄漏风险极高 - Reserve() 适合需要预判等待时间的场景(比如前端倒计时提示),但要注意
reservation.Delay()返回的是纳秒级,别直接当秒数用
真正难的不是写限流代码,而是判断哪个接口该用 IP 限流、哪个该绑手机号、哪个必须上验证码——这些决策得看日志里真实攻击流量的分布,而不是拍脑袋设个 “10r/s”。











