必须用EVAL因为Redis单命令组合(如INCR+EXPIRE)在并发下不原子,而EVAL将读值、判断、更新、设过期封装为一个原子脚本执行。

Redis + Lua 实现原子限流为什么必须用 EVAL
因为单靠 Redis 命令组合(比如 INCR + EXPIRE)在并发下会丢计数,EVAL 把整个限流逻辑压进一个 Lua 脚本里执行,Redis 保证脚本原子性。常见错误是先 INCR 再判断再 EXPIRE,中间被其他请求插进来,就超限了。
典型场景:API 网关对某用户 ID 每分钟最多调用 100 次。你得在一次网络往返里完成「读旧值、判是否超限、更新、设过期」——只有 EVAL 能做到。
-
EVAL脚本里不能用redis.call("SET", key, val, "EX", 60)这种带可变参数的写法,得写死过期时间或用ARGV传入,否则 Lua 解析失败 - Redis 6.0+ 支持
ACL权限控制,如果用非 default 用户,要确保该用户有SCRIPT和READ/WRITE权限,否则报(error) NOPERM this user has no permissions to run the 'eval' command - 脚本长度别超过 1KB,太长会影响 Redis 主从同步和 AOF 重写;Golang 侧建议把 Lua 脚本定义为
const字符串,避免运行时拼接
Golang 调用 EVAL 时怎么传参才不踩坑
Redis Go 客户端(比如 github.com/go-redis/redis/v9)的 Eval 方法签名是 Eval(ctx, script, keys, args...interface{}),但很多人混淆 keys 和 args 的用途——Lua 脚本里 KEYS[1] 对应第一个 keys 元素,ARGV[1] 对应第一个 args 元素,不能反。
比如限流 key 是 "rate:uid:123",窗口是 60 秒,阈值是 100,那调用得这样写:
立即学习“go语言免费学习笔记(深入)”;
ctx := context.Background()
result, err := rdb.Eval(ctx, luaScript, []string{"rate:uid:123"}, 60, 100).Int64()
注意:第 3 个参数是 keys 切片,只放 key 名(不带值),第 4 个参数开始才是 args(TTL、limit 等动态值)。
- 如果脚本里用了
KEYS[2]但只传了一个 key,Lua 会报ERR Error running script (call to f_...): @user_script:3: bad argument #1 to 'call' (string expected, got nil) - 传整数用
int64,别传int(32 位系统可能截断);传字符串确保不含控制字符,否则 Luatonumber失败 - 不要在
args里传复杂结构体或 JSON 字符串——Lua 解析成本高,且容易因引号/转义出错;阈值、窗口这类纯数值,直接传原始类型最稳
为什么不用 redis-cell 或 RediSearch 做限流
因为它们要么增加部署复杂度(redis-cell 是 Redis Module,要编译加载),要么偏离核心需求(RediSearch 是搜索模块,没原生限流语义)。标准 Redis + Lua 已经覆盖 95% 的分布式限流场景,且兼容所有 Redis 版本(>=2.6)。
真实性能差异:单节点 Redis 在 10K QPS 下,纯 Lua 限流耗时稳定在 0.3–0.6ms;加一层 redis-cell 后,平均延时升到 0.8–1.2ms,还多了 module 加载失败、版本不匹配等运维风险点。
- 如果你用的是云 Redis(如阿里云 Tair、腾讯云 CRS),确认是否禁用了
EVAL——部分厂商默认关闭 Lua 脚本以保安全,得提工单开通 -
redis-cell的CL.THROTTLE返回 5 个字段,其中第 4 个是“可重试时间戳”,但 Golang 客户端解析容易漏掉,不如自己 Lua 脚本返回一个{ allowed: 1, remaining: 99 }结构清晰 - 别为了“看起来高级”引入新组件,限流逻辑本身应该轻量、确定、易测;Lua 脚本可以单独抽出来用
redis-cli --eval手动验证,而 module 往往没法脱离环境调试
Go 服务重启后 Lua 脚本怎么避免重复 SCRIPT LOAD
每次启动都 SCRIPT LOAD 一遍不是必须的——Redis 会缓存已加载脚本的 SHA1,只要不重启 Redis,用 EVALSHA 就行。但 Go 进程重启后不知道 Redis 里有没有这个脚本,所以得加一层检测逻辑。
推荐做法:启动时先 SCRIPT EXISTS 查 SHA1,不存在再 SCRIPT LOAD;后续全用 EVALSHA。别图省事每次都 EVAL,那样会多一次字符串解析开销。
-
SCRIPT LOAD返回的 SHA1 是 40 位小写 hex 字符串,比如"a1b2c3...",硬编码到 Go 里做比对不现实,必须运行时算 - 用
sha1.Sum([]byte(luaScript)).Hex()计算本地 SHA1,再传给SCRIPT EXISTS,注意别漏了换行符——Lua 脚本末尾的\n会影响哈希值 - 如果 Redis 集群有多个节点(如 Codis、Twemproxy),
SCRIPT LOAD必须发到每个节点,否则某节点没加载会导致EVALSHA报NOSCRIPT No matching script
int64,而不是 float64 或 interface{};还有就是 Redis 集群分片后,key 设计必须保证同一用户的限流请求落到同一个 slot,否则计数就乱了。











