Redis写操作超时不会直接导致Lua阻塞,真正卡死的是脚本执行写命令后陷入不可中断状态,此时SCRIPT KILL失效、主线程锁死、所有新请求返回BUSY。

Redis写操作超时本身不会直接导致Lua阻塞,真正卡死的是 Lua 脚本里执行了写命令后陷入不可中断状态——此时 SCRIPT KILL 失效,主线程被锁死,所有新请求返回 BUSY。
为什么SCRIPT KILL对已写入的脚本完全无效
Redis 的原子性约束决定了:只要脚本中调用过一次 redis.call("SET", ...)、redis.call("INCR", ...) 等写命令,后续就无法安全中断。否则可能只改了一半数据,造成不一致。
- 执行
SCRIPT KILL会立即返回(error) UNKILLABLE Sorry the script already executed write commands - 此时脚本仍在 CPU 上运行(比如
while true do end),但 Redis 不再响应任何新命令 -
INFO commandstats中cmdstat_eval的usec_per_call会持续飙升(>5000000 微秒) -
CLIENT LIST里能看到cmd=eval、idle=0、age持续增长的连接
紧急情况下唯一可靠的终止方式是SHUTDOWN NOSAVE
这是生产环境最后手段,必须确认两点才可执行:当前无 RDB/AOF 持久化风险;且你有从其他节点或上游系统恢复数据的能力。
- 在**脚本实际运行的那个 master 节点**上执行:
SHUTDOWN NOSAVE - 不要在从节点、随机节点或集群代理(如 Codis/Redis Cluster Proxy)上执行 —— 它只会杀掉本地进程,不影响目标节点
- 执行后该节点立即退出,客户端连接断开,BUSY 状态解除
- 重启后需人工检查数据一致性(尤其涉及计数器、库存、分布式锁等关键状态)
如何避免下次再进同一个坑
应急处理只是止血,真正要防的是脚本跑飞。重点不在“怎么 kill”,而在“不让它有机会写完再循环”。
- 所有循环必须带守卫:
for i = 1, math.min(1000, #KEYS) do ... end,禁用while true和无界for - 写操作前不做重试或长耗时计算:把校验逻辑(如权限、库存是否充足)前置到应用层,Lua 只做最终原子提交
- 客户端必须设
socketTimeout(Jedis)或CommandTimeout(Lettuce),值建议设为lua-time-limit + 500ms(例如服务端设 2000,则客户端设 2500) - 集群环境下,
SCRIPT KILL必须连到对应 slot 的 master 节点 —— 用CLUSTER KEYSLOT key_name查 slot,再用CLUSTER NODES找 owner
最易被忽略的一点:脚本里调用 redis.call("SLEEP", 1) 或 redis.call("BLPOP", ...) 看似“友好”,但它仍会触发 lua-time-limit 计时,并在超时后进入可中断等待态;而纯 Lua 计算(如遍历大数组、JSON 解析)完全不触发计时,一旦失控就只能靠 SHUTDOWN NOSAVE。











