18-原子性和执行时间
6.4.4 原子性和执行时间
Redis的脚本执行是原子的,即脚本执行期间Redis不会执行其他命令。所有的命令都必须等待脚本执行完成后才能执行。为了防止某个脚本执行时间过长导致Redis无法提供服务(比如陷入死循环),Redis提供了 lua-time-limit 参数限制脚本的最长运行时间,默认为5秒钟。当脚本运行时间超过这一限制后,Redis将开始接受其他命令但不会执行(以确保脚本的原子性,因为此时脚本并没有被终止),而是会返回“ BUSY ”错误。现在我们打开两个redis-cli实例 A 和 B 来演示这一情况。首先在 A 中执行一个死循环脚本:
redis A> EVAL "while true do end" 0
然后马上在B中执行一条命令:
redis B> GET foo
这时实例 B 中的命令并没有马上返回结果,因为Redis已经被实例 A 发送的死循环脚本阻塞了,无法执行其他命令。等到脚本执行5秒后实例 B 收到了“ BUSY ”错误:
(error) BUSY Redis is busy running a script. You can only call SCRIPT KILL or SHUTDOWN
NOSAVE.
(3.74s)
此时 Redis 虽然可以接受任何命令,但实际会执行的只有两个命令: SCRIPT KILL 和 SHUTDOWN NOSAVE 。
在实例 B 中执行 SCRIPT KILL 命令可以终止当前脚本的运行:
redis B> SCRIPT KILL
OK
此时脚本被终止并且实例 A 中会返回错误:
(error) ERR Error running script (call to f_694a5fe1ddb97a4c6a1bf299d9537c7d3d0f84e7):
Script killed by user with SCRIPT KILL...
(28.77s)
需要注意的是如果当前执行的脚本对 Redis 的数据进行了修改(如调用 SET 、 LPUSH 或 DEL 等命令)则 SCRIPT KILL 命令不会终止脚本的运行以防止脚本只执行了一部分。因为如果脚本只执行了一部分就被终止,会违背脚本的原子性要求,即脚本中的所有命令要么都执行,要么都不执行。比如在实例 A 中执行:
redis A> EVAL "redis.call('SET', 'foo', 'bar') while true do end" 0
5秒钟后在实例 B 中尝试终止该脚本:
redis B> SCRIPT KILL
(error) UNKILLABLE Sorry the script already executed write commands against the dataset.
You can either wait the script termination or kill the server in an hard way using
the SHUTDOWN NOSAVE command.
这时只能通过 SHUTDOWN NOSAVE 命令强行终止 Redis。在第 2 章中我们介绍过使用 SHUTDOWN 命令退出Redis,而 SHUTDOWN NOSAVE 命令与 SHUTDOWN 命令的区别在于前者将不会进行持久化操作,这意味着所有发生在上一次快照(会在7.1节介绍)后的数据库修改都会丢失。
由于Redis脚本非常高效,所以在大部分情况下都不用担心脚本的性能。但同时由于脚本的强大功能,很多原本在程序中执行的逻辑都可以放到脚本中执行,这时就需要开发者根据具体应用权衡到底哪些任务适合交给脚本。通常来讲不应该在脚本中进行大量耗时的计算,因为毕竟Redis是单进程单线程执行脚本,而程序却能够多进程或多线程运行。