redis不会预编译lua脚本;script load仅存脚本并计算sha1,不触发编译,真正编译发生在eval或首次evalsha执行时,通过lual_loadbuffer()动态生成字节码,且每次执行均新建独立lua_state,无字节码复用或执行计划。

Redis 的 Lua 脚本没有传统意义上的“执行计划”,它不解析、不优化、不生成 AST 或字节码缓存——脚本在 EVAL 时才被一次性编译并立即执行,且全程无可观测的中间表示。
Redis 真的会“预编译”Lua脚本吗?
不会。所谓“预编译”是常见误解。Redis 在 EVAL 执行时才调用 Lua C API 的 luaL_loadbuffer() 加载并编译脚本为 Lua 字节码,这个过程发生在每次 EVAL(或首次 EVALSHA)时;而 SCRIPT LOAD 只是把脚本字符串存进 lua_scripts 字典,并计算 SHA1,**不触发任何编译行为**。
这意味着:
-
SCRIPT LOAD后脚本仍可能在后续EVALSHA时报语法错误(比如 Lua 5.1 语法写成 5.4 风格),因为编译被延迟到了执行时刻 - 没有 JIT、没有字节码复用(不同客户端重复
EVALSHA仍会各自加载字节码到独立 Lua state)、也没有类似 SQL 的EXPLAIN输出通道 - 你无法通过
redis-cli --verbose或MONITOR看到“编译阶段”,只能看到命令入队和返回结果两个原子点
为什么看不到执行计划?根本原因在架构设计
Redis 的 Lua 支持本质是“沙箱直译器集成”,不是“数据库查询引擎嵌入”。它的目标是轻量、确定、可控,而非可分析、可优化:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- Lua 环境每次执行都新建(伪客户端绑定独立
lua_State),不共享上下文,也就不存在跨执行的计划缓存 - 所有 Redis 命令调用都经由
redis.call()封装为同步阻塞调用,不支持异步/延迟求值,自然无需执行树或计划重排 - 禁止非确定性函数(如
os.time()、math.random())就是为了杜绝“同一脚本不同执行路径”,让“不可见”变成“没必要可见”
EVALSHA 比 EVAL 快,但快在哪?
快在**网络和字符串处理开销**,不在执行阶段:
-
EVAL:传输完整脚本字符串 → 解析长度 →luaL_loadbuffer()编译 → 执行 → 缓存 SHA1+脚本 -
EVALSHA:只传 40 字节 SHA1 → 查lua_scripts字典 → 若命中,跳过字符串解析和编译,直接执行已缓存字节码 - 但注意:即使命中,每次执行仍要重新创建
lua_State、重设全局表、重载 redis.call 等钩子——这部分开销无法省略
想“看”脚本实际做了什么?只有 runtime 观测这一条路
既然没有编译期计划,就只能靠运行时行为推断:
- 用
redis.call("debug", "object", KEYS[1])查 key 内存结构(需开启notify-keyspace-events或调试模式) - 在脚本里加
redis.log(redis.LOG_WARNING, "step: "..i)(仅限开发,生产禁用) - 配合
SLOWLOG GET看耗时,再结合LATENCY LATEST判断是否卡在 Lua(而非网络或磁盘) - 用
INFO commandstats观察cmdstat_eval和cmdstat_evalsha的调用频次与耗时分布
真正容易被忽略的是:脚本里每行 Lua 代码(比如 for i=1,1000 do table.insert(res, redis.call("GET", KEYS[i])) end)都会触发一次 C 层调用开销,这种“隐式循环放大”比语法错误更难定位,也完全不会出现在任何“计划”里。










