evalsha + script load 不是可选项,因为每次用 eval 发送完整脚本需重复传输、解析、计算 sha1 并加载缓存,高频调用时开销远超脚本逻辑本身。

为什么 EVALSHA + SCRIPT LOAD 不是可选项
每次用 EVAL 发送完整脚本,Redis 都要重复做三件事:传输整个脚本体、解析语法树、计算 SHA1 并加载进内存缓存。高频调用(比如限流器每秒上千次)时,这部分开销远超脚本逻辑本身。
常见错误现象:EVAL "return redis.call('INCR', KEYS[1])" 1 counter 在压测中 CPU 占用异常高,SLOWLOG GET 显示大量耗时集中在“script load”阶段。
- 必须先用
SCRIPT LOAD预加载脚本,拿到返回的 SHA1 值(如"29854f0a9b76a3e5c7d8e9f0a1b2c3d4e5f6a7b8") - 后续全部改用
EVALSHA调用,例如EVALSHA 29854f0a9b76a3e5c7d8e9f0a1b2c3d4e5f6a7b8 1 counter - 客户端需维护 SHA1 到脚本内容的映射表,并实现自动 fallback:当
EVALSHA返回NOSCRIPT时,触发一次SCRIPT LOAD再重试
怎么避免脚本卡住整个 Redis
Redis 执行 Lua 是单线程的,一个脚本跑太久,所有其他命令都得排队。默认 lua-time-limit 是 5 秒,超时直接报 BUSY Redis is busy running a script。
常见错误现象:脚本里写 for i=1,500 do redis.call('HGET', KEYS[1], 'field_'..i) end,或对几百个元素做 string.match() 过滤。
- 禁止在循环体内调用
redis.call()—— 每次调用都走完整命令流程(权限校验、键路由、执行),开销极大 - 批量读写优先用原生命令:
HMGET替代循环HGET,ZRANGEBYSCORE替代多次ZSCORE,MSET替代循环SET - 真要循环,限制在几十次内,且只做纯 Lua 计算(如拼接字符串、累加数值),不碰 Redis 命令
- 大数据量遍历必须用
SCAN分批,把筛选/聚合逻辑移回客户端
local 变量不是风格问题,是安全边界
Lua 脚本里的全局变量(如 counter = 1)既不持久、也不受 Redis 管理,还可能因残留状态导致下一次执行逻辑错乱。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
常见错误现象:脚本里定义 tmp_result = {},循环中不断 table.insert(tmp_result, ...),但没声明 local;第二次执行时发现 tmp_result 已有旧数据。
- 所有中间值必须用
local声明,例如local val = redis.call('GET', KEYS[1]) - 不要靠 Lua 变量“记状态”,需要跨调用的状态必须显式写入 Redis 键,比如
redis.call('SET', KEYS[2], 'processing') -
redis.call()的返回值别重复调用,缓存到局部变量复用 - 大对象处理完及时置空:
big_table = nil,避免内存持续增长
Functions 比 EVAL 更稳,但迁移不能跳步
传统 EVAL 脚本只存在内存里,主从切换、重启、SCRIPT FLUSH 后就丢了,客户端必须重新 SCRIPT LOAD,否则 EVALSHA 直接失败。
而 FUNCTION LOAD 注册的函数会写入 AOF/RDB,自动同步到从节点,集群里真正“活”着。
- 头必须是第一行且严格格式:
#!lua name=lib:stock version=1,空行都不行 - 禁用
redis.call大写写法,必须小写:redis.call("INCR", key),REDIS.CALL会报错 - 不支持
os.time()、io、loadstring等非安全模块,JSON 解析仅限极简场景且确认已加载cjson - ACL 权限需显式授予
+function,否则FCALL报(error) NOPERM
Functions 不解决单线程阻塞问题,慢函数一样卡 Redis;它只解决脚本“丢了怎么办”这个运维痛点。










