简介:
redis脚本使用lua解释器来执行脚本。
常用命令:
1.eval
2.evalsha
3.script_load
4.script_exists
5.script_flush
6.script_kill
命令详解:
1.eval
可用版本: >=2.6.0
时间复杂度: 寻找要被执行脚本的复杂度为O(1)
命令格式: eval script numkeys key [key ...] arg [arg ...]
命令介绍:
redis2.6.0版本之后,通过内置的lua内置器,可以使用eval命令对lua脚本进行求值。
script参数是一段lua5.1脚本程序,它会运行在redis服务器上下文中,这脚本不必(也不应该)定位为一个lua函数。
number参数用来指定键名参数的个数。
键名参数key [key ...]从eval的第三个参数开始,表示在脚本中用到的redis-key,这些参数可以在lua脚本中通过全局变量keys数组获取,用以1开始的下标访问keys[1]、keys[2]。
在命令的最后,那些不是键名参数的附加参数arg [arg ...],可以在Lua中通过全局变量ARGV数组访问,访问形式与KEYS变量类似。
> 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:
在Lua脚本中,可以使用两个不同的函数来执行redis命令(redis.call()/redis.pcall())。
这两个函数唯一的不同在于对于错误信息的处理。
> eval "return redis.call('set',KEYS[1],'bar')" 1 foo OK
数据转换:
当 Lua 通过 call() 或 pcall() 函数执行 Redis 命令的时候,命令的返回值会被转换成 Lua 数据结构。同样地,当 Lua 脚本在 Redis 内置的解释器里运行时,Lua 脚本的返回值也会被转换成 Redis 协议(protocol),然后由 EVAL 将值返回给客户端。
数据类型之间的转换遵循这样一个设计原则:如果将一个 Redis 值转换成 Lua 值,之后再将转换所得的 Lua 值转换回 Redis 值,那么这个转换所得的 Redis 值应该和最初时的 Redis 值一样。
换句话说, Lua 类型和 Redis 类型之间存在着一一对应的转换关系。
脚本原子性:
Redis 使用单个 Lua 解释器去运行所有脚本,并且, Redis 也保证脚本会以原子性(atomic)的方式执行:当某个脚本正在运行的时候,不会有其他脚本或 Redis 命令被执行。这和使用 MULTI / EXEC 包围的事务很类似。在其他别的客户端看来,脚本的效果(effect)要么是不可见的(not visible),要么就是已完成的(already completed)。
另一方面,这也意味着,执行一个运行缓慢的脚本并不是一个好主意。写一个跑得很快很顺溜的脚本并不难,因为脚本的运行开销(overhead)非常少,但是当你不得不使用一些跑得比较慢的脚本时,请小心,因为当这些蜗牛脚本在慢吞吞地运行的时候,其他客户端会因为服务器正忙而无法执行命令。
错误处理:
当 redis.call() 在执行命令的过程中发生错误时,脚本会停止执行,并返回一个脚本错误,错误的输出信息会说明错误造成的原因:
redis> lpush foo a (integer) 1 redis> eval "return redis.call('get', 'foo')" 0 (error) ERR Error running script (call to f_282297a0228f48cd3fc6a55de6316f31422f5d17): ERR Operation against a key holding the wrong kind of value
redis.pcall() 出错时并不引发(raise)错误,而是返回一个带 err 域的 Lua 表(table),用于表示错误:
redis 127.0.0.1:6379> EVAL "return redis.pcall('get', 'foo')" 0 (error) ERR Operation against a key holding the wrong kind of value
带宽与evalsha:
EVAL 命令要求你在每次执行脚本的时候都发送一次脚本主体(script body)。Redis 有一个内部的缓存机制,因此它不会每次都重新编译脚本,不过在很多场合,付出无谓的带宽来传送脚本主体并不是最佳选择。
为了减少带宽的消耗, Redis 实现了 EVALSHA 命令,它的作用和 EVAL 一样,都用于对脚本求值,但它接受的第一个参数不是脚本,而是脚本的 SHA1 校验和(sum)。
EVALSHA 命令的表现如下:
如果服务器还记得给定的 SHA1 校验和所指定的脚本,那么执行这个脚本
如果服务器不记得给定的 SHA1 校验和所指定的脚本,那么它返回一个特殊的错误,提醒用户使用 EVAL 代替 EVALSHA
> 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 保证所有被运行过的脚本都会被永久保存在脚本缓存当中,这意味着,当 EVAL 命令在一个 Redis 实例上成功执行某个脚本之后,随后针对这个脚本的所有 EVALSHA 命令都会成功执行。
刷新脚本缓存的唯一办法是显式地调用 SCRIPT FLUSH 命令,这个命令会清空运行过的所有脚本的缓存。通常只有在云计算环境中,Redis 实例被改作其他客户或者别的应用程序的实例时,才会执行这个命令。
缓存可以长时间储存而不产生内存问题的原因是,它们的体积非常小,而且数量也非常少,即使脚本在概念上类似于实现一个新命令,即使在一个大规模的程序里有成百上千的脚本,即使这些脚本会经常修改,即便如此,储存这些脚本的内存仍然是微不足道的。
事实上,用户会发现 Redis 不移除缓存中的脚本实际上是一个好主意。比如说,对于一个和 Redis 保持持久化链接(persistent connection)的程序来说,它可以确信,执行过一次的脚本会一直保留在内存当中,因此它可以在流水线中使用 EVALSHA 命令而不必担心因为找不到所需的脚本而产生错误(稍候我们会看到在流水线中执行脚本的相关问题)。
script命令:
Redis 提供了以下几个 SCRIPT 命令,用于对脚本子系统(scripting subsystem)进行控制:
1)script flush:清除所有脚本缓存
2)script exists sha1 [sha1 ...]:根据给定的脚本校验和,检测指定的脚本是否存在与脚本缓存中
3)script load script:将一个脚本写入脚本缓存,但是不运行它
4)script kill:杀死当前正在运行的脚本
redis日志:
在Lua脚本中,可以通过使用redis.log函数来写redis的日志:
redis.log(loglevel, message)
其中,message参数是一个字符串,而loglevel参数可以是:
1)redis.LOG_DEBUG
2)redis.LOG_VERBOSE
3)redis.LOG_NOTICE
4)redis.LOG_WARNING
沙箱与最大执行时间:
脚本应该仅仅用于传递参数和对 Redis 数据进行处理,它不应该尝试去访问外部系统(比如文件系统),或者执行任何系统调用。
除此之外,脚本还有一个最大执行时间限制,它的默认值是 5 秒钟,一般正常运作的脚本通常可以在几分之几毫秒之内完成,花不了那么多时间,这个限制主要是为了防止因编程错误而造成的无限循环而设置的。
最大执行时间的长短由 lua-time-limit 选项来控制(以毫秒为单位),可以通过编辑 redis.conf 文件或者使用 CONFIG GET parameter 和 CONFIG SET parameter value 命令来修改它。
当一个脚本达到最大执行时间的时候,它并不会自动被 Redis 结束,因为 Redis 必须保证脚本执行的原子性,而中途停止脚本的运行意味着可能会留下未处理完的数据在数据集(data set)里面。
因此,当脚本运行的时间超过最大执行时间后,以下动作会被执行:
1)Redis 记录一个脚本正在超时运行
2)Redis 开始重新接受其他客户端的命令请求,但是只有 SCRIPT KILL 和 SHUTDOWN NOSAVE 两个命令会被处理,对于其他命令请求, Redis 服务器只是简单地返回 BUSY 错误。
3)可以使用 SCRIPT KILL 命令将一个仅执行只读命令的脚本杀死,因为只读命令并不修改数据,因此杀死这个脚本并不破坏数据的完整性
4)如果脚本已经执行过写命令,那么唯一允许执行的操作就是 SHUTDOWN NOSAVE ,它通过停止服务器来阻止当前数据集写入磁盘
2.evalsha
可用版本: >=2.6.0
时间复杂度: 根据脚本的复杂度而定
命令格式: evalsha sha1 numkeys key [key ...] arg [arg ...]
作用:
根据给定的sha1校验码,对缓存在服务器中的脚本进行求值。
参数传入方式与eval一致。
返回值:
脚本执行的返回值。
3.script_load
可用版本: >=2.6.0
时间复杂度: O(N) , N 为脚本的长度(以字节为单位)
命令格式: script load script
作用:
将脚本script添加到脚本缓存中,但并不执行这个脚本。
eval 命令也会将脚本添加到脚本缓存中,但是会立刻执行这个脚本。
如果给定的脚本已经在缓存里面了,那么不做动作。
返回值:
给script的SHA1校验和。
4.script_exists
可用版本: >=2.6.0
时间复杂度: O(N) , N 为给定的 SHA1 校验和的数量
命令格式: script exists sha1 [sha1 ...]
作用:
给定一个或者多个脚本的SHA1校验和,返回一个包含0/1列表,表示校验和所指定的脚本是否已经被保存在缓存当中。
返回值:
一个列表,包含 0 和 1 ,前者表示脚本不存在于缓存,后者表示脚本已经在缓存里面了。 列表中的元素和给定的 SHA1 校验和保持对应关系,比如列表的第三个元素的值就表示第三个 SHA1 校验和所指定的脚本在缓存中的状态。
5.script_flush
可用版本: >=2.6.0
时间复杂度: O(N) , N 为缓存中脚本的数量
命令格式: script flush
作用:
清除所有Lua脚本缓存。
返回值:
总是返回OK。
6.script_kill
可用版本: >=2.6.0
时间复杂度: O(1)
命令格式: script kill
作用:
杀死当前正在运行的 Lua 脚本,当且仅当这个脚本没有执行过任何写操作时,这个命令才生效。
这个命令主要用于终止运行时间过长的脚本,比如一个因为 BUG 而发生无限 loop 的脚本,诸如此类。
返回值:
执行成功返回 OK ,否则返回一个错误。