Redis Lua脚本本身不直接导致内存碎片化,但高频字符串拼接、临时表创建及反复调用redis.call返回小响应,会使Lua VM堆(由jemalloc管理)产生大量短生命周期小对象,堆积在small-bin中形成不可合并空隙,从而隐式推高mem_fragmentation_ratio。

Redis Lua脚本本身不会直接导致内存碎片化,但脚本中高频的字符串拼接、临时表创建、以及反复调用 redis.call 返回大量小响应,会加剧 jemalloc 分配器的内部碎片积累——尤其在长期运行、高 QPS 的 Lua 脚本服务中,这个问题常被误判为“Redis 内存泄漏”。
为什么 Lua 脚本会隐式推高 mem_fragmentation_ratio
Redis 执行 Lua 脚本时,所有中间变量(local 变量、table、拼接结果)都分配在 Lua VM 的堆上,而该堆底层仍由 jemalloc 管理。关键点在于:
- 每次
..拼接字符串都会产生新string对象,旧对象等待 GC;但 Lua 5.1/5.2 的增量 GC 不保证及时回收,大量短生命周期小字符串堆积在 jemalloc 的 small-bin 中,形成不可合并的空隙 -
redis.call("GET", key)返回值是 Lua 字符串,若 key 值很小(如"1"),jemalloc 仍按最小对齐单位(通常 16B 或 32B)分配,实际利用率低 - 频繁创建空表或仅含 1–2 个字段的
table(如{status="ok", ts=redis.call("TIME")[1]}),触发 jemalloc 多级 bin 切换,放大碎片率 - 脚本未显式
collectgarbage("collect"),且 Redis 默认不主动触发 full GC,导致已死亡对象长期滞留
EVAL 中避免字符串拼接的硬性写法
不要在循环内用 res = res .. redis.call("GET", k) 累加结果。改用以下方式:
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 用
table.insert收集结果,最后一次性table.concat:local parts = {} for i, k in ipairs(KEYS) do table.insert(parts, redis.call("GET", k)) end return table.concat(parts, "|") - 若需前缀/后缀,把分隔符作为
table.concat第二参数,而非反复拼接 - 绝对避免
string.format("%s:%s", a, b)—— 它比a..":"..b开销更大,且生成更多临时对象 - 对固定格式输出(如 JSON 片段),直接构造字面量字符串:
'{"code":0,"msg":"ok"}',而非运行时拼接
控制 table 创建与复用的边界
Lua 表不是免费的。高频脚本中应限制动态表数量:
- 脚本顶层预定义可复用表(利用 Lua 闭包特性):
local _buf = {} -- 全局缓冲区,脚本内复用 return function(keys, argv) table.clear(_buf) -- 5.3+;5.1/5.2 用 for k in pairs(_buf) do _buf[k] = nil end for _, k in ipairs(keys) do table.insert(_buf, redis.call("GET", k)) end return _buf end - 避免在循环内写
local t = {a=1, b=2};改用已有表字段赋值:_buf.a = 1; _buf.b = 2 - 返回多值时,优先用多个
return a, b, c,而非打包成return {a,b,c}—— 后者强制分配新表 - 不用
json.encode类库(除非已静态编译进 Redis);原生 Lua 实现 JSON 序列化极易产生碎片;必要时用redis.call("JSON.GET", ...)替代
监控与验证:确认是否真是 Lua 引起的碎片
不能只看 mem_fragmentation_ratio 就归因于 Lua。要交叉验证:
- 执行
redis-cli info memory | grep -E "(used_memory|mem_fragmentation_ratio)",记录基线 - 停用所有 Lua 脚本流量(如切走业务请求),观察
mem_fragmentation_ratio是否缓慢回落(>30 分钟) - 对比
INFO commandstats中cmdstat_eval和cmdstat_evalsha的calls与usec_per_call,若后者显著升高(>5ms),说明 GC 压力已影响执行效率 - 用
redis-cli --scan --pattern "lua:*" | wc -l检查是否意外缓存了大量未清理脚本(SCRIPT FLUSH可清空) - 真正棘手的情况是:脚本逻辑本身没问题,但业务方在 Python/Java 客户端里用
eval动态拼接脚本字符串再发送——这会导致 Redis 缓存海量 SHA 不同的脚本,每个都占独立内存块,必须从客户端侧收敛
碎片问题最易被忽略的点:它不总在脚本执行时立刻显现,而是随实例运行时间指数级累积;一次 MEMORY PURGE 只能缓解,不能根治——根源永远在数据结构生命周期管理,不在某次 flush。

















