redis之lua实现原理

时间:2025-02-15 08:28:35

文章目录

  • 创建并修改Lua环境
  • Lua环境协作组件
    • 伪客户端
    • lua scripts字典
  • EVAL命令的实现
    • 定义脚本函数
    • 执行脚本函数
  • EVALSHA命令的实现
  • 脚本管理命令的实现
    • SCRIPT FLUSH
    • SCRIPTEXISTS
    • SCRIPT LOAD
    • SCRIPT KILL
  • 脚本复制
    • 复制 EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令
    • * 复制EVALSHA命令
      • 1.判断传播EVALSHA命令是否安全的方法
      • 清空repl_scriptcache_dict字典
      • 传播EVALSHA命令的方法
  • 总结

Redis从2.6版本开始引人对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器端 原子地执行多个Redis命令

创建并修改Lua环境

为了在Redis服务器中执行Lua脚本,Redis在服务器内嵌了一个Lua环境(environ-ment),并对这个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字典

Redis服务器为Lua环境创建的另一个协作组件是lua_scripts字典,这个字典的键为某个Lua脚本的SHAI校验和(checksum),而字典的值则是SHA
校验和对应的Lua脚本:
``
struct redisServer {

dict *lua_scripts ;

}
``
Redis服务器会将所有被EVAL命令执行过的Lua脚本,以及所有被SCRIPTLOAD命令载人过的Lua脚本都保存到 lua_scripts 字典里面。

lua_scripts字典有两个作用,一个是实现SCRIPTEXISTS命令,另一个是实现脚本复制功能。

EVAL命令的实现

EVAL命令的执行过程可以分为以下三个步骤

  • 1)根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
  • 2)将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用。
  • 3)执行刚刚在Lua环境中定义的函数,以此来执行客户端给定的Lua脚本。

定义脚本函数

当客户端向服务器发送EVAL命令,要求执行某个Lua脚本的时候,服务器首先要做的就是在Lua环境中,为传入的脚本定义一个与这个脚本相对应的Lua函数,其中,Lua函数的名字由f育前级加上脚本的SHA1校验和(四十个字符长)组成,而函数的体(body)则是脚本本身。

使用函数来保存客户端传人的脚本有以下好处:

  • 执行脚本的步骤非常简单,只要调用与脚本相对应的函数即可
  • 通过函数的局部性来让Lua环境保持清洁,减少了垃圾回收的工作量,并且避免了使用全局变量。
  • 如果某个脚本所对应的函数在Lua环境中被定义过至少一次,那么只要记得这个脚本的SHA1校验和,服务器就可以在不知道脚本本身的情况下,直接通过调用Lua函数来执行脚本,这是EVALSHA命令的实现原理,稍后在介绍EVALSHA命令的实现时就会说到这一点。

执行脚本函数

在为脚本定义函数,并且将脚本保存到lua_scripts字典之后,服务器还需要进行一些设置钩子、传入参数之类的准备动作,才能正式开始执行脚本。整个准备和执行脚本的过程如下:

  • 1)将EVAL命令中传人的键名(keyname)参数和脚本参数分别保存到KEYS数组和ARGV数组,然后将这两个数组作为全局变量传人到Lua环境里面。
  • 2)为Lua环境装载超时处理钩子(hook),这个钩子可以在脚本出现超时运行情况时,让客户端通过SCRIPTKLLL命令停止脚本,或者通过SHUTDOWN命令直接关闭服务器。
  • 3)执行脚本函数。
  • 4)移除之前装载的超时钩子。
  • 5)将执行脚本函数所得的结果保存到客户端状态的输出缓冲区里面,等待服务器将结果返回给客户端。
  • 6)对Lua环境执行垃圾回收操作。

EVALSHA命令的实现

根据lua脚本的SHA1校验值执行对应的lua脚本。如果存在则执行,不存在则报错。

脚本管理命令的实现

SCRIPT FLUSH

SCRIPT FLUSH命令用于清除服务器中所有和Lua脚本有关的信息,这个命令会释放并重建lua_scripts字典,关闭现有的Lua环境并重新创建一个新的Lua环境。

SCRIPTEXISTS

SCRIPT EXISTS 命令根据输人的SHA1校验和,检查校验和对应的脚本是否存在于服务器中

SCRIPT LOAD

SCRIPT LOAD命令所做的事情和EVAL命令执行脚本时所做的前两步完全一样:命令首先在Lua环境中为脚本创建相对应的函数,然后再将脚本保存到lua_scripts字典里面。

SCRIPT KILL

如果服务器设置了lua-time-limit配置选项,那么在每次执行Lua脚本之前,服务器都会在Lua环境里面设置一个超时处理钩子(hook)。

超时处理钩子在脚本运行期间,会定期检查脚本已经运行了多长时间,一旦钩子发现脚本的运行时间已经超过了lua-time-limit选项设置的时长,钩子将定期在脚本运行的间隙中,查看是否有SCRIPT KLLL命令或者SHUTDOWN命令到达服务器。

如果超时运行的脚本未执行过任何写人操作,那么客户端可以通过SCRIPT KLL命令来指示服务器停止执行这个脚本,并向执行该脚本的客户端发送一个错误回复。处理完SCRIPT KILL命令之后,服务器可以继续运行。

另一方面,如果脚本已经执行过写人操作,那么客户端只能用SHUTDOWN nosave命令来停止服务器,从而防止不合法的数据被写人数据库中。

脚本复制

与其他普通Redis命令一样,当服务器运行在复制模式之下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括EVAL命令、EVALSHA命令、SCRIPTFLUSH命令,
以及SCRIPTLOAD 命令。

复制 EVAL命令、SCRIPT FLUSH命令和SCRIPT LOAD命令

Redis复制EVAL、SCRIPT FLUSH、SCRIPT LOAD三个命令的方法和复制其他普通Redis命令的方法一样,当主服务器执行完以上 三个命令的其中一个时,主服务器会直接将被执行的命令传播(propagate)给所有从服务器。

* 复制EVALSHA命令

EVALSHA4命令是所有与Lua脚本有关的命令中,复制操作最复杂的一个,因为主服务器与从服务器载入Lua脚本的情况可能有所不同,所以主服务器不能像复制EVAL命令、SCRIPT LOAD命令或者SCRIPT FLUSH命令那样,直接将EVALSHA命令传播给从服务器。对于一个在主服务器被成功执行的EVALSHA命令来说,相同的EVALSHA命令在从服务器执行时却可能会出现脚本未找到(not found)错误。

因为多个从服务器之间载入Lua脚本的情况也可能各有不同,所以即使一个EVALSHA命令可以在某个从服务器成功执行,也不代表这个EVALSHA命令就一定可以在另一个从服务器成功执行。

为了防止以上假设的情况出现,Redis要求主服务器在传播EVALSHA命令的时候,必须确保EVALSHA命令要执行的脚本已经被所有从服务器载人过,如果不能确保这一点的话主服务器会将EVALSHA命令转换成一个等价的EVAL命令,然后通过传播EVAL命令来代替EVALSHA命令。

传播EVALSHA命令,或者将EVALSHA命令转换成EVAL命令,都需要用到服务器状态的lua_scripts字典和repl_scriptcache_dict字典。

1.判断传播EVALSHA命令是否安全的方法

主服务器使用服务器状态的repl_scriptcache_dict字典记录自己已经将哪些脚本传播给了所有从服务器。

repl_scriptcache_dict字典的键是一个个Lua脚本的SHAI校验和,而字典的值则全部都是NULL,当一个校验和出现在repl_scriptcache_dict字典时,说明这个校验和对应的Lua脚本已经传播给了所有从服务器,主服务器可以直接向从服务器传播包含这个SHA1校验和的EVALSHA命令,而不必担心从服务器会出现脚本未找到错误。

如果一个脚本的SHAI校验和存在于lua.scriptss字典,但是却不存在于repl_scriptcache_dict字典,那么说明校验和对应的Lua脚本已经被主服务器载入,但是并没有传播给所有从服务器,如果我们尝试向从服务器传播包含这个SHA1校验和的EVALSHA命令,那么至少有一个从服务器会出现脚本未找到错误。所以需要转化为EVAL命令。

清空repl_scriptcache_dict字典

每当主服务器添加一个新的从服务器时,主服务器都会清空自已的repl_scriptcache_dict字典,这是因为随着新从服务器的出现,repl_scriptcache_dict字典里面记录的脚本已经不再被所有从服务器载人过,所以主服务器会清空repl_scriptcache_dict字典,强制自己重新向所有从服务器传播脚本,从而确保新的从服务器不会出现脚本未找到错误。

传播EVALSHA命令的方法

当主服务器成功在本机执行完一个EVALSHA命令之后,它将根据EVALSHA命令指定的SHA1校验和是否存在于repl_scriptcache_dict字典来决定是向从服务器传播EVALSHA命令还是EVAL命令:

  • 1)如果EVALSHA命令指定的SHA1校验和存在于repl_scriptcache_dict字典,那么主服务器直接向从服务器传播EVALSHA命令。
  • 2)如果EVALSHA命令指定的SHA1校验和不存在于repl_scriptcache_dict字典,那么主服务器会将EVALSHA命令转换成等价的EVAL命令,然后传播这个等价的EVAL命令,并将EVALSHA命令指定的SHAI校验和添加到repl_scriptcache_dict字典里面。

总结

  • Redis服务器在启动时,会对内嵌的Lua环境执行一系列修改操作,从而确保内嵌的Lua环境可以满足Redis在功能性、安全性等方面的需要。
  • Redis服务器专门使用一个伪客户端来执行Lua脚本中包含的Redis命令
  • Redis 使用脚本字典来保存所有被EVAL命令执行过,或者被SCRIPT LOAD命令载入过的Lua脚本,这些脚本可以用于实现SCRIPT EXISTS命令,以及实现脚本复制功能。
  • EVL命令为客户端输人的脚本在Lua环境中定义一个函数,并通过调用这个函数来执行脚本。
  • EVALSHA命令通过直接调用Lua环境中已定义的函数来执行脚本。
  • SCRIPTFLUSH命令会清空服务器lua scripts字典中保存的脚本,并重置Lua环境。
  • SCRIPT EXISTS命令接受一个或多个SHA1校验和为参数,并通过检查lua_scripts字典来确认校验和对应的脚本是否存在。
  • SCRIPT LOAD命令接受一个Lua脚本为参数,为该脚本在Lua环境中创建函数,并将脚本保存到lua_scripts字典中。
  • 服务器在执行脚本之前,会为Lua环境设置一个超时处理钩子,当脚本出现超时运行情况时,客户端可以通过向服务器发送SCRIPT KLLL命令来让钩子停止正在执行的脚本,或者发送SHUTDOWN nosave命令来让钩子关闭整个服务器。
  • 主服务器复制EVAL、SCRIPTFLUSH、SCRIPTLOAD三个命令的方法和复制普通Redis命令一样,只要将相同的命令传播给从服务器就可以了。
  • 主服务器在复制EVALSHA命令时,必须确保所有从服务器都已经载人了EVALSHA命令指定的SHAI校验和所对应的Lua脚本,如果不能确保这一点的话,主服务器会将EVALSHA命令转换成等效的EVAL命令,并通过传播EVAL命令来获得相同的脚本执行效果