evalsha触发超时而非noscript,主因是网络或响应链路异常:连接空闲断开、lettuce未启用keepalive、拓扑变更后请求发至不可用节点、command-timeout未显式配置、lua参数类型错误导致响应解析失败,或集群中节点缓存清空后静默丢弃请求。

不是脚本慢,是客户端没收到响应——Lettuce 的 RedisCommandTimeoutException 和 Lua 脚本执行时长无关,而是命令发出去后卡在了网络或 Redis 响应链路上。
为什么 EVALSHA 会触发超时而非 NOSCRIPT?
很多人以为超时是因为脚本找不到、回退到 EVAL 导致耗时增加,但实际中更常见的是:脚本确实存在(EVALSHA 成功),但 Redis 处理完后迟迟没把结果发回 Lettuce。原因包括:
-
EVALSHA请求本身不带重试逻辑,一旦底层 Netty 连接因空闲被服务端断开(如 Redistimeout配置为 0 或较小值)、或中间网络抖动,响应就丢了 - Lettuce 默认不启用连接保活(
keepAlive),连接池中空闲连接可能已失效,但客户端仍复用它发命令 - Redis 主从切换或集群拓扑变更后,Lettuce 未及时刷新节点路由,请求发到了不可用节点,既不报错也不返回,直接卡到超时
command-timeout 设置不对,超时永远治标不治本
spring.redis.timeout 只影响 Spring 层封装的同步操作(如 redisTemplate.opsForValue().get()),而 Lettuce 底层真正的命令级超时由 command-timeout 控制。漏配它,等于让高速行驶的车没有刹车。
- 默认值是 60 秒,远高于业务可接受范围(通常 1–3 秒)
- 必须通过 Java Config 显式设置:
LettuceClientConfiguration.builder().commandTimeout(Duration.ofMillis(2000)) - 别只调
max-active:连接池大小和单条命令超时无关;盲目拉高反而加剧 Netty EventLoop 竞争,尤其在低核数容器里
Lua 脚本参数类型错误会伪装成超时
当 Lua 脚本里传了 nil、布尔值或浮点数给 redis.call(),Redis 会立刻拒绝并抛 ERR Lua redis lib command arguments must be strings or integers。但 Lettuce 在某些版本(如 5.1.x)中可能无法及时解析该错误响应,导致线程挂起直至 command-timeout 触发,最终表现为“超时”而非明确的类型错误。
- 务必对
ARGV做防御转换:local val = tostring(ARGV[1])或local num = tonumber(ARGV[2]) or 0 - 避免在脚本中直接拼接
nil:"key:"..ARGV[1]→ 若ARGV[1]是nil,整个表达式崩掉 - 用
redis.log()打点关键变量值,确认进脚本前参数已是字符串/整数
集群环境下 EVALSHA 更容易“静默卡住”
Redis Cluster 中脚本缓存是按节点独立维护的。如果客户端用 EVALSHA 发往一个节点,而该节点刚完成故障转移或被清空缓存(比如 SCRIPT FLUSH),它不会像单机那样返回 NOSCRIPT,而是可能延迟响应、甚至丢弃请求——因为集群协议对脚本缺失的处理不如单机严格。
- 务必开启 Lettuce 的拓扑刷新:
ClusterTopologyRefreshOptions.builder().enableAllAdaptiveRefreshTriggers().build() - 生产环境禁用
SCRIPT FLUSH,改用灰度预热:上线前先对所有分片节点SCRIPT LOAD同一 SHA - 不要依赖客户端自动回退(如 Redisson 的
EVALSHA + EVALfallback)——它只解决NOSCRIPT,不解决“无响应”
真正难排查的,是那些既不报错、也不返回、只安静等到超时的请求。它们往往藏在连接复用、拓扑变更、参数隐式转换的缝隙里,而不是脚本本身有多复杂。











