语法
EVAL script numkeys key [key ...] arg [arg ...]
可用版本>= 2.6.0 时间复杂度:依赖脚本的执行。
EVAL介绍
eval和evalsha使用lua解释器评估脚本。
eval的第一个参数是lua5.1脚本,这个脚本不需要定义lua函数,只是一个lua程序运行在redis server 容器内。
eval的第二个参数是key的数量。
eval的第三个参数是 key全局变量,使用KEYS[1],KEYS[2]...
eval的第四个参数是args全局变量,使用ARGV[1],ARGV[2]...
比如,请注意大小写
127.0.0.1:6379>eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 value1 value2 1) "key1" 2) "key2" 3) "value1" 4) "value2" 127.0.0.1:6379> eval "return {keys[1],keys[2],argv[1],argv[2]}" 2 key1 key2 value1 value2 (error) ERR Error running script (call to f_8cfd66635a33779a52579a216e55263704ce398a): @enable_strict_lua:15: user_script:1: Script attempted to access nonexistent global variable 'keys'
注意:你可以使用lua数据返回redis多个回复,redis会做类型转换,后面介绍。
redis.call()和redis.pcall() 及错误处理
可以使用lua脚本调用redis命令,使用redis.call()和redis.pcall() 方法。
127.0.0.1:6379> eval "return redis.pcall('incr','key1')" 0 (integer) 3 127.0.0.1:6379> eval "return redis.call('incr','key1')" (integer) 4
127.0.0.1:6379> eval "return redis.call('set',KEYS[1],ARGV[1])" 1 hello world
OK 127.0.0.1:6379> get hello
"world"
所有的redis命令必须执行前分析,决定哪些keys命令执行。为了确保这点,KEYS必须精确传递。在很多方面是有用的,但是特别确保redis集群转发请求到合适的集群节点。
这两个方法很类似,唯一的区别是如果redis命令导致错误,redis.call()会上抛lua错误,会强制eval给命令调用者返回错误信息,但是redis.pcall()会捕获异常返回lua表精确错误信息(使用lua表的err字段)。
举例说明
127.0.0.1:6379> eval "return redis.call('incre','key1')" 0 (error) ERR Error running script (call to f_759ca743926970d3713f91641f418515a32ddafb): @user_script:1: @user_script: 1: Unknown Redis command called from Lua script 127.0.0.1:6379> eval "return redis.pcall('incre','key1')" 0
(error) @user_script: 1: Unknown Redis command called from Lua script
lua和redis数据类型转换
当lua调用redis.call()或者pcall()函数,redis返回数据转化为lua数据类型。例如,当调用redis命令和lua脚本返回值时,lua数据类型被转换为redis协议,所以lua脚本可以控制eval返回给客户端的数据类型。
redis to lua 转换表
- redis 整型返回 -> lua数字型
- redis 批量返回 -> lua 字符串
- redis 多个批量返回 -> lua表「内部可能有其他的redis数据类型」
- redis 状态回执 -> lua表只有一个ok字段,包含状态信息
- redis 错误回执 -> lua表只有一个error字段,包含错误信息
- redis nil 批量返回 和 Nil 多个批量返回 -> lua false 布尔类型
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 boolean true -> Redis integer reply with value of 1.
127.0.0.1:6379> eval "return false" 0 (nil) 127.0.0.1:6379> eval "return true" 0 (integer) 1 127.0.0.1:6379> eval "return {err='hello world'}" 0 (error) hello world 127.0.0.1:6379> eval "return {ok='hello world'}" 0 hello world
最后,有三个需要特别注意的规则:
- lua有单独数字型类型,lua Numbers。不会区分整形和浮点型,所以通常将lua numbers 转换为integer ,移除小数部分。如果你想从lua脚本中返回浮点型,应该返回string 类型。
127.0.0.1:6379> eval "return '1233.233'" 0 "1233.233" 127.0.0.1:6379> eval "return 1233.233" 0 (integer) 1233
- 如果lua数组遇到nil,转换会停止。
127.0.0.1:6379> eval 'return {12,234,nil,3,2}' 0 1) (integer) 12 2) (integer) 234
- 当lua表包含key及他们value,redis转换不会包括。
RESP3 模式转换规则:注意lua引擎可以工作在RESP3模型,使用redis6协议,在这种场景下,还有额外的转换规则,
redis返回类型帮助函数
lua返回类型函数
- redis.error_reply(error_string) 返回指定的错误信息,字符串,设置err的值。下面两种用法一致。
127.0.0.1:6379> eval "return {err='hello world'}" 0 (error) hello world 127.0.0.1:6379> eval "return redis.error_reply('hello world')" 0
(error) hello world
- redis.status_reply(status_string) 返回状态回执,设置ok的值。下面两种用法一致。
127.0.0.1:6379> eval "return {ok='hello'}" 0 hello 127.0.0.1:6379> eval "return redis.status_reply('hello')" 0 hello
脚本的原子性
redis使用相同的lua解释器运行所有的命令,当然也保证按照原子的方式脚本执行:没有其他的脚本或者命令被执行,当有脚本在执行中。语法类似于muilt/exec。从这个观点上说,其他的客户端要么看到脚本已经完成要么不可见。然而这也意味着执行慢脚本不是好的主意。创建快速的脚本不是困难的事情,但是如果你使用慢脚本你需要了解当这个脚本在执行时,其他的脚本是阻塞的。
evalsha和带宽
eval命令强制客户端一次又一次发送脚本,redis没有必要每次重新编译脚本,使用网略缓存机制,然而在大量脚本内容时,消息额外的带宽并不是最有的。另外一方面,定义命令使用特别的命令或者redis.conf可能会导致一些问题,原因如下:
- 不同的实例可能实现不同的命令。
- 在分布式环境中,部署是困难的,必须确保所有的实例包含给定的命令。
- 读取应用代码,但是脚本命令定义在服务端,对于应用来说已完成的语法可能是不明朗的。
为了解决这些问题,redis提供evalsha命令。evalsha和eval相似,只是替换eval第一个lua脚本参数,转而替代的是lua脚本的sha1字符串(这么做对于用户来说就直观明了吗?真有意思)。
- 如果服务端保存脚本其对应的sha1摘要,脚本会被执行。
- 如果服务脚本其对应的sha1摘要没有被保存,指定的错误信息返回(如下面例子),告诉客户端私用eval代替。
注意sha1摘要的内容是lua脚本,不加双引号,如下面的例子sha1(return redis.pcall('get',KEYS[1])=b8fadcd4ae40c7a8814847ab7a5e202b3bcca756
127.0.0.1:6379> set key4 123 OK 127.0.0.1:6379> eval "return redis.pcall('get',KEYS[1])" 1 key4 "123" 127.0.0.1:6379> evalsha d7c9b2e2bdc79e3a69e21b26316bb62031861e13 1 key4## 这个是加上双引号的摘要 (error) NOSCRIPT No matching script. Please use EVAL.
127.0.0.1:6379> evalsha b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1 key4 "123"
脚本缓存语法
执行的脚本被保证永远保存在redis实例中。这意味着如果eval被执行,在当前实例中执行evalsha也会成功。脚本缓存很长一段时间的原因是因为不会像其他大量写应用导致内存问题。每个脚本在概念上像实现新的命令,尽管一个大的应用可能拥有成百上千个脚本,尽管应用可以多次修改及脚本变化,但是内存的使用是极少的。
只有使用srcipt flush 命令刷新脚本缓存,刷新脚本移除目前已经执行的脚本。当然,之前提到,重启redis实例刷新脚本缓存,这不是持久性的。然后从这个观点来看,客户端只有两种方式确定redis实例没有重启,两种不同的命令
- 客户端和服务端持久化的,不会被关闭。
- 客户端明确的监测运行ID文件使用info命令,确保服务没有重启同时在同一个程序中。
可以使用script load 加载所有可能出现在管道的脚本,可以直接使用evalsha命令,不需要任何校验检查,不会出现脚本未识别的错误。
脚本命令
- script flush redis刷新脚本缓存的唯一方式。在云环境中特别有用,相同的实例可以分配给不同的用户。特使客户端库脚本功能的实现。
127.0.0.1:6379> set key4 123 OK 127.0.0.1:6379> eval "return redis.pcall('get',KEYS[1])" 1 key4 "123" 127.0.0.1:6379> evalsha b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1 key4 "123" 127.0.0.1:6379> script flush OK 127.0.0.1:6379> evalsha b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1 key4 (error) NOSCRIPT No matching script. Please use EVAL. 127.0.0.1:6379> eval "return redis.pcall('get',KEYS[1])" 1 key4 "123"
- script exists sha1 sha2 ... shaN 这个命令的参数是sha1列表集合,返回值是1或者0,1:指定sha1是可以被辨识的脚本,已经存在脚本缓存中,0意味着这个脚本之前不可见(或者在执行script flush命令后没有被执行 )。
127.0.0.1:6379> eval "return redis.pcall('get',KEYS[1])" 1 key4 "123" 127.0.0.1:6379> script exists b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1) (integer) 1 127.0.0.1:6379> script exists b8fadcd4ae40c7a8814847ab7a5e202b3bcca757 1) (integer) 0 127.0.0.1:6379> script flush OK 127.0.0.1:6379> script exists b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1) (integer) 0
- script load 用于注册指定的脚本到脚本缓存中。不需要实际执行脚本。
127.0.0.1:6379>script flush OK 127.0.0.1:6379> script exists b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1) (integer) 0 127.0.0.1:6379> evalsha b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1 key4 (error) NOSCRIPT No matching script. Please use EVAL. 127.0.0.1:6379> script load "return redis.pcall('get',KEYS[1])" "b8fadcd4ae40c7a8814847ab7a5e202b3bcca756" 127.0.0.1:6379> evalsha b8fadcd4ae40c7a8814847ab7a5e202b3bcca756 1 key4 "123"
- script kill 是打断长时间运行脚本的唯一方式,脚本的执行时间已经超过配置的最大执行时间。脚本script Kill命令只能用于处理无修改数据的过程。