直接用INCR不能做精准计数器,因其仅原子递增却不保证过期、不防漏设、不控窗口边界;必须用Lua脚本原子执行计数+过期+阈值判断三步,结合合理key设计与超时降级机制,才能实现高并发下稳定限流。

直接用 client.Incr() 不能做精准计数器——它只保证原子递增,不保证过期、不防漏设、不控窗口边界。真正“精准”的核心是:计数 + 过期 + 判断,三者必须原子执行。
为什么 INCR 单独调用会出错
INCR 自动初始化 key 并返回当前值,看似够用,但真实场景下极易失控:
- 服务在
client.Incr()成功后、client.Expire()执行前崩溃,key 永久存在,后续所有请求被误拒 - 多个实例并发调用,可能一个设了过期,另一个又覆盖设一遍,或干脆漏设,导致计数器长期滞留
- 窗口重置逻辑缺失:比如“每分钟最多 100 次”,第 61 秒时旧 key 已过期,但新 key 从 1 开始计,中间无衔接,实际允许突增
必须用 Lua 脚本封装 INCR+EXPIRE+阈值判断
Lua 在 Redis 服务端原子执行,彻底规避竞态。以下是最小可用脚本(保存为 rate_limit.lua):
local current = redis.call("INCR", KEYS[1])
if current == 1 then
redis.call("EXPIRE", KEYS[1], tonumber(ARGV[1]))
end
if current > tonumber(ARGV[2]) then
return 0
end
return current
对应 Go 调用要点:
-
redis.NewScript()只需初始化一次,建议放在init()或应用启动时 -
script.Eval(ctx, client, []string{key}, strconv.Itoa(windowSec), strconv.Itoa(limit))——KEYS必须是非空切片,ARGV两个参数分别传窗口秒数和阈值 - 返回值为
0表示超限,非零表示当前计数值,直接比较即可 - 别在 Lua 里做循环或复杂计算,避免阻塞 Redis 单线程
Key 设计决定“精准”粒度
同一个 key 被不同请求复用,就不是精准计数。常见错误写法:"counter:global" 或 "counter:" + userID。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
应按业务维度组合 key,例如:
- 按 IP + 接口路径限流:
"rate:ip:" + c.ClientIP() + ":path:" + c.Request.URL.Path - 按用户 + 方法 + 路径:
"rate:user:" + userID + ":method:" + c.Request.Method + ":path:" + c.Param("id") - 需要区分环境时加前缀:
"prod:rate:...",避免测试流量污染生产计数器
注意:key 中避免拼接不可控输入(如原始 User-Agent),防止注入或爆炸式 key 分裂。
连接与上下文必须带超时
Redis 不可用时,计数器不能拖垮整个接口。关键控制点:
- 初始化
redis.Client时设置Timeout和ReadTimeout(建议 ≤ 500ms) - 每次
script.Eval()都要传带超时的ctx,例如ctx, cancel := context.WithTimeout(context.Background(), 300*time.Millisecond) - 错误处理不要 panic,而是记录 warn 日志并降级(如默认放行或返回固定计数值)
- 连接池大小按压测结果设,
PoolSize过小会导致排队,过大则浪费 fd
真正难的不是写对一行 Incr(),而是让这个计数器在高并发、网络抖动、Redis 故障时,仍能稳定输出可预期的结果——这取决于 Lua 的原子性、key 的合理性、以及超时与降级的完备性。

















