Available since 2.6.0. 加入版本2.6
Time complexity: Depends on the script that is executed. 时间复杂度: 取决于脚本的执行
出处:http://blog.csdn.net/column/details/redisbanli.html
Introduction to EVAL 介绍EVAL
EVAL and EVALSHA are used to evaluate scripts using the Lua interpreter built into Redis starting from version 2.6.0.
EVAL和EVALSHA是从Redis2.6.0版本使用内置脚本解释器引入的。
The first argument of EVAL is a Lua 5.1 script. The script does not need to define a Lua function (and should not). It is just a Lua program that will run in the context of the Redis server.
EVAL的第一个参数是一个lua.5.1的脚本.这段脚本不需要定义lua方法函数(也不应该定义)。仅仅是一个运行在Redis 服务器的一段lua程序。
The second argument of EVAL is the number of arguments that follows the script (starting from the third argument) that represent Redis key names. This arguments can be accessed by Lua using the KEYS global variable in the form of a one-based array (so KEYS[1]
, KEYS[2]
, ...).
EVAL的第二个参数是一个数字,它表示紧跟着的脚本(第三个参数开始)中前多少个是Redis中的key的名称。这些Redis中的key的名称可以使用lua的数组 KEYS取出value(比如 KEYS[1],KEUS[2],...)。
All the additional arguments should not represent key names and can be accessed by Lua using the ARGV
global variable, very similarly to what happens with keys (soARGV[1]
, ARGV[2]
, ...).
附加参数不代表key的名称并且和keys一样可以使用lua的ARGV全局变量数组访问(比如:ARGV[1],ARGV[2])。
The following example should clarify what stated above:
下面的例子应该可以说明上面的规定:
> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"
Note: as you can see Lua arrays are returned as Redis multi bulk replies, that is a Redis return type that your client library will likely convert into an Array type in your programming language.
注意:正如你所见,lua数组的返回是Redis的多重应答,这是redis的一个返回类型,你的客户端可能会转换成所用程序语言的数组。
It is possible to call Redis commands from a Lua script using two different Lua functions:
使用两个不同的lua函数去调用Redis命令:
redis.call()
redis.pcall()
redis.call()
is similar to redis.pcall()
, the only difference is that if a Redis command call will result in an error, redis.call()
will raise a Lua error that in turn will force EVAL to return an error to the command caller, while redis.pcall
will trap the error and return a Lua table representing the error.redis.call()
and redis.pcall()
functions are all the arguments of a well formed Redis command:> eval "return redis.call('set','foo','bar')" 0
OK
All Redis commands must be analyzed before execution to determine which keys the command will operate on. In order for this to be true for
EVAL, keys must be passed explicitly. This is useful in many ways, but especially to make sure Redis Cluster can forward your request to the appropriate cluster node (Redis Cluster is a work in progress, but the scripting feature was designed in order to play well with it).
Conversion between Lua and Redis data types 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 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 boolean true -> Redis integer reply with value of 1.
- Lua has a single numerical type, Lua numbers. There is no distinction between integers and floats. So we always convert Lua numbers into integer replies, removing the decimal part of the number if any. If you want to return a float from Lua you should return it as a string, exactly like Redis itself does (see for instance the ZSCORE command).
- There is no simple way to have nils inside Lua arrays, this is a result of Lua table semantics, so when Redis converts a Lua array into Redis protocol the conversion is stopped if a nil is encountered.
> 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"
The last example shows how it is possible to receive the exact return value of
redis.call()
or
redis.pcall()
from Lua that would be returned if the command was called directly.
> eval "return {1,2,3.3333,'foo',nil,'bar'}" 0
1) (integer) 1
2) (integer) 2
3) (integer) 3
4) "foo"
As you can see 3.333 is converted into 3, and the
bar
string is never returned as there is a nil before.
Helper functions to return Redis types 返回redis类型的帮助函数
redis.error_reply(error_string)
returns an error reply. This function simply returns the single field table with theerr
field set to the specified string for you.
redis.status_reply(status_string)
returns a status reply. This function simply returns the single field table with theok
field set to the specified string for you.
Atomicity of scripts 脚本的原子性
Redis uses the same Lua interpreter to run all the commands. Also Redis guarantees that a script is executed in an atomic way: no other script or Redis command will be executed while a script is being executed. This semantic is similar to the one of
MULTI
/
EXEC
. From the point of view of all the other clients the effects of a script are either still not visible or already completed.
Error handling 错误处理
redis.call()
resulting in a Redis command error will stop the execution of the script and return an error, in a way that makes it obvious that the error was generated by a script:
> del foo
(integer) 1
> lpush foo a
(integer) 1
> eval "return redis.call('get','foo')" 0
(error) ERR Error running script (call to f_6b1bf486c81ceb7edf3c093f4c48582e3
Using
redis.pcall()
no error is raised, but an error object is returned in the format specified above (as a Lua table with an
err
field). The script can pass the exact error to the user by returning the error object returned by
redis.pcall()
.
Bandwidth and EVALSHA 带宽和EVALSHA
The
EVAL
command forces you to send the script body again and again. Redis does not need to recompile the script every time as it uses an internal caching mechanism, however paying the cost of the additional bandwidth may not be optimal in many contexts.
redis.conf
would be a problem for a few reasons:-
Different instances may have different implementations of a command.
-
Deployment is hard if we have to make sure all instances contain a given command, especially in a distributed environment.
- 使得所有的实例都包含给出的命令部署是很困难的,特别是在分布式系统中。
-
Reading application code, the complete semantics might not be clear since the application calls commands defined server side.
-
If the server still remembers a script with a matching SHA1 digest, the script is executed.
如果服务器记得这个脚本的SHA1摘要,脚本就会被执行。
-
If the server does not remember a script with this SHA1 digest, a special error is returned telling the client to use EVAL instead.
> 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).
The client library implementation can always optimistically send
EVALSHA
under the hood even when the client actually calls
EVAL, in the hope the script was already seen by the server. If the
NOSCRIPT
error is returned
EVAL
will be used instea
NOSCRIPT
的错误在使用EVAL命令。
Script cache semantics 缓存脚本的意义
- The connection we have with the server is persistent and was never closed so far.
- The client explicitly checks the
runid
field in the INFO command in order to make sure the server was not restarted and is still the same process.
The SCRIPT command 脚本命令
Redis offers a SCRIPT command that can be used in order to control the scripting subsystem. SCRIPT currently accepts three different commands:
-
SCRIPT FLUSH. This command is the only way to force Redis to flush the scripts cache. It is most useful in a cloud environment where the same instance can be reassigned to a different user. It is also useful for testing client libraries' implementations of the scripting feature.
SRIPUT FLUSH。这个命令时使Redis清空脚本缓存的唯一方式。在云环境中当同一个实例被重新指定给不同的用户时这个命令是最有用的。在客户端测试脚本的实现时也非常有用。
-
SCRIPT EXISTS sha1 sha2... shaN. Given a list of SHA1 digests as arguments this command returns an array of 1 or 0, where 1 means the specific SHA1 is recognized as a script already present in the scripting cache, while 0 means that a script with this SHA1 was never seen before (or at least never seen after the latest SCRIPT FLUSH command).
SCRIPT EXISTS sha1 sha2...shaN。命令的参数是SHA1摘要的列表,它将返回一个由0或者1组成的数组,1表示对应的脚本已经被服务器认可放入脚本缓存了,0表对应的脚本服务器一直都没有见过(或者至少在最后一次执行SCRIPT FLUSH后从来没有见过)
-
SCRIPT LOAD script. This command registers the specified script in the Redis script cache. The command is useful in all the contexts where we want to make sure that EVALSHA will not fail (for instance during a pipeline or MULTI/EXEC operation), without the need to actually execute the script.
SCRIPT LOAD script. 这个命令是在Redis脚本缓存中缓存指定的脚本。这个命令在我们想确保EVALSHA不管在任何环境下都不会失败时非常有用(比如在管道或者 MULTI/EXEC 操作时),并且加载时它不会去执行脚本。
-
SCRIPT KILL. This command is the only way to interrupt a long-running script that reaches the configured maximum execution time for scripts. The SCRIPT KILL command can only be used with scripts that did not modify the dataset during their execution (since stopping a read-only script does not violate the scripting engine's guaranteed atomicity). See the next sections for more information about long running scripts.
SCRIPT KILL.这个命令是去中断正在运行并且运行时间达到配置最大时间的脚本的唯一方式。这个命令只能使用于脚本执行不修改数据的情况(因为停止脚本不能违反脚本引擎的原子性)。下一小节将看到关于长时间运行脚本的更多信息。
Scripts as pure functions 脚本作为纯粹的方法
The only drawback with this approach is that scripts are required to have the following property:
唯一的缺点是,使用的脚本必须有下面的特点:
- The script always evaluates the same Redis write commands with the same arguments given the same input data set. Operations performed by the script cannot depend on any hidden (non-explicit) information or state that may change as script execution proceeds or between different executions of the script, nor can it depend on any external input from I/O devices.
-
Lua does not export commands to access the system time or other external state.
没有导出命令来访问系统时间或其他外部状态。
-
Redis will block the script with an error if a script calls a Redis command able to alter the data set after a Redis random command like RANDOMKEY,SRANDMEMBER, TIME. This means that if a script is read-only and does not modify the data set it is free to call those commands. Note that a random command does not necessarily mean a command that uses random numbers: any non-deterministic command is considered a random command (the best example in this regard is the TIME command).
修改数据的脚本应该避免调用随机设置数据的命令,比如RANDOMKEY,SRANDMEMBER,TIME。反过来也就是说,如果一个脚本是只读的,不修改数据设置的,它就可以自由地调用这些命令。注意这里说的随机命令并不只是只那些随机数字的命令:任何非确定性的命令都被认为是一个随机命令(最好的例子就是TIME命令)。
-
Redis commands that may return elements in random order, like SMEMBERS(because Redis Sets are unordered) have a different behavior when called from Lua, and undergo a silent lexicographical sorting filter before returning data to Lua scripts. So
redis.call("smembers",KEYS[1])
will always return the Set elements in the same order, while the same command invoked from normal clients may return different results even if the key contains exactly the same elements.随机返回元素的命令,像SMEMBERS(因为Set是无序的)被Lua脚本调用时会有不同的结果,并且返回要经过lua过滤排序。因此 redis.call("smembers",KEY[1]) 总是以相同的顺序返回元素,当同一个命令客户端调用时将返回不同的结果,尽管key包含完全相同的元素。
-
Lua pseudo random number generation functions
math.random
andmath.randomseed
are modified in order to always have the same seed every time a new script is executed. This means that callingmath.random
will always generate the same sequence of numbers every time a script is executed ifmath.randomseed
is not used.
为了每次调用lua的math.random和math.randomseed方法都是一个相同的随机种子,Redis对math.random和mathrandomseed被进行了修正。意思是如果math.randomseed没有被调用,每次调用math.random都生成相同的数字序列。
However the user is still able to write commands with random behavior using the following simple trick. Imagine I want to write a Redis script that will populate a list with N random integers.
但是,使用以下简单的技巧,用户仍然能够编写与随机行为的相关的命令。想象一下,我想写一个Redis的脚本,将N个随机整数填充一个list。
-
I can start with this small Ruby program: 我可以用这个小Ruby程序启动
Every time this script executed the resulting list will have exactly the following elements: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)])
每次执行这个脚本列表将会有确切的以下要素:
In order to make it a pure function, but still be sure that every invocation of the script will result in different random elements, we can simply add an additional argument to the script that will be used in order to seed the Lua pseudo-random number generator. The new script is as follows:> 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))
What we are doing here is sending the seed of the PRNG as one of the arguments. This way the script output will be the same given the same arguments, but we are changing
one of the arguments in every invocation, generating the random seed client-side. The
seed will be propagated as one of the arguments both in the replication link and in the
Append Only File, guaranteeing that the same changes will be generated when the AOF is reloaded or when the slave processes the script.
我们这里做的是发送PRNG的种子作为参数之一,这种方式给定相同的参数输出也将相同,但是每次调用时我都可以在客户端这边改变随机种子。当AOF重载或者从服务器处理脚本时,种子作为复制链接和附加文档的参数传输,保证生成相同的变化。
Note: an important part of this behavior is that the PRNG that Redis implements asmath.random
and math.randomseed
is guaranteed to have the same output regardless of the architecture of the system running Redis. 32-bit, 64-bit, big-endian and
little-endian systems will all produce the same output.
注意:上面的做法最重要的是Redis 的PRNG实现 math.random和math.randomseed能保证Redis运行底层系统都有相同的输出。32-bit, 64-bit, big-endian 和little-endian系统都将产生相同的输出。
Global variables protection 全局变量的保护
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'
Accessing a
non existing
global variable generates a similar error. 使用不存在的全局变量的错误。
Using SELECT inside scripts在脚本中使用SELECT
Available libraries 有效的类库
The Redis Lua interpreter loads the following Lua libraries:
Redis的解释器加载了以下类库:
- base lib.
- table lib.
- string lib.
- math lib.
- debug lib.
- struct lib.
- cjson lib.
- cmsgpack lib.
- bitop lib
- redis.sha1hex function.
struct
struct is a library for packing/unpacking structures within 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
Example:
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
The CJSON library provides extremely fast JSON manipulation within Lua.
CJSON提供了快速操作json的类库。
Example:
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
The cmsgpack library provides simple and fast MessagePack manipulation within Lua.
cmsgpack 提供了简单并且快速的MessagePack操作类库。
Example:
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
The Lua Bit Operations Module adds bitwise operations on numbers. It is available for scripting in Redis since version 2.8.18.
Example:
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"
It supports several other functions:
bit.tobit
,
bit.tohex
,
bit.bnot
,
bit.band
,
bit.bor
,
bit.bxor
,
bit.lshift
,
bit.rshift
,
bit.arshift
,
bit.rol
,
bit.ror
,
bit.bswap
. All available functions are documented in the
Lua BitOp documentation
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 documentation上。
redis.sha1hex
Perform the SHA1 of the input string.
127.0.0.1:6379> eval 'return redis.sha1hex(ARGV[1])' 0 "foo"
"0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33"
Emitting Redis logs from scripts从脚本记录日志
It is possible to write to the Redis log file from Lua scripts using the
redis.log
function.
redis.log(loglevel,message)
loglevel
is one of: loglevel
是下面中的一个
redis.LOG_DEBUG
redis.LOG_VERBOSE
redis.LOG_NOTICE
redis.LOG_WARNING
message
argument is simply a string. Example:redis.log(redis.LOG_WARNING,"Something is wrong with this script.")
Will generate the following: 将生产:
[32343] 22 Mar 15:21:39 # Something is wrong with this script.
Sandbox and maximum execution time 研究最大执行时间
Scripts should never try to access the external system, like the file system or any other system call. A script should only operate on Redis data and passed arguments.
脚本应该不要去使用外部的系统,比如文件或者其他的系统。一个脚本应该仅仅是操作Redis的数据和传递参数。
Scripts are also subject to a maximum execution time (five seconds by default). This default timeout is huge since a script should usually run in under a millisecond. The limit is mostly to handle accidental infinite loops created during development.
脚本也应该受制于一个最大执行时间(缺省是5秒)。因为脚本应该一般运行时间都是毫米级别的,所以这个缺省的时间是很大的。这个限制主要是去处理脚本运行期间可能发生的死循环。
It is possible to modify the maximum time a script can be executed with millisecond precision, either via redis.conf
or using the CONFIG GET / CONFIG SET command. The configuration parameter affecting max execution time is called lua-time-limit
.
- Redis logs that a script is running too long.
- Redis日志脚本运行很长时间。
- It starts accepting commands again from other clients, but will reply with a BUSY error to all the clients sending normal commands. The only allowed commands in this status are SCRIPT KILL and
SHUTDOWN NOSAVE
. - 开始接收来自其他客户端的命令,但是将返回一个BUSY的错误。这时仅仅允许运行的命令是 SCRIPT KILL 和SHUTDOWN NOSAVE
- It is possible to terminate a script that executes only read-only commands using the SCRIPT KILL command. This does not violate the scripting semantic as no data was yet written to the dataset by the script.
- 可能会使用SCRIPT KILL命令去终止只读模式的脚本。因为没有数据的写操作所以没有违反脚本的原子性。
- If the script already called write commands the only allowed command becomes
SHUTDOWN NOSAVE
that stops the server without saving the current data set on disk (basically the server is aborted). - 如果脚本已经调用了写命令,现在唯一允许的命令是 SHUTDOWN NOSAVE,将导致服务器没有保存当前set的数据到硬盘上(主要是Redis 服务器终止了)
EVALSHA in the context of pipelining 在通道中使用EVALSHA
Care should be taken when executing EVALSHA in the context of a pipelined request, since even in a pipeline the order of execution of commands must be guaranteed. IfEVALSHA will return a NOSCRIPT
error the command can not be reissued later otherwise the order of execution is violated.
在管道里使用EVALSHA要小心,因为即使在管道也必须保证命令的执行顺序。如果EVALSHA返回NOSCRIPT的错误,之后就不会再被执行,否则就违反了执行顺序。
The client library implementation should take one of the following approaches:
客户端应该采取下面当中的一种方法处理:
-
Always use plain EVAL when in the context of a pipeline.
在管道环境中总是使用EVAL。
-
Accumulate all the commands to send into the pipeline, then check for EVALcommands and use the SCRIPT EXISTS command to check if all the scripts are already defined. If not, add SCRIPT LOAD commands on top of the pipeline as required, and use EVALSHA for all the EVAL calls.
积累所有的命令到管道里,然后使用EVAL命令运行检测,并且使用SCRIPT EXISTS去检测是否所有的脚本已经明确定义。如果没有,在管道请求头添加SCRIPT LOAD 命令,并且使用EVALSHA 代替所有的VAAL调用。