lua脚本易触发oom因无内存硬限,循环构造大数组、全量命令(如hgetall)或递归过深会快速耗尽redis内存;需禁用全量命令、预分配table、设循环上限、客户端分批传参并压测验证。

Lua 脚本在 Redis 中执行时引发内存溢出,通常不是 Redis 本身内存不足,而是脚本内部逻辑失控导致临时内存暴涨——比如循环构造超大数组、递归过深、或一次性 redis.call("HGETALL", key) 返回几十 MB 数据。这类问题必须从脚本编写习惯和运行约束入手。
为什么 Lua 脚本容易触发 OOM?
Redis 的 Lua 环境是单线程同步执行的,且所有变量都存于栈/堆中(Lua 5.1 使用 lua_Alloc 分配),但 Redis 没有对脚本内内存使用做硬限制。一旦脚本里出现:
– 用 table.insert 累积数万条结果
– 对一个含百万字段的 Hash 执行 HGETALL 并全量遍历
– 递归调用未设深度 guard
就会迅速耗尽 Redis 进程可用内存,触发 OOM killer 或直接报错 OOM command not allowed when used memory > 'maxmemory'.
如何安全编写和部署 Lua 脚本?
核心原则:脚本只做原子计算,不承担数据搬运或聚合职责。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 禁止在脚本中调用
HGETALL、LRANGE key 0 -1、SMEMBERS等全量返回命令;改用HSCAN/SSCAN分页,但注意:这些命令不能在Lua中直接分页(Redis 不支持嵌套游标),应由客户端控制分批调用 - 若必须在脚本中构造集合,用
table.new(n, 0)预分配大小(Lua 5.2+),避免动态扩容带来的内存抖动 - 所有循环必须带明确退出条件,且上限硬编码(如
for i = 1, math.min(#keys, 1000) do) - 避免字符串拼接累积:
res = res .. item改为table.insert(buf, item)+ 最后table.concat(buf)
怎样限制 Lua 脚本的资源消耗?
Redis 本身不提供内存限额,但可通过间接手段施加约束:
- 启用
lua-time-limit(默认 5000ms),超时后脚本被强制终止 —— 这能防住死循环,但对“慢速内存增长”无效 - 在客户端侧做预检:调用前先用
DEBUG OBJECT key或MEMORY USAGE key估算目标 key 大小,超过阈值(如 1MB)直接拒绝执行脚本 - 对关键脚本做沙箱测试:用
redis-cli --eval script.lua , key1 key2在低峰期压测,观察used_memory峰值增幅 - 生产环境禁用
EVAL,只允许通过EVALSHA执行已注册脚本,并配合监控latency latest抓取长耗时脚本
PHP/Java 客户端调用时的典型陷阱
客户端代码往往掩盖了 Lua 的危险性,尤其在批量操作场景:
- PHP 中用
$redis->eval($script, [$key], 1)传入大数组参数,实际会序列化为字符串塞进 Lua 栈 —— 若数组含 10 万个 ID,序列化后可能超 5MB,直接 OOM - Java 的 Lettuce 默认开启
io.netty.maxDirectMemory,但 Lua 脚本产生的内存是在 JVM 堆外(Netty ByteBuf)还是 Redis 进程内?答案是后者 —— 所以调大-Dio.netty.maxDirectMemory无济于事 - 真正有效的防御是:客户端拆分大参数,每次只传 100~500 个元素给脚本,用循环多次调用代替单次巨量调用
redis.call("ZRANGEBYSCORE", key, "-inf", "+inf") 就可能让 Redis 在几毫秒内吃掉 2GB 内存。它不像普通业务代码有 GC 或作用域自动清理,一旦分配,就卡在 Redis 进程里直到脚本结束。










