`

Lua 脚本

阅读更多
        Redis 2.6 版本开始引入对 Lua 脚本的支持,通过在服务器中嵌入 Lua 环境,Redis 客户端可以使用 Lua 脚本,直接在服务端原子地执行多个 Redis 命令。如下所示:
redis> EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 "msg" "hello world"
OK
redis> GET msg
"hello world"

        本文将对这整个过程进行介绍。

        创建并修改 Lua 环境
        为了在 Redis 服务器中执行 Lua 脚本,Redis 服务器内嵌了一个 Lua 环境,并对其进行了一系列修改,以确保其可以满足 Redis 服务器的需要。Redis 服务器按照以下步骤创建并修改 Lua 环境:
        1、创建一个基础的 Lua 环境,之后的所有修改都是针对这个环境进行。
        2、载入多个 Lua 函数库(如基础库、字符串库、数学库等)到 Lua 环境中,让 Lua 脚本可以用于数据操作。
        3、创建全局变量 redis 表格,其中包含了以下函数:
            1)用于在 Lua 脚本中执行 Redis 命令的 redis.call 和 redis.pcall 函数。
            2)用于记录 Redis 日志的 redis.log 函数,以及相应的日志级别常量:redis.LOG_DEBUG,redis.LOG_VERBOSE,redis.LOG_NOTICE,和 redis.LOG_WARNING。
            3)用于计算 SHA1 校验和的 redis.sha1hex 函数。
            4)用于返回错误信息的 redis.error_reply 和 redis.status_reply 函数。
        4、使用 Redis 自制的随机函数替换 Lua 原有的带有副作用的随机函数。这是为了保证相同的脚本可以在不同的机器上产生相同的结果,因为之前载入的 Lua 的数学库 math 中,用于生成随机数的 math.random 和 math.randomseed 函数都是带有副作用的,替换后,除非在脚本中使用 math.randomseed 显示地修改了 seed,否则每次 Lua 环境都使用固定的 math.randomseed(0) 语句来初始化 seed。如下面的代码片段所示:
-- random-with-default-seed.lua

-- math.randomseed(10086)     -- 取消这行注释表示使用指定的 seed 10086

local i = 4
local sed = {}
while (i > 0) do
    seq[i] = math.random(i)
    i = i - 1
end
return seq

        执行结果多次都会得到相同的结果:
$ redis-cli --eval random-with-default-seed.lua
1) (integer) 1
2) (integer) 1
3) (integer) 3
4) (integer) 1

        5、创建排序辅助函数,以便 Lua 环境用来对一部分 Redis 命令的结果进行排序,从而消除这些命令结果的不确定性。这些“带有不确定性的命令”包括:SINTER、SUNION、SDIFF、SMEMBERS、HKEYS、HVALS 和 KEYS,因为这些命令即使包含完全相同的元素,但只是因为元素添加顺序不同,就可能输出顺序不一致的结果。
        6、创建 redis.pcall 函数的错误报告辅助函数,以提供更详细的出错信息。
        7、对 Lua 环境中的全局环境进行保护,防止用户在执行 Lua 脚本的过程中,不会因为忘记使用 local 关键字而将额外的全局变量添加到 Lua 环境中(不过 Redis 并未禁止用户修改已存在的全局变量,如 redis 表格,所以在执行 Lua 脚本时务必小心,以免错误地修改了已存在的全局变量)。
        8、将完成修改的 Lua 环境保存到服务器状态结构 redisServer 的 lua 属性中,等待执行服务器传来的 Lua 脚本。因为 Redis 使用串行化的方式来执行 Redis 命令,所以在任何特定时间里,最多只会有一个脚本能够被放进 Lua 环境里执行,因此,整个 Redis 服务器只需要创建一个 Lua 环境即可。

        Lua 环境协作组件
        除了创建并修改 Lua 环境,Redis 服务器还创建了以下两个用于与 Lua 环境进行协作的组件:
        1、伪客户端。因为执行 Redis 命令必须要有相应的客户端状态,所以为了执行 Lua 脚本中 redis.call 或者 redis.pcall 函数里面包含的 Redis 命令,Redis 服务器专门为 Lua 环境创建了一个伪客户端。
        2、lua_scripts 字典。RedisServer 结构的 lua_scripts 指针属性就指向这个字典,它的键为某个 Lua 脚本的 SHA1 校验和,字典的值则是对应的 Lua 脚本。Redis 服务器会将所有被 EVAL 命令执行过或者被 SCRIPT LOAD 命令载入过的 Lua 脚本都保存到这个字典里面。这个字典有两个作用,一个是实现 SCRIPT EXISTS 命令,另一个是实现 Lua 脚本复制功能,详情见下文内容。

        EVAL 命令的实现
        Redis 命令 EVAL 的执行过程可分为以下三个步骤:
        1、根据客户端给定的 Lua 脚本,在 Lua 环境中定义一个 Lua 函数,其中,Lua 函数的名字由“f_”前缀加上该脚本的 SHA1 校验和(四十个字符长度)组成,而函数体则是脚本本身。使用函数来保存客户端传入的脚本主要有以下好处:
            1)通过函数的局部性来让 Lua 环境保持清洁,减少了垃圾回收的工作量,并且避免使用了全局变量。
            2)如果某个脚本所对应的函数在 Lua 环境中已定义过一次,则只要记得其所对应的 SHA1 校验和,服务器就可以在不知道脚本本身的情况下,直接调用 Lua 函数来执行脚本,这正是 EVALSHA 命令的实现原理。
        2、将客户端给定的脚本保存到 lua_scripts 字典,以便将来进一步使用。
        3、执行刚刚在 Lua 环境中定义的函数,以此来执行客户端给定的 Lua 脚本。不过在正式开始执行之前,服务器还需要进行一些设置钩子、传入参数之类的准备工作,整个准备和执行脚本的过程如下:
            1)将 EVAL 命令中传入的键名参数和脚本参数(如果有的话)分别保存到 KEYS 数组和 ARGS 数组,然后将这两个数组作为全局变量传入到 Lua 环境里面。
            2)为 Lua 环境装载超时处理钩子,以便脚本在出现超时运行的情况时,客户端可以通过 SCRIPT KILL 命令停止脚本,或者通过 SHUTDOWN 命令直接关闭服务器。
            3)执行脚本函数。
            4)移除之前装载的超时钩子。
            5)将执行脚本函数所得的结果保存到客户端状态的输出缓冲区里面,等待服务器将结果返回给客户端。
            6)对 Lua 环境执行垃圾回收操作。

        脚本管理命令的实现
        除了 EVAL 和 EVALSHA 命令外,Redis 中与 Lua 脚本有关的命令还有以下四个:
        1、SCRIPT FLUSH:这个命令用于清除服务器中所有和 Lua 脚本有关的信息,它会释放并重建 lua_scripts 字典,关闭现有的 Lua 环境并重新创建一个新的 Lua 环境。
        2、SCRIPT EXISTS:这个命令可以根据输入的多个 SHA1 校验和来检查对应的 Lua 脚本是否存在于服务器中,这是通过查找 lua_scripts 字典来实现的,返回 1 与 0 表示存在与否。
        3、SCRIPT LOAD:这个命令所做的事情和 EVAL 命令执行脚本时所做的前两步完全一样,即在 Lua 环境中定义函数,然后将脚本保存到 lua_scripts 字典。在这之后,客户端就可以使用 EVALSHA 命令来执行先前载入的脚本了。
        4、SCRIPT KILL:如果服务器设置了 lua-time-limit 配置选项,那么在每次执行 Lua 脚本之前,服务器都会在 Lua 环境里面设置一个超时处理钩子。这个钩子在脚本运行期间,会定期检查脚本已经运行了多长时间,一旦发现运行时间超过了 lua-time-limit 选项设置的时长,它将定期在脚本运行的间隙中,查看是否有 SCRIPT KILL 命令或者 SHUTDOWN 命令到达服务器。如果超时运行的脚本未执行过任何写入操作,那么客户端就可以通过 SCRIPT KILL 命令来指示服务器停止执行这个脚本,并向客户端发送一个错误回复,之后服务器会继续运行。而如果脚本已经执行过写入操作,那么客户端只能用 SHUTDOWN NOSAVE 命令来停止服务器,以防止不合法的数据被写入到数据库中。

        脚本复制
        与其他普通 Redis 命令一样,当服务器运行在复制模式下时,具有写性质的脚本命令也会被复制到从服务器,这些命令包括 EVAL、EVALSHA、SCRIPT FLUSH 和 SCRIPT LOAD 命令。其中,EVALSHA 命令之外的其他三个命令的复制方法同复制其他普通 Redis 命令的方法一样:当主服务器执行完这三个命令中的其中一个时,会直接将这个命令传播给所有的从服务器执行。
        而对于 EVALSHA 命令,因为主服务器与从服务器载入 Lua 脚本的情况可能有所不同,一个在主服务器上可以成功执行的 EVALSHA 命令,在从服务器上执行时却可能会出现脚本未找到错误,所以主服务器不能直接将其传播给所有的从服务器。比如,一个新的从服务器是在主服务器执行完 SCRIPT LOAD 命令后才开始复制主服务器,则它就没有对应的 SHA1,此时执行 EVALSHA 就会出错。
        因此,Redis 要求主服务器在传播 EVALSHA 命令时,必须确保 EVALSHA 命令要执行的脚本已经被所有从服务器载入过,如果不能确保这一点的话,主服务器就会将之转换成一个等价的 EVAL 命令,然后通过传播 EVAL 命令来代替。这两种情况都需要用到服务器状态 redisServer 结构的 lua_scripts 和 repl_scriptcache_dict 字典属性。
        主服务器使用 repl_scriptcache_dict 字典记录自己已经将哪些脚本(使用 EVAL 或 SCRIPT LAOD 命令执行过的)传播给了所有从服务器,该字典的键是一个 Lua 脚本的 SHA1 校验和,字典的值则全部是 NULL。当一个校验和出现在 repl_scriptcache_dict 中时,说明它所对应的 Lua 脚本已经传播给了所有从服务器,所以主服务器可以直接向从服务器传播包含这个 SHA1 的 EVALSHA 命令,而不必担心从服务器会出现脚本未找到错误。否则,主服务器就会将其转换成等价的 EVAL 命令来代替。
        此外,每当主服务器新添加一个从服务器时,主服务器都会清空自己的 repl_scriptcache_dict 字典,因为此时该字典里面记录的脚本已经不再被所有从服务器载入过了。
        通过使用 EVALSHA 命令指定的校验和,以及 lua_scripts 字典保存的脚本,服务器总可以将一个 EVALSHA 命令转换成一个等价的 EVAL 命令,因为两者的其他参数都是相同的,只需要将校验和改写成 lua_scripts 中对应的脚本即可。在传播完该 EVAL 命令后,服务器也会将这个校验和添加到 repl_scriptcache_dict 字典,这样下次遇到时就不必再进行转换了。


参考书籍:
1、《Redis设计与实现》第 20 章—— Lua 脚本。
分享到:
评论

相关推荐

    lua脚本执行行数和次数统计Dll

    "lua脚本执行行数和次数统计Dll"就是这样一个工具,它能够帮助开发者分析lua脚本的运行行为,提供关于脚本执行的详细信息。 这个Dll动态链接库专门设计用于统计lua脚本的执行行数和次数。通过加载这个Dll,你可以...

    LUA脚本|LUA脚本支持库

    LUA脚本是一种轻量级的、嵌入式的脚本语言,因其简洁的语法和高效性能,被广泛应用于游戏开发、服务器配置、自动化任务等多个领域。LUA脚本支持库则是为了扩展LUA的功能,提供更丰富的API和工具,使得开发者能够更...

    delphi调用lua脚本的一个例子

    在本文中,我们将深入探讨如何在Delphi编程环境中调用Lua脚本,以实现更灵活的逻辑处理和功能扩展。Delphi是一款强大的Object Pascal集成开发环境(IDE),而Lua则是一种轻量级、高效的脚本语言,常用于游戏开发和...

    基于串口屏LUA脚本—系统参数设置功能V1.0.pdf

    标题《基于串口屏LUA脚本—系统参数设置功能V1.0》所涉及的知识点主要集中在如何使用LUA脚本语言来设置和控制串口屏上的系统参数。串口屏一般指的是一种可以通过串行通信接口连接的显示设备,其上可以运行LUA脚本,...

    Lua 脚本

    Lua脚本可以很容易的被C/C++代码调用,也可以反过来调用C/C++的函数,这使得Lua在应用程序中可以被广泛应用。不仅仅作为扩展脚本,也可以作为普通的配置文件,代替XML,Ini等文件格式,并且更容易理解和维护。 Lua由...

    游戏引擎HGE结合Lua脚本 教程.zip

    这个“游戏引擎HGE结合Lua脚本 教程.zip”压缩包提供了关于如何利用HGE与Lua进行游戏编程的详细指导。 首先,我们来看“LUA语言学习教程+lua小脚本还是很像bash shell的.txt”。Lua是一种轻量级的脚本语言,以其...

    STM32F103 运行lua脚本例程.rar

    在本例程中,STM32F103被用于运行Lua脚本,这是一种轻量级的、可嵌入式的脚本语言,通常用于游戏开发、自动化任务和快速原型设计。 Lua的移植意味着将Lua解释器的源代码修改和编译,使其能在STM32F103的硬件平台上...

    Lua脚本语言在自动测试中的应用

    【Lua脚本语言在自动测试中的应用】 Lua是一种轻量级的脚本语言,因其高效、易用和可扩展性而在自动测试领域中得到广泛应用。它允许开发者快速编写测试脚本,实现对软件和硬件的自动化测试,从而提高测试效率,减少...

    visual c++ HGE游戏引擎+Lua脚本的结合使用.zip

    《Visual C++与HGE游戏引擎与Lua脚本的整合应用》 在游戏开发领域,高效的游戏引擎和灵活的脚本语言是不可或缺的工具。Visual C++作为一款强大的编程环境,常常被用于游戏的底层系统构建,而HGE(Happy Game Engine...

    SpringBoot+Redis执行lua脚本的方法步骤

    SpringBoot+Redis 执行 Lua 脚本的方法步骤 以下是 SpringBoot+Redis 执行 Lua 脚本的方法步骤的知识点总结: 1. 背景:在开发中,我们需要一次性操作多个 Redis 命令,但是这些操作不具备原子性,而 Redis 的事务...

    基于Lua脚本语言的嵌入式UART通信的实现

    "基于Lua脚本语言的嵌入式UART通信的实现" 本文提出了一种基于Lua脚本语言的解决方案,旨在提高IED装置对各种类型串口数据报文帧格式的适应性。该方案将具体串口报文规约的组建和解析交给Lua脚本进行处理,使设计者...

    Java调用Lua脚本(LuaJava使用、安装及Linux安装编译)

    Java调用Lua脚本是一种常见的跨语言交互技术,特别是在游戏开发和自动化脚本编写中。本文将详细介绍如何在Java环境中使用LuaJava库进行交互,并在Linux系统上进行安装和编译。 首先,让我们理解LuaJava。LuaJava是...

    excel表转Lua脚本工具

    《Excel表转Lua脚本工具详解》 在IT行业中,数据转换是一项常见的任务,尤其当涉及到配置文件或者游戏脚本时。Lua作为一种轻量级、高效且易读的脚本语言,常被用于游戏开发和其他配置文件的编写。然而,手动编写Lua...

    lua脚本源码包

    这个"lua脚本源码包"提供了lua在嵌入式开发中的应用实例,特别适合那些希望学习如何在Windows XP环境下,使用Visual Studio 2008进行lua脚本开发的初学者。 首先,我们要了解Lua的基本概念。Lua是一种解释型语言,...

    window系统 Lua脚本语言编译器

    标题提到的“window系统 Lua脚本语言编译器”实际上指的是用于在Windows平台上运行和编译Lua脚本的工具。 Lua的编译器主要是`luac.exe`,它将Lua源代码转换为字节码,这是一种中间表示,可以在Lua虚拟机上直接执行...

    android Lua脚本 文件

    本文将深入探讨如何在Android环境中运行Lua脚本,执行Lua脚本文件,以及如何调用Android API,以实现与Android系统的深度交互。 一、Android上的Lua环境 在Android上运行Lua,首先需要集成一个支持Lua的库,如...

    Lua脚本支持库

    "Lua脚本支持库"指的是为Lua提供额外功能或便利的库,这些库可以扩展Lua的基本功能,使其能够处理更复杂的任务。下面将详细探讨Lua脚本支持库及其相关知识点。 1. **元编程能力**:Lua的核心特性之一是强大的元表和...

Global site tag (gtag.js) - Google Analytics