rate.limiter不能用于分布式限流,因其仅在单进程内存维护状态,多实例下各节点独立计数导致总流量失控;必须用redis+lua实现跨进程原子计数,将计数、过期、判断封装于同一脚本中确保一致性。

rate.Limiter 不能直接用于分布式限流
它只在单个 Go 进程内存里维护令牌桶状态,多实例部署时各节点各自计数,总流量完全失控。压测看到每个 Allow() 都返回 true,但下游数据库连接池早已被打满——这不是配置错了,是根本没共享状态。
常见错误包括:把 *rate.Limiter 存进 sync.Map、塞进全局变量、甚至用 goroutine 定时同步——全无效。跨进程/跨机器的原子性计数,Go 标准库不提供,必须交给 Redis。
- 单机场景下可用:
rate.NewLimiter(rate.Every(200*time.Millisecond), 5)表示每 200ms 放行 1 次,桶容量 5,适合 CLI 工具或边缘网关 - HTTP 中间件里别每次请求 new 一个
rate.Limiter,复用实例,否则 GC 压力大且无意义 -
Wait()会阻塞 goroutine,API 场景应改用Allow()或Reserve().OK主动返回429
Redis + Lua 是唯一靠谱的原子方案
直接调 INCR + EXPIRE 是错的:两步非原子,网络中断或 Redis 崩溃会导致 key 永不过期,后续所有请求被永久拒绝。
正确做法是把计数和过期封装进 Lua 脚本,在 Redis 单线程内一次性执行:
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current = redis.call("INCR", key)
if current == 1 then
redis.call("EXPIRE", key, window)
end
return current
关键实操点:
-
script.Load()只需在服务启动时执行一次,别放 handler 里反复加载 -
script.Eval()的keys参数必须是非空切片,例如[]string{key},传nil会报ERR wrong number of arguments -
ARGV[1]是阈值(如"100"),ARGV[2]是窗口秒数(如"60"),都得转成字符串传入 - 返回值是
int64,比较时用int(result) >= threshold
Key 设计决定限流效果,不是随便拼的
限流粒度全靠 key 构造逻辑。写死 "rate:global" 就是全站共用一个桶;只用 userID 会漏掉未登录用户;只用 IP 在 NAT 或移动网络下极易误伤。
推荐按业务组合维度,例如:
- 按 IP + 接口路径:
"rate:ip:" + clientIP + ":path:" + path - 按用户 + 方法 + 路径:
"rate:user:" + userID + ":method:" + r.Method + ":path:" + path - 加业务前缀避免冲突:
"rl:auth:login:" + clientIP
注意评估 key 数量:如果用户量大+路径多,Redis 内存可能撑不住,此时要考虑降级策略(比如 fallback 到 IP 限流)或引入布隆过滤器预筛。
防刷不能只靠计数,要叠加行为特征
单纯限流挡不住模拟登录、撞库、验证码绕过等行为。真正有效的防刷需要多层信号:
- 基础层:IP + 用户 ID 的 Redis 计数(固定窗口或滑动窗口)
- 行为层:同一 IP 短时间内大量 401/403 请求 → 触发临时黑名单
- 设备层:结合 UA、JS 特征指纹(服务端可提取
User-Agent+Accept-Language+Referer做简单聚类) - 降级开关:Redis 不可用时,自动 fallback 到本地
rate.Limiter,并记录告警,避免雪崩
最容易被忽略的是时间精度问题:Lua 脚本里的 redis.call("TIME") 返回的是 Redis 服务器时间,若与应用服务器时钟偏差超过窗口大小,滑动窗口就会错乱。生产环境务必 NTP 同步所有节点。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











