自Redis2.6.0版本起可用。
时间复杂度:取决于执行的脚本。
所有其他参数不应该代表键名称,可以通过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类型。
redis.call()
redis.pcall()
redis.call()类似于redis.pcall(),唯一的区别是,
如果一个Redis命令调用会导致一个错误,redis.call()将引发一个Lua错误,反过来会强制EVAL向命令调用者返回一个错误,
然而redis.pcall将捕捉错误,并返回一个Lua table代表错误。
> eval "return redis.call('set','foo','bar')" 0
OK
上面的脚本将键foo设置为字符串bar。然而,它违反了EVAL命令的语义,因为脚本使用的所有键都应该使用KEYS数组传递:
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo
OK
Lua脚本可以使用一组转换规则,返回从Lua类型转换为Redis协议的值。
Redis到Lua转换表。
Redis integer reply -> Lua number
Redis bulk reply -> Lua string
Redis multi bulk reply -> Lua table (may have other Redis data types nested)
Redis status reply -> Lua table with a single ok field containing the status
Redis error reply -> Lua table with a single err field containing the error
Redis Nil bulk reply and Nil multi bulk reply -> Lua false boolean 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
Lua boolean false -> Redis Nil bulk reply.
还有一个额外的Lua-to-Redis转换规则,它没有对应的Redis到Lua转换规则:
Lua boolean true -> Redis integer reply with value of 1.
还有两个重要的规则要注意:
以下是几个转换示例:
> 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"
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"
有两个helper 函数可以从Lua返回Redis类型。
使用helper 函数或直接以指定的格式返回table 没有区别,所以以下两种形式是等价的:
return {err="My Error"}
return redis.error_reply("My Error")
> 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
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).
另外,如前所述,重新启动Redis实例刷新脚本缓存,这是不持久的。但是从客户端的角度来看,只有两种方法可以确保Redis实例在两个不同的命令之间不重新启动。
实际上,对于客户端来说,简单地假设在给定连接的上下文中,保证缓存脚本在那里,除非管理员明确地调用了SCRIPT FLUSH命令。
用户可以指望Redis不删除脚本的事实在流水线上下文中是语义上有用的。
例如,一个与Redis持久连接的应用程序可以确定,如果脚本一旦被发送,它仍然在内存中,那么EVALSHA就可以在管道中用于这些脚本,而不会由于未知的脚本而产生错误(我们稍后会详细看到这个问题)。
一个常见的模式是调用SCRIPT LOAD来加载将出现在管道中的所有脚本,然后直接在管道中使用EVALSHA,而不需要检查由于脚本哈希不被识别而导致的错误。
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开始,脚本引擎能够复制脚本执行产生的写入命令序列,而不是复制脚本本身。请参阅下一节获取更多信息。在本节中,我们将假设通过发送整个脚本来复制脚本。我们把这个复制模式称为整个脚本复制。
整个脚本复制方法的主要缺点是脚本需要具有以下属性:
比如使用系统时间,调用像RANDOMKEY这样的Redis随机命令 ,或者使用Lua随机数生成器,都可能导致脚本不总是以相同的方式进行评估。
为了在脚本中执行这个行为,Redis执行以下操作:
但是用户仍然可以使用以下简单的技巧编写随机行为的命令。想象一下,我想写一个Redis脚本,用N个随机整数填充一个列表。
我可以从这个小小的Ruby程序开始:
require 'rubygems'
require 'redis'
r = Redis.new
RandomPushScript = < 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 = < 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。
根据用例,这在几个方面是有用的:
为了启用脚本特效复制,您需要在脚本操作之前发出以下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关键字声明要使用的每个变量。
可以像使用普通客户端一样在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/unsigned byte
h/H - signed/unsigned short
l/L - signed/unsigned long
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库在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库在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"
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文档中
执行输入字符串的SHA1。
例:
127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo"
"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
可以使用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与脚本引擎之间的合约,以确保脚本是原子的。中断脚本意味着可能将数据集保留为半写入数据。由于这个原因,当脚本执行超过指定的时间时,会发生以下情况:
在流水线请求的上下文中执行EVALSHA时要小心,因为即使在流水线中,也必须保证命令的执行顺序。如果EVALSHA将返回一个NOSCRIPT错误,则该命令不能在稍后重新发布,否则违反了执行顺序。
客户端库实现应采取以下方法之一:
在管道环境中始终使用简单的EVAL。
累积所有要发送到管道中的命令,然后检查EVAL 命令并使用SCRIPT EXISTS命令检查是否所有脚本都已经定义。如果没有,请根据需要在管道顶部添加SCRIPT LOAD命令,并对所有EVAL呼叫使用EVALSHA。
从Redis 3.2开始,Redis支持原生Lua调试。Redis Lua调试器是一个远程调试器,由一个服务器(Redis本身)和一个默认的客户端组成redis-cli。
Lua调试器在Redis文档的Lua脚本调试部分进行了描述。
参考来源: https://redis.io/commands/eval