唯一可靠方案是将判断、递增、设过期全部封装进lua脚本原子执行;分开调用incr+expire会导致key永不过期、计数分裂、crossslot错误等确定性失败。

直接用 redis.Incr + redis.Expire 两步走,限流一定不可靠——高并发下 key 永不过期、计数分裂、跨 slot 报 CROSSSLOT 错误都是常态。唯一能落地的方案是把判断、递增、设过期全部压进 Lua 脚本,在 Redis 端原子执行。
为什么 INCR 和 EXPIRE 分开调用会崩
这不是“偶尔出错”,而是并发场景下的确定性失败:
-
INCR成功但EXPIRE因网络抖动/Redis 主从切换失败 → key 永不过期,后续所有请求被永久拦截 - 两个请求同时读到 key 不存在,各自
INCR并设EXPIRE→ 计数器分裂,实际 QPS 翻倍 - 集群模式下,
INCR和EXPIRE若落在不同 slot(比如 key 哈希后分片不一致),直接返回CROSSSLOT Keys in request don't hash to the same slot - 客户端时间不准或时钟漂移,导致过期时间计算错误;Redis 自身
TIME命令在主从间也可能不同步
Go 客户端调用 Lua 脚本的硬性规范
用 github.com/redis/go-redis/v9 时,以下几点不遵守就等于没写:
-
redis.NewScript()必须提前加载(如init()或服务启动时),不能每次请求都 new 一个脚本对象 -
script.Run()的keys参数必须是非空切片,哪怕只有一个 key,也要传[]string{"rate:api:/login:10.0.0.1"},否则报ERR wrong number of arguments -
ARGV所有值必须是字符串,Lua 里用tonumber(ARGV[1])转换;传整数会 panic - 返回值必须用
.Int64()提取,result.Val()是 interface{},直接转 int 会丢精度或 panic - 错误要分类处理:
redis.Nil(key 不存在)、*redis.RedisError(连接/语法/集群错误)、nil(正常执行)不能混为一谈
滑动窗口限流的 Lua 脚本怎么写才不出事
固定窗口(每分钟重置)有临界问题,滑动窗口更准,但必须用 ZSET + Lua 原子封装。下面这段是生产验证过的最小可用脚本:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
local key = KEYS[1]
local window_ms = tonumber(ARGV[1])
local max_count = tonumber(ARGV[2])
local now_ms = tonumber(ARGV[3])
<p>-- 清理过期项:score 小于 (now_ms - window_ms) 的全删
redis.call("ZREMRANGEBYSCORE", key, 0, now_ms - window_ms)</p><p>-- 当前请求数量
local count = redis.call("ZCARD", key)</p><p>-- 如果未超限,插入当前时间戳
if count </p><p>注意点:</p>
- 不用
TIME,客户端传now_ms(毫秒级时间戳),避免主从时钟差 -
PEXPIRE时间设为window_ms + 1000,防止 key 在窗口结束瞬间被删掉 -
ZADD的 member 用now_ms即可,不需要额外 ID,节省内存 - key 命名必须带业务维度,例如
"rate:login:ip:10.0.0.1",别用泛化 key 如"rate:all"
令牌桶 Lua 实现的坑比想象中多
令牌桶平滑但状态同步难,Go 层做 refill 会丢精度。真要用,必须把“计算剩余 token + 扣减 + 补充”全塞进 Lua:
-
redis.call("HMGET", key, "last_ts", "tokens")返回的是数组,bucket[1]可能是 nil,得用tonumber(bucket[1]) or 0 - 补 token 公式是
math.min(capacity, tokens + (now - last_ts) * rate),浮点运算结果要math.floor()或math.ceil()显式取整,否则 Redis 存不了小数 - 返回值是
0或1,别依赖tokens数值本身做判断,Lua 里比较用>= 1,不是> 0 - 不要用
INCRBYFLOAT,它不支持原子过期;HINCRBY只能整数,所以 rate 得换算成“每毫秒多少 token”再乘以 delta
真正难的不是写脚本,是让每个 key 的命名、时间戳来源、TTL 偏移、错误分类都对齐——漏掉任意一环,压测时就会突然放行或误拒。
golang免费学习笔记(深入):立即使用
在学习笔记中,你将探索golang的核心概念和高级技巧!










