Redis从2.6版本开始支持Lua脚本,通过在服务器端嵌入Lua环境,客户端可以使用Lua脚本在服务器端原子的执行多个命令。
Redis服务器会单线程原子性执行Lua脚本,保证Lua脚本在处理的过程中不会被任意其它请求打断。
创建并修改Lua环境
为了在Redis服务器中执行Lua脚本,需要在Redis服务器内嵌一个Lua环境。因为Redis在任何时间最多只有一个脚本被放进Lua环境中执行,所以一个Redis服务器只需要创建一个Lua环境。
- 调用
lua_open函数创建一个基本的Lua环境;—— 接下来的步骤都是针对这个Lua环境操作 - 载入多个函数库到
Lua环境;—— Lua脚本基于这些函数库进行数据操作 - 在Lua
Lua中创建一个redis表格全局变量table;—— 包含对Redis进行操作的函数 - 使用
Redis自制的随机函数替换Lua原有的随机函数; - 创建排序函数;—— 对
Redis命令的结果进行排序,消除不确定性
- 什么是不确定性?—— 集合是无序的,即使两个集合包含的元素完全相同,输出的结果也可能不同。
- 不确定性的命令:
sinter、sunion、sdiff、smembers、hkeys、hvals、keys; - 如何消除不确定性:当
Lua脚本执行完一个带有不确定的命令后,会调用table.sort函数对命令的返回结果进行排序,保证相同的数据集总是产生相同的输出结果。
- 创建
redis.pcall函数报告错误; - 对
Lua环境中的全局变量进行保护;—— 防止用户在执行Lua脚本过程中将额外的Lua全局变量添加到Lua环境中 - 将
Lua环境保存到redisServer.lua属性中,等待执行服务器传来的Lua脚本;
如何执行Lua脚本?
伪客户端
因为执行Redis命令必须要有相应的客户端,所以为了执行LuaLua中包含的Redis命令,需要创建一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有命令。
lua_scripts字典
lua_scripts字典主要用于实现script exists命令和脚本复制功能。
Redis服务器使用一个lua_scripts字典保存Lua脚本,key为某个脚本的sha1校验和,value为对应的Lua脚本。所有被eval命令执行过的以及所有被script load命令载入过的Lua脚本保存到lua_scripts字典中。
使用Lua脚本执行命令过程
Lua环境将redis.call/redis.pcall函数的参数(即命令)传给伪客户端;- 伪客户端将脚本想要执行的命令传给命令执行器;
- 命令执行器执行命令,并将结果返回伪客户端;
- 伪客户端接收命令执行器的命令结果,并发送给Lua环境;
Lua环境接收命令结果并返回给redis.call/redis.pcall函数;redis.call/redis.pcall函数将命令结果作为函数返回值发送给脚本调用者;
Lua相关命令
可以通过eval/evalsha命令执行Lua脚本。
eval
eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2
作用
eval命令可以直接对输入的脚本进行求值。
执行过程
- 客户端向服务器发送
eval命令,要求执行某个Lua脚本; - 服务器在Lua
Lua中为客户端传入的Lua脚本定义一个Lua函数; - 将客户端给定的
Lua脚本保存进lua_scripts字典; - 执行定义的
Lua函数;
evalsha
evalsha可以根据脚本的sha1校验和来对脚本请求,但是要求这个脚本必须至少被eval命令执行过一次或这个校验和对应的脚本曾经被script load命令载入过。
每个被eval执行过的命令,在Lua环境中都有一个对应的Lua函数,函数名称为f_ + sha1校验和,函数体是脚本本身。如果某个脚本对应的Lua函数被定义过至少一次,那么只需要知道这个Lua函数的校验和直接调用Lua函数来执行脚本,而不需要具体的脚本内容。
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。
- 当主服务器执行
eval、script load、script flush这三个命令时,会直接将被执行命令传播给所有从服务器。 - 同一个evalsha命令可能在主服务器执行成功,在从服务器执行失败,因为主服务器与从服务器载入的
Lua脚本可能不同。所以Redis要求主服务器在传播evalsha命令时必须确保evalsha要执行的脚本在所有从服务器都载入过,否则主服务器会将evalsha命令转为eval命令再传播给从服务器。 - 主服务器向服务器传播命令时,会使用
redisServer.repl_scriptcacha_dict字典记录已经将哪些脚本传播给了所有从服务器,key为sha1校验和,value为NULL。如果一个校验和存在lua_script而不存在repl_scriptcacha_dict,说明该脚本还未同步到全部从服务器。
使用Lua脚本的好处
- 减少网络开销:可以将多个命令用一个请求完成减少了网络往返时延;
- 原子操作:
Redis会将整个脚本作为一个整体执行,中间不会被其他命令插入; - 复用:客户端发送的脚本会保存在
Redis服务器中,其他客户端可以复用这一脚本;