redis.incr不能直接用于限流,因其与expire非原子操作,易导致key永不过期;应使用lua脚本(如setex+incr)保证原子性,并注意key构造、超时控制、失败降级及生命周期管理。

为什么 redis.Incr 不能直接用在限流场景
因为 INCR 没有原子性过期控制,单纯调用 redis.Incr 后再 redis.Expire 会存在竞态:如果服务在 Incr 成功但 Expire 失败前崩溃,这个 key 就永远不过期,导致后续所有请求被误拦。
正确做法是用 redis.SetNX + redis.Incr 组合,或更稳妥地直接用 Lua 脚本封装原子操作。Go 官方 redis 客户端(如 github.com/redis/go-redis/v9)推荐走 client.Eval 路径:
const luaScript = `
if redis.call("exists", KEYS[1]) == 0 then
redis.call("setex", KEYS[1], tonumber(ARGV[1]), 1)
return 1
else
return redis.call("incr", KEYS[1])
end
`
// 使用时:
result, err := client.Eval(ctx, luaScript, []string{key}, ttlSeconds, "").Int64()
- 脚本里用
setex替代set + expire,保证设置值和过期时间原子生效 -
KEYS[1]必须是动态构造的限流键(比如"rate:ip:"+clientIP),不能写死 - 注意
Eval返回类型要显式用.Int64()或.Result()处理,否则易 panic
Go 中用 middleware 套壳限流器的常见陷阱
很多示例把限流逻辑塞进 HTTP middleware,但没考虑路由参数提取时机和并发安全——比如用 r.URL.Path 当 key,却忽略了带查询参数的请求(/api/user?id=1 和 /api/user?id=2 被当成不同路径,实际应归一化)。
更关键的是:中间件里直接调用阻塞式 Redis 命令,会拖垮整个 goroutine 调度。必须确保:
- 所有 Redis 调用都带
ctx并设超时(例如ctx, cancel := context.WithTimeout(r.Context(), 300*time.Millisecond)) - 限流失败(Redis 不可用)时默认放行,而不是 500;可通过配置开关切换为“保守模式”(失败即拒绝)
- 避免在 middleware 里拼接用户输入做 key,防止 Redis 注入(如
username包含{或空格)——应先url.PathEscape或白名单校验
用 redis.Pipeline 批量查多个限流键反而更慢?
当你要对同一请求检查 IP、用户 ID、API 路径三重限流时,直觉是用 pipeline 一次发三个 GET,但实测可能比串行还慢。原因在于 pipeline 只减少网络往返,不减少 Redis 单次命令执行开销;而三个 key 若不在同一个 Redis 分片(cluster 模式下),pipeline 会被拆成多次跨节点转发。
更优解是合并为单个 Lua 脚本,一次性读取多个 key:
const multiKeyScript = `
local counts = {}
for i, key in ipairs(KEYS) do
table.insert(counts, redis.call("get", key) or "0")
end
return counts
`
// 调用:
vals, _ := client.Eval(ctx, multiKeyScript, keys).Slice()
- 脚本内用
KEYS数组传入多个 key,避免硬编码 - 返回值是
[]interface{},需用.Slice()+ 类型断言转为[]string - 如果某些 key 需要不同 TTL,就无法合并——此时宁可分开调用,也别强行 pipeline
限流器上线后发现 redis.MemoryUsage 暴涨
不是代码写错了,很可能是 key 没清理。比如按 "limit:uid:"+uid+":hour" 设计,但没配好过期时间,或者用了 SET 而非 SETEX,导致 key 永久残留。
排查步骤:
- 用
redis-cli --scan --pattern "limit:*"粗略看 key 分布 - 挑几个样本执行
ttl keyname,确认是否返回-1(永不过期)或-2(key 不存在) - 检查代码里所有
Set/SetEX调用点,确认没有漏掉EX参数(v9 客户端中Set默认不过期,SetEX才带秒级过期) - 生产环境建议加一层 key 命名规范检查:所有限流 key 必须含时间维度(如
:202405),并配定时 job 清理过期前缀
限流键的生命周期管理,比计数逻辑本身更容易出问题。别只盯着 “怎么+1”,先想清楚 “什么时候该消失”。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!











