一次EVAL比多次SET/GET更快,因网络往返是Redis高并发隐性瓶颈;EVAL将读+算+写全过程压缩至1次RTT,而多命令需4次以上,且redis.call()为内部调用无额外开销。

为什么一次 EVAL 比多次 SET/GET 更快
因为网络往返(round-trip)是 Redis 高并发下的隐性瓶颈。客户端发一条命令、等响应、再发下一条,光 TCP 往返延迟在跨机房场景就可能达 10–50ms;而 EVAL 把逻辑“塞进服务端”执行,只用一次请求完成读+算+写全过程。
- 典型场景:秒杀扣库存——先
GET库存,再DECR,再判断是否为负,再SET新值。不用 Lua 至少 4 次 RTT;用 Lua 只需 1 次 - 注意:
redis.call()在脚本内调用命令不产生新网络开销,它走的是内部函数调用路径,不是 client-server 协议栈 - 别指望 Lua 脚本“加速单条命令”,它的价值永远在“多步协同”。单独用
EVAL "return redis.call('GET', KEYS[1])" 1 mykey反而比原生命令慢一点(多了脚本解析开销)
EVALSHA 是怎么省带宽的
EVALSHA 不是语法糖,它是 Redis 的脚本缓存机制落地方式:首次用 SCRIPT LOAD 提交脚本,Redis 返回一个 SHA1 摘要(比如 "a1b2c3d4..."),之后所有执行都只传这个 40 字符摘要,而不是几百字节的完整 Lua 源码。
- 实操建议:上线前用
SCRIPT LOAD预热所有业务脚本,避免运行时首次EVAL触发重复加载和 SHA 计算 - 集群环境要注意:
SCRIPT LOAD必须发到对应 key 所在 slot 的节点,否则EVALSHA会报错(error) NOSCRIPT No matching script. Please use EVAL. - Spring Boot 项目里如果用 Lettuce,记得开启
ScriptExecutor自动管理 SHA 缓存,否则每次重启都重新LOAD,反而增加初始化压力
KEYS 和 ARGV 传参不规范的后果
Redis 强制要求键名必须通过 KEYS 数组传入,不能拼在 Lua 字符串里(比如 "mykey:"..ARGV[1])。这是为了支持集群分片——Redis 需提前从 KEYS 提取所有涉及的 key,计算 slot 并路由到正确节点。
- 错误写法:
EVAL "return redis.call('GET', 'user:'..ARGV[1])" 0 123→ 集群下直接失败,提示CROSSSLOT Keys in request don't hash to the same slot - 正确写法:
EVAL "return redis.call('GET', KEYS[1])" 1 user:123,哪怕只有一个 key,1这个参数也不能省 -
ARGV只能传非 key 数据:ID、过期时间、标志位、JSON 片段等。它不参与 slot 计算,也不被 Redis 解析,纯 Lua 层变量
脚本超时和阻塞风险怎么控
Lua 脚本在 Redis 是单线程原子执行的,一个跑太久的脚本会卡住整个实例的所有命令。Redis 默认限制 5 秒(lua-time-limit 5000),超时后进入 busy 状态,此时 SCRIPT KILL 可中断(仅限无写操作的脚本),否则只能 SHUTDOWN NOSAVE。
- 高危操作:不要在脚本里用
for i=1,100000 do ... end或redis.call('KEYS', '*')—— 后者在大数据集上极易超时 - 安全做法:所有循环必须有明确上限,且用
redis.call()替代本地计算(例如用SCARD取集合大小,别用#redis.call('SMEMBERS', KEYS[1])) - 调试技巧:本地用
redis-cli --eval测试脚本时,加--ldb参数可启用 Lua 调试器,逐行看变量和耗时
return redis.call('INCR', KEYS[1]),而是想清楚哪一步必须原子、哪一步可以拆、哪一步其实该交给下游服务做。

















