redis+lua实现分布式限流的核心在于利用lua脚本的原子性,一次性完成计数、判断与过期设置,避免incr和expire分步执行导致的竞态问题和key永久残留,确保限流精准可靠。

防刷不是加个中间件就完事,关键在「限流维度选对、Redis原子操作写稳、拒绝逻辑前置」。单机用 rate.Limiter 必须 per-user 隔离;分布式必须用 Redis + Lua 或 pipeline,否则 INCR 和 EXPIRE 分两步会漏设过期时间,key 永久残留。
为什么直接用 time.Sleep 或全局 sync.Mutex 会失效
每个 HTTP 请求跑在独立 goroutine,time.Sleep 只阻塞当前协程,对其他请求完全没影响;而用 sync.Mutex 锁住整个计数器,高并发下锁争用严重,QPS 上不去。更糟的是内存 map 不清理过期 key,会 OOM。
- 别用
time.Sleep模拟限流——它根本不是限流,只是“假装限流” - 别把所有 IP 共用一个
rate.Limiter实例——等于全局限流,登录接口会被注册接口拖垮 - 别用未加锁的
map[string]int存计数——并发读写 panic,且无 TTL,内存只增不减
rate.Limiter 单机部署怎么避免误杀和资源争用
官方 golang.org/x/time/rate 是令牌桶实现,轻量线程安全,但必须按维度隔离实例。不能全局一个,也不能靠 if-else 判断后才拦截——那已经进业务层了。
- 用
sync.Map缓存:比如limiterMap.LoadOrStore(userID, rate.NewLimiter(rate.Every(time.Second/10), 5)) - 在中间件里调
limiter.Wait(r.Context()),超时自动返回429 Too Many Requests - 路径级限流要取
r.URL.Path做 key,别用r.Host或硬编码字符串 - 突发量(burst)建议设为最大容忍重试次数,比如 3~5,别设成 100
Redis 分布式限流为什么必须用 Lua 或 pipeline
两个请求同时执行 INCR,都看到值为 0,都执行 EXPIRE,结果 key 被设了两次过期时间,但计数已翻倍。竞态下限流形同虚设。
- 正确做法:用 Lua 脚本一次性完成
INCR、判断、EXPIRE,例如传入KEYS[1]和ARGV[1](limit)、ARGV[2](expire_time) - 或用
redis.TxPipeline()包裹INCR+EXPIRE,确保原子提交 - key 命名必须含真实 IP(从
X-Forwarded-For提取并校验可信代理)和用户标识(从 JWT payload 解析),如rate_limit:user:123:ip:8a1f2e4c - 计数前先
EXISTS,避免新用户第一次请求因 key 不存在导致INCR返回 0,误判为“未超限”
双维度(IP + Token)防刷最容易被忽略的三个点
Token 未绑定 IP、IP 提取错误、计数器未绑定上下文,这三处任一出错,攻击者换 IP 或复用 Token 就能绕过。
- 真实 IP 必须从
X-Forwarded-For或X-Real-IP提取,并校验是否在trustedProxies列表中,否则可伪造 - 必须先
jwt.Parse拿到userID,再取 IP;顺序反了会导致匿名流量全挤在一个 IP 下被误杀 - Redis key 要拼
sha256.Sum256([]byte(token)).[:8],而不是直接存原始 token——防 key 过长和泄露敏感信息
真正卡住人的从来不是“会不会写限流”,而是“限在哪儿、依据谁、失效是否及时”。IP 维度错了,Token 维度空转;Redis 操作不原子,限流就只是心理安慰;中间件里没前置拦截,等日志打完、DB 查完才 return 429,资源早就耗光了。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











