EVAL 脚本 numkeys 键[键...] arg [arg ...]
自Redis2.6.0版本起可用。
时间复杂度:取决于执行的脚本。
EVAL介绍
- EVAL和EVALSHA用于从Redis2.6.0版本,开始使用内置在Redis中的Lua解释器来评估脚本。
- EVAL的第一个参数是一个Lua 5.1脚本。脚本不需要定义一个Lua函数(不应该)。这只是一个将在Redis服务器上下文中运行的Lua程序。
- EVAL的第二个参数是脚本后面的参数个数,(从第三个参数开始)代表Redis键名称。参数可以通过Lua中使用来访问KEYS全局变量在基于一个数组(这样的形式KEYS[1],KEYS[2]...)。
所有其他参数不应该代表键名称,可以通过ARGV全局变量被Lua使用,非常类似于键做了什么(如:ARGV[1],ARGV[2]...)。
下面的例子应该阐明上面的内容:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second 1) "key1" 2) "key2" 3) "first" 4) "second"
注意:正如你所看到的,Lua数组是以Redis多个批量回复的形式返回的,这是一个Redis返回类型,你的客户端类库可能会用你的编程语言转换成一个Array类型。
- 可以使用两个不同的Lua函数从Lua脚本调用Redis命令:
redis.call() redis.pcall()
redis.call()类似于redis.pcall(),唯一的区别是,
如果一个Redis命令调用会导致一个错误,redis.call()将引发一个Lua错误,反过来会强制EVAL向命令调用者返回一个错误,
然而redis.pcall将捕捉错误,并返回一个Lua table代表错误。
- redis.call()和redis.pcall()函数的参数,是Redis命令格式的所有参数:
> eval "return redis.call('set','foo','bar')" 0 OK
上面的脚本将键foo设置为字符串bar。然而,它违反了EVAL命令的语义,因为脚本使用的所有键都应该使用KEYS数组传递:
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
- 在执行之前,必须先分析所有Redis命令,以确定命令将在哪些键上运行。为了使EVAL成为真,键必须明确地传递。这在许多方面都很有用,但特别是要确保Redis群集可以将请求转发到适当的群集节点。
- 请注意:为了向用户提供使用Redis单实例配置的机会,不会执行此规则,但要以编写与Redis群集不兼容的脚本为代价。
Lua脚本可以使用一组转换规则,返回从Lua类型转换为Redis协议的值。
Lua和Redis数据类型之间的转换
- 当Lua使用call()or 调用Redis命令时,Redis返回值被转换为Lua数据类型pcall()。同样,在调用Redis命令和Lua脚本返回值时,Lua数据类型转换为Redis协议,以便脚本可以控制EVAL返回给客户端的值。
- 数据类型之间的这种转换的设计方式是,如果将Redis类型转换为Lua类型,然后将结果转换回Redis类型,则结果与初始值相同。
- 换句话说,Lua和Redis类型之间存在一对一的转换。下表显示了所有的转换规则:
Redis到Lua转换表。
Redisinteger reply ->Luanumber
Redisbulk reply ->Luastring
Redismulti bulk reply ->Luatable (may have other Redis data types nested)
Redisstatus reply ->Luatable with a single ok field containing the status
Rediserror reply ->Luatable with a single err field containing the error
RedisNil bulk reply and Nil multi bulk reply ->Luafalseboolean type
Lua到Redis转换表。
Lua number ->Redis integer reply (the number is converted into an integer)
Lua string ->Redis bulk reply
Lua table (array)->Redis multi bulk reply (truncated to the first nil inside the Lua array if any)
Lua table with a single ok field ->Redis status reply
Lua table with a single err field ->Redis error reply
Luabooleanfalse->RedisNil bulk reply.
还有一个额外的Lua-to-Redis转换规则,它没有对应的Redis到Lua转换规则:
Luabooleantrue->Redis integer reply with value of 1.
还有两个重要的规则要注意:
- Lua有一个数字类型,Lua numbers。整数和浮点数没有区别。所以我们总是把Lua数字转换成整数回复,如果有小数部分的话去掉数字的小数部分。如果你想从Lua返回一个浮点数,你应该像字符串一样返回它,就像Redis本身一样(例如见ZSCORE命令)。
- 没有简单的方法,在Lua数组中有nils(There is no simple way to have nils inside Lua arrays),这是的Lua表语义的结果,所以当Redis的一个Lua数组转换成Redis的协议,如果遇到nil的转换停止。
以下是几个转换示例:
> eval "return 10" 0 (integer) 10
> eval "return {1,2,{3,'Hello World!'}}" 0 1) (integer) 1 2) (integer) 2 3) 1) (integer) 3 2) "Hello World!"
> eval "return redis.call('get','foo')" 0 "bar"
- 最后一个例子展示它是如何从Lua获得,确切redis.call()或者redis.pcall()的返回值 ,如果命令直接调用将被返回。
- 在下面的例子中,我们可以看到如何处理带有nil的浮点数组和数组:
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0 1) (integer) 1 2) (integer) 2 3) (integer) 3 4) "foo"
- 正如你可以看到3.333转化成3,bar 永远不会返回的字符串,因为前面是零。
Helper函数返回Redis类型
有两个helper 函数可以从Lua返回Redis类型。
- redis.error_reply(error_string)返回错误回复。这个函数只是简单的返回a single field table ,其中的err字段设置为指定的字符串。
- redis.status_reply(status_string)返回状态回复。这个函数只是简单的返回a single field table ,其中的ok字段设置为指定的字符串。
使用helper 函数或直接以指定的格式返回table 没有区别,所以以下两种形式是等价的:
return {err="My Error"} return redis.error_reply("My Error")
脚本的原子性
- Redis使用相同的Lua解释器来运行所有的命令。另外,Redis保证以原子方式执行脚本:执行脚本时不会执行其他脚本或Redis命令。这个语义类似于MULTI / EXEC。从所有其他客户的角度来看,脚本的效果要么不可见,要么已经完成。
- 但是这也意味着执行缓慢的脚本不是一个好主意。创建快速脚本并不难,因为脚本开销非常低,但是如果要使用慢速脚本,则应该意识到在脚本运行时,没有其他客户端可以执行命令。
错误处理
- 如前所述,redis.call()导致Redis命令错误的调用会停止脚本的执行并返回一个错误,使得脚本错误显而易见:
> del foo (integer) 1 > lpush foo a (integer) 1 > eval "return redis.call('get','foo')" 0 (error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e38c0e791): ERR Operation against a key holding the wrong kind of value
- 使用redis.pcall()不会引发错误,但错误对象是在上述规定的格式返回(作为一个 Lua table 与err字段)。该脚本可以通过返回redis.pcall()的错误对象,将确切的错误传递给用户。
Bandwidth和EVALSHA
EVAL命令迫使你一次又一次地发送脚本主体。Redis不需要每次重新编译脚本,因为它使用内部缓存机制,但是在许多情况下支付额外带宽(bandwidth )的代价可能不是最佳的。
另一方面,使用特殊命令或通过定义命令redis.conf 将是一个问题,原因如下:
- 不同的实例可能有不同的命令实现。
- 如果我们必须确保所有实例都包含给定的命令,特别是在分布式环境中,则部署非常困难。
- 阅读应用程序代码,完整的语义可能不清楚,因为应用程序调用命令定义的服务器端。
为了避免这些问题,避免带宽损失,Redis实现了EVALSHA命令。
EVALSHA的工作方式与EVAL完全相同,但不是将脚本作为第一个参数,而是使用脚本的SHA1 digest 。
行为如下:
- 如果服务器仍然记住具有匹配的SHA1摘要的脚本,则执行该脚本。
- 如果服务器不记得具有此SHA1 digest的脚本,则会返回一个特殊错误,告诉客户端使用EVAL。
例:
> set foo bar OK > eval "return redis.call('get','foo')" 0 "bar" > evalsha 6b1bf486c81ceb7edf3c093f4c48582e38c0e791 0 "bar" > evalsha ffffffffffffffffffffffffffffffffffffffff 0 (error) `NOSCRIPT` No matching script. Please use [EVAL](/commands/eval).
- 即使客户端实际调用了EVAL,客户端库实现也总能乐观地发送EVALSHA,希望脚本已经被服务器看到了。如果错误返回,将使用EVAL。
- NOSCRIPT将键和参数作为额外的EVAL参数传递在这个上下文中也是非常有用的,因为脚本字符串保持不变并且可以被Redis有效地缓存。
脚本缓存语义
- 执行的脚本被保证永远在Redis实例的给定执行的脚本缓存中。这意味着如果对Redis实例执行EVAL,所有后续的EVALSHA调用都将成功。
- 脚本可以长时间缓存的原因是,编写良好的应用程序不可能有足够的不同脚本来引起内存问题。每个脚本在概念上都像执行一个新的命令,即使是一个大的应用程序也可能只有几百个。即使应用程序被多次修改,脚本也会改变,所使用的内存可以忽略不计。
- 刷新脚本缓存的唯一方法是显式调用SCRIPT FLUSH命令,这将彻底刷新脚本缓存,删除到目前为止执行的所有脚本。
- 这通常仅在实例将被实例化为云环境中的另一个客户或应用程序时才需要。
另外,如前所述,重新启动Redis实例刷新脚本缓存,这是不持久的。但是从客户端的角度来看,只有两种方法可以确保Redis实例在两个不同的命令之间不重新启动。
- 我们与服务器的连接是持久的,并且从来没有关闭过。
- 客户端显式检查INFO命令中的runid字段,以确保服务器未重新启动,并且仍然是相同的进程。
实际上,对于客户端来说,简单地假设在给定连接的上下文中,保证缓存脚本在那里,除非管理员明确地调用了SCRIPT FLUSH命令。
用户可以指望Redis不删除脚本的事实在流水线上下文中是语义上有用的。
例如,一个与Redis持久连接的应用程序可以确定,如果脚本一旦被发送,它仍然在内存中,那么EVALSHA就可以在管道中用于这些脚本,而不会由于未知的脚本而产生错误(我们稍后会详细看到这个问题)。
一个常见的模式是调用SCRIPT LOAD来加载将出现在管道中的所有脚本,然后直接在管道中使用EVALSHA,而不需要检查由于脚本哈希不被识别而导致的错误。
SCRIPT命令
Redis提供了一个SCRIPT命令,可以用来控制脚本子系统。SCRIPT目前接受三个不同的命令:
- 脚本刷新
此命令是强制Redis刷新脚本缓存的唯一方法。在云环境中,可以将相同的实例重新分配给不同的用户,这非常有用。测试客户端库的脚本功能实现也很有用。
SCRIPT EXISTS sha1 sha2 ... shaN
- 给定一个SHA1摘要列表作为参数,这个命令返回一个1或0的数组,其中1表示特定的SHA1被识别为已存在于脚本缓存中的脚本,而0表示具有该SHA1的脚本从来没有见过或者至少从未见过最新的SCRIPT FLUSH命令)。
SCRIPT LOAD script
- 该命令将指定的脚本注册到Redis脚本缓存中。这个命令在我们想要确保EVALSHA不会失败的所有上下文中是有用的 (例如在管道或MULTI / EXEC操作期间),而不需要真正执行脚本。
SCRIPT KILL
- 此命令是中断长时间运行的脚本的唯一方法,该脚本达到配置的脚本最大执行时间。SCRIPT KILL命令只能用于在执行期间不修改数据集的脚本(因为停止只读脚本不会违反脚本引擎的保证原子性)。有关长时间运行的脚本的更多信息,请参阅下一节。
脚本作为纯粹的功能
脚本的一个非常重要的部分是编写纯函数的脚本。在默认情况下,在Redis实例中执行的脚本会通过发送脚本(而不是结果命令)在从属设备上复制到AOF文件中。
原因在于,将脚本发送到另一个Redis实例通常要比发送脚本生成的多个命令快得多,因此如果客户端将大量脚本发送给主设备,则将脚本转换为slave / AOF的单个命令会导致复制链接或“只附加文件”的带宽过多(而且由于调度通过网络接收到的命令,CPU数量也比分派由Lua脚本调用的命令要多得多)。
通常复制脚本而不是脚本的效果是有意义的,但不是在所有情况下。因此,从Redis 3.2开始,脚本引擎能够复制脚本执行产生的写入命令序列,而不是复制脚本本身。请参阅下一节获取更多信息。在本节中,我们将假设通过发送整个脚本来复制脚本。我们把这个复制模式称为整个脚本复制。
整个脚本复制方法的主要缺点是脚本需要具有以下属性:
- 脚本必须始终使用给定相同输入数据集的相同参数来评估相同的Redis 写入命令。由脚本执行的操作不能依赖于任何隐藏的(非显式的)信息或状态,这些信息或状态可能会随着脚本执行的进行或脚本的不同执行而改变,也不能依赖于来自I / O设备的任何外部输入。
比如使用系统时间,调用像RANDOMKEY这样的Redis随机命令 ,或者使用Lua随机数生成器,都可能导致脚本不总是以相同的方式进行评估。
为了在脚本中执行这个行为,Redis执行以下操作:
- Lua不会导出命令来访问系统时间或其他外部状态。
- 如果一个脚本调用了一个Redis命令,Redis命令可以在RANDOMKEY,SRANDMEMBER,TIME之类的Redis 随机命令 之后修改数据集, Redis将会阻塞该脚本。这意味着如果一个脚本是只读的,并且不修改数据集,则可以自由地调用这些命令。请注意,随机命令不一定意味着使用随机数的命令:任何非确定性命令都被视为随机命令(这方面的最佳示例是TIME命令)。
- 可能以随机顺序返回元素的Redis命令 (如SMEMBERS(因为Redis集合是无序的))在从Lua调用时具有不同的行为,并在将数据返回到Lua脚本之前经历无声的词典排序过滤器。因此,redis.call("smembers",KEYS[1])将始终以相同的顺序返回Set元素,而从普通客户端调用的相同命令可能会返回不同的结果,即使该键包含完全相同的元素。
- Lua伪随机数生成函数,math.random并且 math.randomseed被修改以便每次执行新的脚本时始终具有相同的种子。这意味着math.random如果math.randomseed不使用脚本,每次执行脚本时,调用将始终生成相同的数字序列。
但是用户仍然可以使用以下简单的技巧编写随机行为的命令。想象一下,我想写一个Redis脚本,用N个随机整数填充一个列表。
我可以从这个小小的Ruby程序开始:
require 'rubygems' require 'redis' r = Redis.new RandomPushScript = <<EOF local i = tonumber(ARGV[1]) local res while (i > 0) do res = redis.call('lpush',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,[:mylist],[10,rand(2**32)])
每次执行该脚本时,结果列表都将具有以下元素:
> lrange mylist 0 -1 1) "0.74509509873814" 2) "0.87390407681181" 3) "0.36876626981831" 4) "0.6921941534114" 5) "0.7857992587545" 6) "0.57730350670279" 7) "0.87046522734243" 8) "0.09637165539729" 9) "0.74990198051087" 10) "0.17082803611217"
为了使它成为一个纯函数,但仍然要确保每个脚本的调用都会导致不同的随机元素,我们可以简单地向脚本添加一个额外的参数,这个参数将用于播种Lua伪随机数发电机。新的脚本如下:
RandomPushScript = <<EOF local i = tonumber(ARGV[1]) local res math.randomseed(tonumber(ARGV[2])) while (i > 0) do res = redis.call('lpush',KEYS[1],math.random()) i = i-1 end return res EOF r.del(:mylist) puts r.eval(RandomPushScript,1,:mylist,10,rand(2**32))
我们在这里做的是发送PRNG的种子作为论据之一。这样,给定相同参数的脚本输出将是相同的,但我们正在改变每个调用中的一个参数,生成随机种子客户端。种子将作为复制链接和“附加文件”中的参数之一传播,以保证在重新加载AOF或从属进程处理脚本时将生成相同的更改。
注意:这种行为的一个重要部分是Redis实现的PRNG,math.random并math.randomseed保证具有相同的输出,而不管运行Redis的系统的体系结构如何。32位,64位,大端和小端系统都将产生相同的输出。
复制命令而不是脚本
从Redis 3.2开始,可以选择另一种复制方法。我们可以复制脚本生成的单个写入命令,而不是复制整个脚本。我们将这个脚本称为复制。
在这种复制模式下,当执行Lua脚本时,Redis收集由Lua脚本引擎执行的所有实际修改数据集的命令。当脚本执行结束时,脚本生成的命令序列被包装到一个MULTI / EXEC事务中,并被发送到从属和AOF。
根据用例,这在几个方面是有用的:
- 当脚本计算速度慢时,但是这些效果可以通过一些写入命令来总结,重新计算从属脚本或重新加载AOF时,这是一个耻辱。在这种情况下只复制脚本的效果要好得多。
- 当启用脚本效果复制时,关于非确定性功能的控件被禁用。例如,您可以在任何地方随意使用脚本中的TIME 或SRANDMEMBER命令。
- 在这种模式下的Lua PRNG在每个呼叫中随机播种。
为了启用脚本特效复制,您需要在脚本操作之前发出以下Lua命令:
redis.replicate_commands()
如果启用了脚本特效复制,则该函数返回true;否则,如果在脚本已经调用某个写入命令后调用该函数,则返回false,并使用正常的整个脚本复制。
命令的选择性复制
当选择脚本特效复制(参见上一节)时,可以更多地控制命令复制到从站和AOF的方式。这是一个非常先进的功能,因为滥用可以通过违反主控,从属和AOF都必须包含相同的逻辑内容的合同来造成破坏。
然而,这是一个有用的功能,因为有时我们只需要在主服务器上执行某些命令来创建中间值。
在我们执行两个集合之间的交集的Lua脚本中思考。选取五个随机元素,并用这五个随机元素创建一个新的集合。最后,我们删除表示两个原始集之间交集的临时密钥。我们要复制的只是五行元素的创造。还复制创建临时密钥的命令是没有用的。
因此,Redis 3.2引入了一个新的命令,该命令仅在启用脚本特效复制时才起作用,并且能够控制脚本复制引擎。redis.set_repl()如果禁用脚本特技复制,则调用该命令并在调用时失败。
该命令可以用四个不同的参数来调用:
redis.set_repl(redis.REPL_ALL) -- Replicate to AOF and slaves. redis.set_repl(redis.REPL_AOF) -- Replicate only to AOF. redis.set_repl(redis.REPL_SLAVE) -- Replicate only to slaves. redis.set_repl(redis.REPL_NONE) -- Don't replicate at all.
默认情况下,脚本引擎始终设置为REPL_ALL。通过调用此函数,用户可以打开/关闭AOF和/或从属复制,并稍后按照自己的意愿将其复位。
一个简单的例子如下:
redis.replicate_commands() -- Enable effects replication. redis.call('set','A','1') redis.set_repl(redis.REPL_NONE) redis.call('set','B','2') redis.set_repl(redis.REPL_ALL) redis.call('set','C','3')
在运行上面的脚本之后,结果是在从属和AOF上只创建了A和C键。
全局变量保护
Redis脚本不允许创建全局变量,以避免数据泄露到Lua状态。如果脚本需要在调用之间保持状态(非常罕见),应该使用Redis键。
当尝试全局变量访问时,脚本被终止,EVAL返回一个错误:
redis 127.0.0.1:6379> eval 'a=10' 0 (error) ERR Error running script (call to f_933044db579a2f8fd45d8065f04a8d0249383e57): user_script:1: Script attempted to create global variable 'a'
访问一个不存在的全局变量会产生类似的错误。
使用Lua调试功能或其他方法(例如修改用于实现全局保护的元表来避免全局保护)并不难。然而,意外地做到这一点很困难。如果用户使用Lua全局状态混淆,则AOF和复制的一致性不能保证:不要这样做。
注意Lua新手:为了避免在脚本中使用全局变量,只需使用local关键字声明要使用的每个变量。
在脚本中使用SELECT
可以像使用普通客户端一样在Lua脚本中调用SELECT。但是,Redis 2.8.11和Redis 2.8.12之间行为的一个细微方面发生了变化。在2.8.12发行版之前,由Lua脚本选择的数据库作为当前数据库被传输到调用脚本。从Redis 2.8.12开始,由Lua脚本选择的数据库只影响脚本本身的执行,但不会修改调用脚本的客户端选择的数据库。
修补程序级别版本之间的语义变化是必需的,因为旧的行为本身与Redis复制层不兼容,并且是错误的原因。
可用的库
Redis Lua解释器加载以下Lua库:
base 库。 table 库。 string 库。 math 库。 struct 库。 cjson 库。 cmsgpack 库。 bitop 库。 redis.sha1hex 功能。 redis.breakpoint and redis.debug函数在Redis Lua调试器的上下文中。
每个Redis实例都保证具有上述所有库,因此您可以确保Redis脚本的环境始终如一。
struct,CJSON和cmsgpack是外部库,所有其他库都是标准的Lua库。
结构
struct是一个在Lua中打包/解包结构的库。
Valid formats:
>- big endian
<- little endian
![num]- alignment
x - pading
b/B -signed/unsignedbyte
h/H -signed/unsignedshort
l/L -signed/unsignedlong
T -size_t
i/In-signed/unsigned integer with size `n'(default is size of int)
cn - sequence of `n' chars (from/to a string); when packing, n==0 means
the whole string; when unpacking, n==0 means use the previous
read number as the string length
s - zero-terminated string
f -float
d -double
' '- ignored
例:
127.0.0.1:6379> eval 'return struct.pack("HH", 1, 2)' 0 "\x01\x00\x02\x00" 127.0.0.1:6379> eval 'return {struct.unpack("HH", ARGV[1])}' 0 "\x01\x00\x02\x00" 1) (integer) 1 2) (integer) 2 3) (integer) 5 127.0.0.1:6379> eval 'return struct.size("HH")' 0 (integer) 4
CJSON
CJSON库在Lua中提供了非常快速的JSON操作。
例:
redis 127.0.0.1:6379> eval 'return cjson.encode({["foo"]= "bar"})' 0 "{\"foo\":\"bar\"}" redis 127.0.0.1:6379> eval 'return cjson.decode(ARGV[1])["foo"]' 0 "{\"foo\":\"bar\"}" "bar"
cmsgpack
cmsgpack库在Lua中提供简单快速的MessagePack操作。
例:
127.0.0.1:6379> eval 'return cmsgpack.pack({"foo", "bar", "baz"})' 0 "\x93\xa3foo\xa3bar\xa3baz" 127.0.0.1:6379> eval 'return cmsgpack.unpack(ARGV[1])' 0 "\x93\xa3foo\xa3bar\xa3baz" 1) "foo" 2) "bar" 3) "baz"
bitop
Lua位操作模块在数字上添加按位操作。自2.8.18版以来,它可用于Redis中的脚本。
例:
127.0.0.1:6379> eval 'return bit.tobit(1)' 0 (integer) 1 127.0.0.1:6379> eval 'return bit.bor(1,2,4,8,16,32,64,128)' 0 (integer) 255 127.0.0.1:6379> eval 'return bit.tohex(422342)' 0 "000671c6"
它支持其他几个功能: bit.tobit,bit.tohex,bit.bnot,bit.band,bit.bor,bit.bxor, bit.lshift,bit.rshift,bit.arshift,bit.rol,bit.ror,bit.bswap。所有可用的功能都记录在Lua BitOp文档中
redis.sha1hex
执行输入字符串的SHA1。
例:
127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo" "0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
从脚本发出Redis日志
可以使用redis.log函数从Lua脚本写入Redis日志文件 。
redis.log(loglevel,message)
loglevel 是其中之一:
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
它们直接对应于正常的Redis日志级别。只有使用等于或大于当前配置的Redis实例日志级别的日志级别通过脚本发出的日志才会被发出。
该message参数是一个简单的字符串。例:
redis.log(redis.LOG_WARNING,"Something is wrong with this script.")
将生成以下内容:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
沙箱和最大的执行时间
脚本不应尝试访问外部系统,如文件系统或任何其他系统调用。脚本只能在Redis数据上运行并传递参数。
脚本也受到最大执行时间(默认为5秒)的限制。这个默认的超时时间很长,因为一个脚本通常应该运行在毫秒以下。限制主要是为了处理在开发过程中产生的意外的无限循环。
可以通过redis.conf或使用CONFIG GET / CONFIG SET命令修改脚本以毫秒级精度执行的最长时间。影响最大执行时间的配置参数被调用 lua-time-limit。
当脚本达到超时时,不会由Redis自动终止,因为这违反了Redis与脚本引擎之间的合约,以确保脚本是原子的。中断脚本意味着可能将数据集保留为半写入数据。由于这个原因,当脚本执行超过指定的时间时,会发生以下情况:
- Redis记录脚本运行时间过长。
- 它开始再次接受来自其他客户端的命令,但将向所有发送正常命令的客户端回复BUSY错误。唯一允许的命令是SCRIPT KILL和SHUTDOWN NOSAVE。
- 可以使用SCRIPT KILL命令终止一个只执行只读命令的脚本。这并不违反脚本语义,因为没有数据被脚本写入数据集。
- 如果脚本已经调用了写入命令,则唯一允许的命令变为 SHUTDOWN NOSAVE停止服务器而不保存磁盘上的当前数据集(基本上服务器被中止)。
EVALSHA在流水线的上下文中
在流水线请求的上下文中执行EVALSHA时要小心,因为即使在流水线中,也必须保证命令的执行顺序。如果EVALSHA将返回一个NOSCRIPT错误,则该命令不能在稍后重新发布,否则违反了执行顺序。
客户端库实现应采取以下方法之一:
- 在管道环境中始终使用简单的EVAL。
- 累积所有要发送到管道中的命令,然后检查EVAL 命令并使用SCRIPT EXISTS命令检查是否所有脚本都已经定义。如果没有,请根据需要在管道顶部添加SCRIPT LOAD命令,并对所有EVAL呼叫使用EVALSHA。
调试Lua脚本
从Redis 3.2开始,Redis支持原生Lua调试。Redis Lua调试器是一个远程调试器,由一个服务器(Redis本身)和一个默认的客户端组成redis-cli。
Lua调试器在Redis文档的Lua脚本调试部分进行了描述。
参考来源: EVAL – Redis