lua-time-limit不等于“超时就停”,因其仅限制单次redis.call的连续执行时间(默认5000ms),每次调用重置计时器;纯计算循环必超时并断连,而一旦执行写命令则无法中断,否则破坏原子性。

不能靠 lua-time-limit “强制杀掉进程”——它只中断未写入的脚本,且不返回错误,而是直接断连。
为什么 lua-time-limit 不等于“超时就停”
这个配置(默认 5000 毫秒)不是给整个脚本设总时限,而是限制「单次连续执行」在 Redis 线程里的耗时。每次调用 redis.call() 或 redis.pcall() 都会重置计时器。
- 纯计算循环(比如
for i=1,1000000 do end)不触发任何redis.call,必然超时,Redis 会断开客户端连接,但不会返回ERR,客户端看到的是ConnectionResetError或ECONNRESET - 如果脚本里写了
redis.call('SET', 'x', '1')再进入死循环,lua-time-limit就完全失效:Redis 不会中断已开始写入的脚本,否则破坏原子性 - 超时日志只出现在
redis.log中,格式类似:Script attempted to execute a command that would exceed the configured timeout,需确保loglevel≥notice
SCRIPT KILL 为什么经常不生效
它只对「尚未执行任何写命令」的脚本有效。一旦脚本调用了 redis.call('SET')、redis.call('DEL')、redis.call('INCR') 等修改数据的命令,状态立刻变成 UNKILLABLE。
Redis 缓存和数据结构管理技能。通过自然语言操作 Redis,支持 String、Hash、List、Set、ZSet、Stream 等数据结构操作。当用户提到 Redis、缓存、消息队列、会话存储时使用此技能。
- 现象:
SCRIPT KILL返回(error) UNKILLABLE Sorry the script already executed write commands - 原因:Redis 必须保证原子性。半途中止可能留下中间态(如扣了库存但没发券),比卡住更危险
- 验证方式:用
CLIENT LIST查找cmd=eval且idle=0、age持续增长的连接;或看INFO commandstats中cmdstat_eval:usec_per_call是否飙升
真正能用的应急手段只有两个
当脚本已写入且 SCRIPT KILL 失效时,没有“温和”的第三条路。
-
SHUTDOWN NOSAVE:最常用。立即终止 Redis 进程,不触发 RDB/AOF 持久化,上一次快照之后的所有写入丢失 -
kill -9 <pid></pid>:效果等同于SHUTDOWN NOSAVE,但跳过 Redis 自身清理逻辑(如关闭监听套接字),风险略高,不推荐作为首选 - 别等它自己结束:只要循环里没
break、没调redis.call()续命,它就不会停
预防比抢救关键得多
生产环境里,修复死循环不是靠事后杀,而是让脚本根本活不过 3 秒。
- 把
lua-time-limit调低到2000(2 秒),并在redis.conf中写死,改完执行CONFIG REWRITE - 所有循环必须带硬性退出条件:
for i = 1, math.min(1000, #KEYS) do ... end,禁用while true do - 本地调试加
redis.log(redis.LOG_WARNING, 'step '..i),上线前删掉——日志本身不耗时,但能快速定位卡点 - 传参务必转类型:
tonumber(ARGV[1]) or 0,避免字符串比较导致条件永远不满足 - 检查
KEYS[1]是否为nil再操作,KEYS和ARGV下标从1开始,写成KEYS[0]会导致空循环
最容易被忽略的一点:超时后连接直接断,客户端收不到任何 Redis 错误响应,只能靠捕获底层网络异常来判断。这和普通命令失败完全不同,不提前适配就会误判为网络抖动。










