eval并发高导致redis cpu突增是因为其单线程执行lua脚本,大量请求串行等待而非算力瓶颈;常见表现为cpu使用率高但延迟不明显、evicted_keys上升;根本原因包括全量keys扫描、未预热evalsha、大结果返回及纯计算循环。

为什么 EVAL 并发高会导致 Redis CPU 突增
Redis 是单线程执行 Lua 脚本的,EVAL 和 EVALSHA 都会阻塞主线程直到脚本跑完。哪怕脚本只耗时几毫秒,100 个并发进来,就等于排队等 100 × 几毫秒——CPU 不是被算力压垮的,是被「串行等待」拖满的。
常见现象:INFO cpu 里 used_cpu_sys 或 used_cpu_user 持续 >80%,但 redis-cli --latency 显示延迟不高;监控看到 instantaneous_ops_per_sec 没爆,但 evicted_keys 或 expired_keys 反而上升(说明其他命令在排队)。
- 脚本里用了
for循环遍历大集合(比如redis.call('KEYS', 'user:*'))——这比单纯多请求更伤 - 用
EVAL发送重复脚本(没预热EVALSHA),每次都要重解析,额外消耗 CPU - 脚本返回超大结果(如
return redis.call('LRANGE', 'list:100k', 0, -1)),序列化+网络拷贝也占 CPU
怎样让 EVALSHA 真正生效,而不是假装优化
EVALSHA 本身不省计算,只省解析。但很多人漏掉关键两步:预加载 + 容错 fallback,结果线上还是走 EVAL 白忙活。
- 启动时用
SCRIPT LOAD提前加载脚本,拿到sha1值缓存到应用层(别每次运行时现场SCRIPT LOAD) - 调用
EVALSHA后必须检查返回是否为NOAUTH或NOSCRIPT错误,遇到就自动 fallback 到EVAL并重新SCRIPT LOAD - 集群模式下,
SCRIPT LOAD必须发到对应 slot 的节点,不能随便连一个 proxy 就 load —— 否则其他节点根本没这个 sha1
示例伪代码:
sha = cache.get("my_script_sha")<br>if not sha:<br> sha = redis.eval("SCRIPT LOAD", script_content)<br> cache.set("my_script_sha", sha)<br>res = redis.eval("EVALSHA", sha, keys, args)<br>if res == "NOSCRIPT":<br> # 重试逻辑,不是直接 panic sha = redis.eval("SCRIPT LOAD", script_content)<br> res = redis.eval("EVALSHA", sha, keys, args)
哪些 Lua 操作最该砍掉,优先级最高
不是所有脚本都适合留在 Redis 里。以下三类操作,CPU 开销和风险远大于收益,应第一时间移出:
-
redis.call('KEYS', '*')或redis.call('SCAN', ...):服务端全量扫描,O(N) 且不可中断,直接干废主线程 -
for i=1,10000 do ... end:纯计算循环,Redis 没做指令数限制(lua-time-limit只管超时,不管循环次数),极易卡死 -
redis.call('GET', key) .. redis.call('GET', key2) .. ...拼大字符串:Lua 字符串拼接在 Redis 里是拷贝语义,10KB × 100 次 = 1MB 临时内存,GC 压力陡增
替代方案:用客户端分批 SCAN + 多 GET 流水线;把聚合逻辑移到应用层;用 HMGET/MGET 替代多次 GET。
用 lua-time-limit 和 slowlog-log-slower-than 定位真凶
光看 CPU 使用率没用,得知道谁在吃时间。Redis 自带两个配置就是干这个的:
-
lua-time-limit(默认 5000ms):不是超时就杀脚本,而是超时后每 100ms 检查一次,若还在跑就记 slowlog、打 warning 日志——所以调低它(比如 100ms)能更快暴露长脚本 -
slowlog-log-slower-than(默认 10000μs):确保设成 ≤10000(即 10ms),否则EVAL耗时 50ms 也不会进 slowlog,你永远看不到它 - 注意:
SLOWLOG GET 10查到的command字段里,EVALSHA显示的是 sha1 值,不是脚本内容,需配合SCRIPT DEBUG YES(仅开发环境)或提前存好映射表
真正难处理的,是那些「平均快、偶尔卡」的脚本——比如依赖外部 key 状态,平时 1ms,遇到某个 key 数据异常就飙到 200ms。这种必须靠 slowlog + 应用层埋点交叉比对,单看 Redis 指标会漏掉。










