【Redis】 Lua脚本
【Redis】 Lua脚本
Metadata
title: 【Redis】 Lua脚本
date: 2023-07-09 13:56
tags:
- 行动阶段/完成
- 主题场景/数据存储
- 笔记空间/KnowladgeSpace/ProgramSpace/BasicsSpace
- 细化主题/数据存储/Redis
categories:
- 数据存储
keywords:
- 数据存储/Redis
description: 【Redis】 Lua脚本
概述
Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端原子地执行多个Redis命令。
- Redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足Redis在功能性、安全性等方面的需要。
- Redis服务器专门使用一个伪客户端来执行Lua脚本中包含的Redis命令。
- Redis使用脚本字典来保存所有被EVAL命令执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
- EVAL命令为客户端输入的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
- EVALSHA命令通过直接调用Lua环境中已定义的函数来执行脚本。
- SCRIPT FLUSH命令会清空服务器lua_scripts字典中保存的脚本,并重置Lua环境。
- SCRIPT EXISTS命令接受一个或多个SHA1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
- SCRIPT LOAD命令接受一个Lua脚本为参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
- 服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送SCRIPT KILL命令来让钩子停止正在执行的脚本,或者发送SHUTDOWN nosave命令来让钩子关闭整个服务器。
- 主服务器复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制普通Redis命令一样,只要将相同的命令传播给从服务器就可以了。
- 主服务器在复制EVALSHA命令时,必须确保所有从服务器都已经载入了EVALSHA命令指定的SHA1校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果。
创建并修改Lua环境
为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境(environment),
并对这个Lua环境进行了一系列修改,从而确保这个Lua环境可以满足Redis服务器的需要。
Redis服务器创建并修改Lua环境的整个过程由以下步骤组成:
1)创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的。
2)载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数据操作。
3)创建全局表格redis,这个表格包含了对Redis进行操作的函数,比如用于在Lua脚本中执行Redis命令的redis.call函数。
4)使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用。
5)创建排序辅助函数,Lua环境使用这个辅佐函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性。
6)创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息。
7)对Lua环境中的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到Lua环境中。
8)将完成修改的Lua环境保存到服务器状态的lua属性中,等待执行服务器传来的Lua脚本。接下来的各个小节将分别介绍这些步骤。
Lua环境协作组件
除了创建并修改Lua环境之外,Redis服务器还创建了两个用于与Lua环境进行协作的组件,它们分别是负责执行Lua脚本中的Redis命令的伪客户端,以及用于保存Lua脚本的lua_scripts字典。
伪客户端
因为执行Redis命令必须有相应的客户端状态,所以为了执行Lua脚本中包含的Redis命令,Redis服务器专门为Lua环境创建了一个伪客户端,并由这个伪客户端负责处理Lua脚本中包含的所有Redis命令。
Lua脚本使用redis.call函数或者redis.pcall函数执行一个Redis命令,需要完成以下步骤:
1)Lua环境将redis.call函数或者redis.pcall函数想要执行的命令传给伪客户端。
2)伪客户端将脚本想要执行的命令传给命令执行器。
3)命令执行器执行伪客户端传给它的命令,并将命令的执行结果返回给伪客户端。
4)伪客户端接收命令执行器返回的命令结果,并将这个命令结果返回给Lua环境。
5)Lua环境在接收到命令结果之后,将该结果返回给redis.call函数或者redis.pcall函数。
6)接收到结果的redis.call函数或者redis.pcall函数会将命令结果作为函数返回值返回给脚本中的调用者。
lua_scripts字典
struct redisServer {
// ...
dict *lua_scripts;
// ...
};
EVAL命令的实现
EVAL命令的执行过程可以分为以下三个步骤:
1)根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
2)将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用。
3)执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。
EVALSHA命令的实现
可以用伪代码来描述这一原理:
def EVALSHA(sha1):
# 拼接出函数的名字
# 例如:f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91
func_name = "f_" + sha1
# 查看这个函数在Lua 环境中是否存在
if function_exists_in_lua_env(func_name):
# 如果函数存在,那么执行它
execute_lua_function(func_name)
else:
# 如果函数不存在,那么返回一个错误
send_script_error("SCRIPT NOT FOUND")
脚本管理命令的实现
SCRIPT FLUSH
SCRIPT FLUSH命令用于清除服务器中所有和Lua脚本有关的信息,这个命令会释放并重建lua_scripts字典,关闭现有的Lua环境并重新创建一个新的Lua环境。
SCRIPT EXISTS
SCRIPT EXISTS命令根据输入的SHA1校验和,检查校验和对应的脚本是否存在于服务器中。
SCRIPT LOAD
SCRIPT LOAD命令所做的事情和EVAL命令执行脚本时所做的前两步完全一样:命令首先在Lua环境中为脚本创建相对应的函数,然后再将脚本保存到lua_scripts字典里面。
SCRIPT KILL
如果服务器设置了lua-time-limit配置选项,那么在每次执行Lua脚本之前,服务器都会在Lua环境里面设置一个超时处理钩子(hook)。
超时处理钩子在脚本运行期间,会定期检查脚本已经运行了多长时间,一旦钩子发现脚本的运行时间已经超过了lua-time-limit选项设置的时长,钩子将定期在脚本运行的间隙中,查看是否有SCRIPT KILL命令或者SHUTDOWN命令到达服务器。
脚本复制
与其他普通Redis命令一样,当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括EVAL命令、EVALSHA命令、SCRIPT FLUSH命令,以及SCRIPT LOAD命令。
复制EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令
复制EVALSHA命令
判断传播EVALSHA命令是否安全的方法
清空repl_scriptcache_dict字典
EVALSHA命令转换成EVAL命令的方法
1)根据SHA1校验和sha1,在lua_scripts字典中查找sha1对应的Lua脚本script。
2)将原来的EVALSHA命令请求改写成EVAL命令请求,并且将校验和sha1改成脚本script,至于numkey s、key、arg等参数则保持不变。