在线时间:8:00-16:00
迪恩网络APP
随时随地掌握行业动态
扫描二维码
关注迪恩网络微信公众号
Lua 脚本功能是 Reids 2.6 版本的最大亮点, 通过在服务器中内嵌对 Lua 环境的支持,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。 Redis 解决了长久以来不能高效地处理 CAS (check-and-set)命令的缺点, 并且可以通过组合使用多个命令, 轻松实现以前很难实现或者不能高效实现的模式。 脚本的执行基本用法:
在脚本环境的初始化工作完成以后, Redis 就可以通过 EVAL 命令或 EVALSHA 命令执行 Lua 脚本了。 1.1 EVAL script numkeys key [key ...] arg [arg ...] numkeys 是key的个数,后边接着写key1 key2... val1 val2.... 其中, EVAL 直接对输入的脚本代码体(body)进行求值: 127.0.0.1:6379> eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 val1 val2 1) "key1" 2) "key2" 3) "val1" 4) "val2" 1.2 SCRIPT LOAD script 把脚本加载到脚本缓存中,返回SHA1校验和。但不会立马执行,举例 127.0.0.1:6379> SCRIPT LOAD "return 'hello world'" "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
1.3 EVALSHA sha1 numkeys key [key ...] arg [arg ...] 根据缓存码执行脚本内容。 EVALSHA 则要求输入某个脚本的 SHA1 校验和, 这个校验和所对应的脚本必须至少被 EVAL 执行过一次: redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 // 上一个脚本的校验和
"hello world"
或者曾经使用 SCRIPT LOAD 载入过这个脚本: redis> SCRIPT LOAD "return 'dlrow olleh'"
"d569c48906b1f4fca0469ba4eee89149b5148092"
redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0
"dlrow olleh"
因为 EVALSHA 是基于 EVAL 构建的, 所以下文先用一节讲解 EVAL 的实现, 之后再讲解 EVALSHA 的实现。 1.4 SCRIPT EXISTS script [script ...] 通过sha1校验和判断脚本是否在缓存中 1.5 SCRIPT FLUSH 清空缓存 127.0.0.1:6379> SCRIPT LOAD "return 'hello jihite'" "3a43944275256411df941bdb76737e71412946fd" 127.0.0.1:6379> SCRIPT EXISTS "3a43944275256411df941bdb76737e71412946fd" 1) (integer) 1 127.0.0.1:6379> SCRIPT FLUSH OK 127.0.0.1:6379> SCRIPT EXISTS "3a43944275256411df941bdb76737e71412946fd" 1) (integer) 0 1.6 SCRIPT KILL 杀死目前正在执行的脚本 一、初始化 Lua 环境在初始化 Redis 服务器时, 对 Lua 环境的初始化也会一并进行。 为了让 Lua 环境符合 Redis 脚本功能的需求, Redis 对 Lua 环境进行了一系列的修改, 包括添加函数库、更换随机函数、保护全局变量, 等等。 整个初始化 Lua 环境的步骤如下:
以上就是 Redis 初始化 Lua 环境的整个过程, 当这些步骤都执行完之后, Redis 就可以使用 Lua 环境来处理脚本了。 严格来说, 步骤 1 至 8 才是初始化 Lua 环境的操作, 而步骤 9 和 10 则是将 Lua 环境关联到服务器的操作, 为了按顺序观察整个初始化过程, 我们将两种操作放在了一起。 另外, 步骤 6 用于创建无副作用的脚本, 而步骤 7 则用于去除部分 Redis 命令中的不确定性(non deterministic), 关于这两点, 请看下面一节关于脚本安全性的讨论。 二、脚本的安全性当将 Lua 脚本复制到附属节点, 或者将 Lua 脚本写入 AOF 文件时, Redis 需要解决这样一个问题: 如果一段 Lua 脚本带有随机性质或副作用, 那么当这段脚本在附属节点运行时, 或者从 AOF 文件载入重新运行时, 它得到的结果可能和之前运行的结果完全不同。 考虑以下一段代码, 其中的 # 虚构例子,不会真的出现在脚本环境中
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
redis> GET number
"10086"
现在, 假如 EVAL 的代码被复制到了附属节点 SLAVE , 因为 # 虚构例子,不会真的出现在脚本环境中
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
redis> GET number
"65535"
可以看到, 带有随机性的写入脚本产生了一个严重的问题: 它破坏了服务器和附属节点数据之间的一致性。 当从 AOF 文件中载入带有随机性质的写入脚本时, 也会发生同样的问题。 只有在带有随机性的脚本进行写入时, 随机性才是有害的。 如果一个脚本只是执行只读操作, 那么随机性是无害的。 比如说, 如果脚本只是单纯地执行 和随机性质类似, 如果一个脚本的执行对任何副作用产生了依赖, 那么这个脚本每次执行所产生的结果都可能会不一样。 为了解决这个问题, Redis 对 Lua 环境所能执行的脚本做了一个严格的限制 —— 所有脚本都必须是无副作用的纯函数(pure function)。 为此,Redis 对 Lua 环境做了一些列相应的措施:
经过这一系列的调整之后, Redis 可以保证被执行的脚本:
三、EVAL 命令的实现EVAL 命令的执行可以分为以下步骤:
以下两个小节分别介绍这两个步骤。 3.1 定义 Lua 函数所有被 Redis 执行的 Lua 脚本, 在 Lua 环境中都会有一个和该脚本相对应的无参数函数: 当调用 EVAL 命令执行脚本时, 程序第一步要完成的工作就是为传入的脚本创建一个相应的 Lua 函数。 举个例子, 当执行命令 function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
end
其中, 函数名以 以函数为单位保存 Lua 脚本有以下好处:
在为脚本创建函数前,程序会先用函数名检查 Lua 环境,只有在函数定义未存在时,程序才创建函数。重复定义函数一般并没有什么副作用,这算是一个小优化。 另外,如果定义的函数在编译过程中出错(比如,脚本的代码语法有错), 那么程序向用户返回一个脚本错误, 不再执行后面的步骤。 3.2 执行 Lua 函数在定义好 Lua 函数之后, 程序就可以通过运行这个函数来达到运行输入脚本的目的了。 如何执行Lua脚本? lua_scripts字典 Redis服务器使用一个lua_scripts字典保存Lua脚本,key为某个脚本的sha1校验和,value为对应的Lua脚本。所有被eval命令执行过的以及所有被script load命令载入过的Lua脚本保存到lua_scripts字典中。 不过, 在此之前, 为了确保脚本的正确和安全执行, 还需要执行一些设置钩子、传入参数之类的操作, 整个执行函数的过程如下:
以下是执行 发送命令请求
EVAL "return 'hello world'" 0
Caller ----------------------------------------> Redis
为脚本 "return 'hello world'"
创建 Lua 函数
Redis ----------------------------------------> Lua
绑定超时处理钩子
Redis ----------------------------------------> Lua
执行脚本函数
Redis ----------------------------------------> Lua
返回函数执行结果(一个 Lua 值)
Redis <---------------------------------------- Lua
将 Lua 值转换为 Redis 回复
并将结果返回给客户端
Caller <---------------------------------------- Redis
上面这个图可以作为所有 Lua 脚本的基本执行流程图, 不过它展示的 Lua 脚本中不带有 Redis 命令调用: 当 Lua 脚本里本身有调用 Redis 命令时(执行 举个例子, 以下是执行命令 发送命令请求
EVAL "return redis.call('DBSIZE')" 0
Caller ------------------------------------------> Redis
为脚本 "return redis.call('DBSIZE')"
创建 Lua 函数
Redis ------------------------------------------> Lua
绑定超时处理钩子
Redis ------------------------------------------> Lua
执行脚本函数
Redis ------------------------------------------> Lua
执行 redis.call('DBSIZE')
Fake Client <------------------------------------- Lua
伪客户端向服务器发送
DBSIZE 命令请求
Fake Client -------------------------------------> Redis
服务器将 DBSIZE 的结果
(Redis 回复)返回给伪客户端
Fake Client <------------------------------------- Redis
将命令回复转换为 Lua 值
并返回给 Lua 环境
Fake Client -------------------------------------> Lua
返回函数执行结果(一个 Lua 值)
Redis <------------------------------------------ Lua
将 Lua 值转换为 Redis 回复
并将该回复返回给客户端
Caller <------------------------------------------ Redis
因为 四、EVALSHA 命令的实现前面介绍 EVAL 命令的实现时说过, 每个被执行过的 Lua 脚本, 在 Lua 环境中都有一个和它相对应的函数, 函数的名字由 evalsha可以根据脚本的sha1校验和来对脚本请求,但是要求这个脚本必须至少被eval命令执行过一次或这个校验和对应的脚本曾经被script load命令载入过。 每个被eval执行过的命令,在Lua环境中都有一个对应的Lua函数,函数名称为f_ + sha1校验和,函数体是脚本本身。如果某个脚本对应的Lua函数被定义过至少一次,那么只需要知道这个Lua函数的校验和直接调用Lua函数来执行脚本,而不需要具体的脚本内容。 只要脚本所对应的函数曾经在 Lua 里面定义过, 那么即使用户不知道脚本的内容本身, 也可以直接通过脚本的 SHA1 校验和来调用脚本所对应的函数, 从而达到执行脚本的目的 —— 这就是 EVALSHA 命令的实现原理。 可以用伪代码来描述这一原理: def EVALSHA(sha1):
# 拼接出 Lua 函数名字
func_name = "f_" + sha1
# 查看该函数是否已经在 Lua 中定义
if function_defined_in_lua(func_name):
# 如果已经定义过的话,执行函数
return exec_lua_function(func_name)
else:
# 没有找到和输入 SHA1 值相对应的函数则返回一个脚本未找到错误
return script_error("SCRIPT NOT FOUND")
除了执行 EVAL 命令之外, SCRIPT LOAD 命令也可以为脚本在 Lua 环境中创建函数: redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"
SCRIPT LOAD 执行的操作和前面《定义 Lua 函数》小节描述的一样。 五、EVALSHA 命令的实现除了eval和evalsha之外,Redis中与Lua脚本有关的命令还有四个,script flush、script exists、script load以及script kill命令。 script flush:script flush命令用于清除服务器中所有和Lua脚本相关的信息,释放并重建lua_script字典,关闭现有Lua环境并新建一个Lua环境。 script exists:script exists命令根据输入的sha1校验和查找对应的脚本是否存在lua_script字典中。 script load:script load命令会在Lua环境为脚本创建对应的Lua函数并保存进lua_script字典中。 script kill:如果服务器设置了lua-time-limit选项,那么每次执行Lua脚本之前,服务器都会在Lua环境中设置一个超时处理钩子。超时处理钩子会在脚本执行期间检查脚本执行了多长时间,如果执行时间超过了lua-time-limit选项设置的时长,超时处理钩子将会在脚本执行的间隙查看是否由script kill命令或shutdown命令到达。 如果超时执行的脚本未执行过写入操作,那么客户端可以通过script kill命令让服务器停止执行该脚本,并向客户端返回一个错误回复。 如果超时执行的脚本执行过写入操作,那么客户端只能通过shutdown nosave命令来停止服务器执行该脚本。 六、脚本复制在主从模式下,Lua执行的写命令也会被同步到从服务器,如eval、evalsha、script load、script flush。
使用Lua脚本的好处减少网络开销:可以将多个命令用一个请求完成减少了网络往返时延; 转自:https://redisbook.readthedocs.io/en/latest/feature/scripting.html https://blog.csdn.net/XuDanT/article/details/108083322 |
请发表评论