【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等参数则保持不变。

传播EVALSHA命令的方法