Redis Lua脚本耗时监控应优先查看INFO commandstats中cmdstat_eval和cmdstat_evalsha的usec_per_call,若持续高于2000微秒(2ms),说明脚本逻辑本身不轻量,已占用主线程CPU。

直接看 INFO commandstats 里的 eval 和 evalsha 耗时
Redis 的 INFO commandstats 会统计每个命令的调用次数、总耗时(usec)和平均耗时(usec_per_call)。Lua 相关命令只可能出现在 cmdstat_eval 和 cmdstat_evalsha 两行里,它们的 usec_per_call 如果持续高于 2000(即 2ms),就说明脚本本身执行不轻量,不是网络或客户端问题,而是脚本逻辑在主线程里卡住了。
常见误判点:
- 只盯
SLOWLOG GET—— 它记录的是「执行时间 > slowlog-log-slower-than」的命令,但 Lua 脚本如果平均耗时 1.8ms、并发 500,CPU 一样会拉满,而它根本不会进慢日志 - 忽略
instantaneous_ops_per_sec和used_cpu_user的组合:如果 OPS 平稳但used_cpu_user持续 >80%,基本可锁定是少数几个 Lua 脚本在反复执行
用 SCRIPT KILL + SLOWLOG 确认是否真有超时脚本在 hang
lua-time-limit 默认是 5000ms(5 秒),超时后 Redis 不会杀脚本,而是进入 busy 状态,后续请求排队等待——此时你看到的 CPU 高,其实是「大量请求在等一个没结束的 EVAL」。
操作步骤:
- 先查配置:
CONFIG GET lua-time-limit,确认当前阈值 - 再查慢日志:
SLOWLOG GET 10,重点看有没有EVAL或EVALSHA耗时接近该值(比如 4980ms) - 如果有,立刻执行
SCRIPT KILL(仅对只读脚本有效);若已写入,只能SHUTDOWN NOSAVE后重启
注意:SCRIPT KILL 不会返回成功提示,静默执行;执行后观察 used_cpu_user 是否快速回落,这是最直接的验证方式。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
检查 EVALSHA 是否真正生效,还是在反复 fallback 到 EVAL
EVALSHA 本意是省解析开销,但如果客户端没做预热或容错,每次都会 fallback 到 EVAL,等于把脚本内容重复传、重复解析——这比单次长脚本更伤 CPU。
判断依据:
- 对比
cmdstat_eval和cmdstat_evalsha的calls字段:如果前者远高于后者(比如 eval: 12000,evalsha: 800),说明大部分请求根本没走 SHA 缓存 - 抓包或客户端日志看是否频繁出现
NOSCRIPT响应,这是 fallback 的明确信号 - 集群模式下,
SCRIPT LOAD必须发到 key 所在 slot 的节点;如果随便连 proxy 或任意节点 load,其他节点没有该 SHA,必然 fallback
别只盯着脚本内容,先看它操作了什么数据结构
很多高 CPU 的 Lua 脚本,问题不在 Lua 语法,而在它调用的 Redis 命令本身。例如:
-
redis.call('KEYS', 'user:*')—— 全量扫描,O(N),不可中断,一跑就卡死 -
redis.call('HGETALL', KEYS[1])返回几万字段,序列化+拷贝占满 CPU -
for i=1,50000 do redis.call('GET', 'counter') end—— 纯循环无意义,lua-time-limit只管超时不管指令数
这类脚本即使改用 EVALSHA 也白搭。真正要砍的,是脚本里这些 redis.call(...) 调用本身——换成 SSCAN 分批、用 HSCAN 替代 HGETALL、把大循环提到客户端做。
最隐蔽的坑是:脚本看起来很短,但里面嵌套了一个 ZRANGE bigzset 0 -1 WITHSCORES,扫了 30 万成员。它不会被 --hotkeys 抓到,也不会进慢日志(因为单次命令复杂度 O(M) 但耗时未必超阈值),却能让主线程 hang 住 300ms+。

















