EVAL命令介绍
自2.6.0版本以来, redis就自建有lua脚本解释器,EVAL
和EVALSHA
就是用于执行lua脚本的命令.
EVAL
的第一个参数就是lua 5.1脚本. 这个脚本不需要(也不应该)定义一个lua方法来执行. 它只需是一段lua程序, 就能够运行在redis服务的上下文.
EVAL
的第二个参数是代表redis键名称的脚本参数的数量. 这些参数可以通过全局数组变量KEYS
在lua中访问(例如KEYS[1], KEYS[2],…顺便说下, lua数组是从1开始的).
EVAL
的其他额外参数可以通过全局数组ARGV
在lua中访问(例如ARGV[1], ARGV[2],…).
下面就是关于EVAL
的例子
> 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.call()
- redis.pcall()
redis.call()
和redis.pcall()
相似, 唯一的不同是如果redis调用报错, redis.call()
将提升到lua错误, 而且强制返回错误到命令调用者中, 而redis.pcall()
将捕获错误, 并返回一个表示错误的lua表(表是lua中的一种key-value的类型)
redis.call()
和redis.pcall()
方法的参数就是redis命令形式的参数(就是以空格拆分出来)
> eval "return redis.call('set', 'foo', 'bar')" 0
OK
上面的脚本把字符串bar保存到foo这个键中. 然而它违反了EVAL
命令的语义, 因为在脚本中使用的key都应该用KEYS
数组来传递:
> eval "return redis.call('set', KEYS[1], 'bar')" 1 foo
OK
在执行之前, 必须分析所有redis命令, 以确定命令在哪些key上进行操作. 为了保证EVAL
也是如此, keys必须明确传递. 这在很多时候是有用的, 特别是在redis集群环境中确保你的请求能否定向到合适的集群节点中.
注意, 这不是强制的规则, 以便用户可以随意使用redis单实例配置, 但代价是写的脚本会与redis集群不兼容. (所以, 推荐使用KEYS
来传递key)
lua脚本返回的值是通过一系列的转换规则, 从lua类型转换成redis协议得来的.
Lua和Redis之间的数据类型转换
当lua脚本调用redis.call()
或redis.pcall()
的时候, redis的返回值会被转换成lua的数据类型. 类似地, 当lua脚本返回结果的时候, lua的数据类型会被转换成redis的协议, 正因如此, lua脚本才能控制EVAL
将返回什么类型给客户端.
lua和redis类型之间存在一对一的转换关系, 下表展示了所有的转换规则
Redis to Lua转换表
- Redis integer reply -> Lua number
- Redis bulk reply -> Lua string
- Redis multi bulk reply -> Lua table
- Redis status reply -> Lua table(只有一个属性ok, 对应status的值)
- Redis error reply -> Lua table(只有一个属性err, 对应error的值)
- Redis Nil bulk reply -> Lua false boolean type
- Redis Nil multi bulk reply -> Lua false boolean type
Lua to Redis转换表
- Lua number -> Redis integer reply
- Lua string -> Redis bulk reply
- Lua table(array) -> Redis multi bulk reply(如果是array, 会被截断到第一个nil的位置)
- Lua table(只有一个ok属性) -> Redis status reply
- Lua table(只有一个err属性) -> Redis error reply
- Lua boolean false -> Redis Nil bulk reply
还有一个额外的Lua-to-Redis
的规则, 但是没有相对应的Redis-to-Lua
规则:
- Lua boolean true -> Redis integer 1 replay
也有两个重要的规则需要注意:
- Lua有一个数值类型, Lua numbers. 整数和浮点数都没有区别. 所以redis通常是把lua numbers转换成integer replies, 移除了小数点部分. 所以如果你想要在lua脚本中返回一个浮点数, 正确的方式是返回一个字符串类型, 就像redis自己做的那样(
ZSCORE
命令就是如此) - 转换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"
在下面的例子中, 我们能看到浮点数和包含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也不会被返回, 只因为nil在它前面
返回Redis类型的帮助方法
有两个帮助方法, 用于从lua中返回redis类型
redis.error_reply(error_string)
返回一个错误回复. 这个方法仅仅返回一个包含err
属性的tableredis.status_reply(status_string)
返回一个状态回复. 这个方法仅仅返回一个包含ok
属性的table
使用帮助方法和直接返回一个指定格式的table并没有区别, 所以下面的两种方式是等价的:
> return {err="My Error"}
> return redis.error_reply("My Error")
脚本的原子性
Redis使用相同的Lua解释器来运行所有的命令. Redis也保证脚本以原子的方式执行: 当一个脚本正在执行时, 其他的脚本或者redis命令都不会执行. 这语义和MULTI
/EXEX
类似.
然而这也就意味着执行慢脚本会非常糟糕. 创建快脚本并不是件困难的事情, 因为脚本开销非常低. 但是如果你打算使用慢脚本, 你就应该知道当执行慢脚本的时候, 其他客户端都不能再执行命令了.
错误处理
正如已经提到的, 如果redis.call()
执行了错误的命令, 那么将停止执行并返回一个错误. 在某种程度上使得明显知道错误是由脚本生成的:
> 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类型数据.
带宽和EVALSHA
EVAL
命令要求你在每次执行脚本的时候都发送一次脚本体. redis有一个内部的缓存机制, 因此它不会每次都重新编译脚本, 不过在很多场合, 支付额外的带宽开销可能不是最佳的选择.
EVALSHA
的执行很像EVAL
, 但是EVALSHA
的第一个参数是脚本的SHA1摘要, 而不是脚本主体. 它的表现如下:
- 如果服务器缓存中匹配SHA1摘要的脚本, 那么就执行这段脚本
- 如果服务器缓存中没有匹配到SHA1摘要的脚本, 那么就会返回一个错误
例如:
> 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).
客户端代码库通常会优化地发送EVALSHA
命令, 即使事实上是调用EVAL
命令. 在接收到NOSCRIPT
错误后再调用EVAL
命令.
将键和参数作为附加的EVAL参数传递在此上下文中也非常有用, 因为脚本字符串保持不变, 并且可以由Redis有效地缓存.
脚本缓存语义
执行的脚本能够保证永久保存在redis实例的脚本缓存中, 这就意味如果EVAL
在一个redis实例中执行了, 那么后续的EVALSHA
调用(用一个脚本摘要)都会成功.
脚本能够缓存那么长的时间是因为, 一个好的应用程序不可能有那么多不同脚本而造成内存问题. 每个脚本概念上就像是一个新命令的实现, 甚至一个大型应用程序也只大概有几百个脚本. 即使应用程序的脚本被修改了多次, 脚本内存的使用也是微乎其微的.
如果真的需要清除脚本的缓存, 那么可以显式调用SCRIPT FLUSH
命令, 它将完全刷新脚本缓存, 删除到目前为止执行的所有脚本.
脚本命令
Redis提供了控制脚本子系统的脚本命令.
SCRIPT FLUSH
这个命令用于强制redis刷新清除脚本缓存.SCRIPT EXISTS sha1 sha2 … shaN
这个命令用来查询脚本缓存是否存在. 这个命令的参数是脚本SHA1摘要列表, 它将返回1或者0的数组, 而1表示存在, 0表示不存在.SCRIPT LOAD script
这个命令是将指定的脚本注册到redis脚本缓存中.SCRIPT KILL
这个命令用于终止运行时间过长(达到配置的最大脚本执行时间)的脚本.当且仅当这个脚本没有执行过任何写操作时,这个命令才生效.