bigywater.tech
Redis · 36

SCRIPT KILL

跑一个失控的纯计算Lua脚本和一个先写数据再死循环的脚本,分别用SCRIPT KILL尝试终止,看Redis是怎么在中断脚本和保护原子性之间划界的。

成功杀死
纯计算脚本5.5秒就被SCRIPT KILL终止
UNKILLABLE
已执行写命令的脚本拒绝被杀
原子性优先
宁可让失控脚本跑完,也不留半截写操作

做的事:对一个失控的Lua脚本,尝试从外部把它杀掉

上一篇看到脚本超过5秒后其它客户端会收到BUSY,但SCRIPT KILL/SHUTDOWN NOSAVE是仅有的两个例外。

代码script = """ local x = 0 for i=1,10000000000 do x = x + 1 end return x """ 线程A: eval(script)(这个循环量要跑几十秒) 主线程: sleep(5.5)秒后,从另一个连接发SCRIPT KILL

验证:纯只读的死循环脚本,SCRIPT KILL成功了

脚本本来要跑几十秒,只跑了5.5秒就被中断。

结果SCRIPT KILL response: True (即+OK) 原脚本这条EVAL收到的错误: Script killed by user with SCRIPT KILL... script: 18fca209ac12270..., on @user_script:3. busy script total time: 5.4998509883880615 秒 之后server responsive: True (立刻恢复正常)

验证了SCRIPT KILL能真正打断一个正在运行的Lua脚本——不是等它自然结束,是立刻终止,原本发起EVAL的那个连接会收到一条明确的"被用户用SCRIPT KILL杀死"的错误,Redis主线程立刻恢复响应其它请求。

验证:脚本一旦执行过写命令,SCRIPT KILL会被拒绝

换一个脚本:先SET一个key,然后再进入同样的死循环。

结果script2 = """ redis.call('SET', 'written_before_loop', '1') -- 先执行了一次写 local x = 0 for i=1,10000000000 do x = x + 1 end return x """ SCRIPT KILL response: 报错 UNKILLABLE Sorry the script already executed write commands against the dataset. You can either wait the script termination or kill the server in a hard way using the SHUTDOWN NOSAVE command.
这条UNKILLABLE错误揭示了SCRIPT KILL的边界:Redis只允许杀掉那些"还没有修改过任何数据"的脚本——因为Lua脚本本身是被当成一个不可分割的原子操作,如果脚本已经执行了SET这类写命令,直接中断它会让数据库停在一个"只做了一半"的中间状态,这破坏了脚本原子性的承诺,比"脚本卡住"这个问题本身还严重。所以Redis选择宁可让这个失控脚本继续跑到底,也不允许留下一个逻辑上不完整的写操作——想要处理这种情况,唯一的选项就是错误信息里说的SHUTDOWN NOSAVE,直接放弃这次修改重启整个进程,这也是为什么写Lua脚本时,通常建议在脚本一开始就用redis.call做完所有真正的写操作,再做耗时的计算,而不是反过来。

分步演示:SCRIPT KILL的两种结果

每一步都是本文上面用SCRIPT KILL命令跑出来的原始结果。

没写过数据的脚本能杀,写过的脚本杀不掉
这一步新验证的场景

    本文用到的工具

    • 本机 Redis 8.10.1
    • SCRIPT KILL:对比纯计算脚本和含写命令脚本两种场景的杀死结果
    京ICP备2026067502号-1京公网安备11010502063487号