Lua脚本

Lua脚本

Lua是redis 2.6 版本最大的亮点,通过内嵌对Lua 环境的支持,Redis 解决了长久以来不能高效地处理CAS (check-and-set)命令的缺点,并且可以通过组合使用多个命令,轻松实现以前很难实现或者不能高效实现的模式。

1、初始化Lua环境

在初始化redis服务器的时候,对Lua环境的初始化也会一并进行;  
整个初始化Lua环境的步骤如下:
1)调用lua_open函数,创建一个新的Lua环境;
2)载入指定的Lua函数库:基础库(base lib)、表格库(table lib)、字符串库(string lib)、数学库(math lib)、调试库(debug lib)、用于处理JSON对象的cjson库、在Lua值和C结构之间进行切换的struct库和处理MessagePack数据的cmsgpack库
3)屏蔽一些可能对Lua环境产生安全问题的函数,比如loadfile;
4)创建一个redis字典,保存Lua脚本,并在复制脚本是使用,字典的键为SHA1校验和,字典的值为Lua脚本;
5)创建一个redis全局表格到Lua环境,表格中包含了各种对redis进行操作的函数;
6)用redis自己定义的随机生成函数,替换math表原有的math.random函数和math.randomseed函数,新的函数具有这样的特质:每次执行Lua 脚本时,除非显式地调用math.randomseed ,否则math.random 生成的伪随机数序列总是相同的;
7)创建一个对Redis 多批量回复(multi bulk reply)进行排序的辅助函数;
8)对Lua 环境中的全局变量进行保护,以免被传入的脚本修改;
9)因为Redis 命令必须通过客户端来执行,所以需要在服务器状态中创建一个无网络连接的伪客户端(fake client),专门用于执行Lua 脚本中包含的Redis 命令:当Lua 脚本需要执行Redis 命令时,它通过伪客户端来向服务器发送命令请求,服务器在执行完命令之后,将结果返回给伪客户端,而伪客户端又转而将命令结果返回给Lua 脚本;
10)将Lua 环境的指针记录到Redis 服务器的全局状态中,等候Redis 的调用;

以上就是Redis 初始化Lua 环境的整个过程,当这些步骤都执行完之后,Redis 就可以使用Lua 环境来处理脚本了。严格来说,步骤1 至8 才是初始化Lua 环境的操作,而步骤9 和10 则是将Lua 环境关联到服务器的操作,为了按顺序观察整个初始化过程,我们将两种操作放在了一起。另外,步骤6 用于创建无副作用的脚本,而步骤7 则用于去除部分Redis 命令中的不确定性(non deterministic),关于这两点,请看下面一节关于脚本安全性的讨论。

2、脚本的安全性

当将Lua 脚本复制到附属节点,或者将Lua 脚本写入AOF 文件时,Redis 需要解决这样一个问题:如果一段Lua 脚本带有随机性质或副作用,当这段脚本在附属节点运行时,或从AOF 文件载入重新运行时,它得到的结果可能和之前运行的结果完全不同。

注意:只有在带有随机性的脚本进行写入时,随机性才是有害的,如果一个脚本只是执行只读操作,那么随机性是无害的。和随机性质类似,如果一个脚本的执行对任何副作用产生了依赖,那么这个脚本每次执行的结果都可能会不一样。为了解决这个问题,Redis 对Lua 环境所能执行的脚本做了一个严格的限制——所有脚本都必须是无副作用的纯函数(pure function)。
为此,Redis 对Lua 环境做了一些列相应的措施:
• 不提供访问系统状态状态的库(比如系统时间库)。
• 禁止使用loadfile 函数。
• 如果脚本在执行带有随机性质的命令(比如RANDOMKEY ),或者带有副作用的命令(比如TIME )之后,试图执行一个写入命令(比如SET ),那么Redis 将阻止这个脚本继续运行,并返回一个错误。
• 如果脚本执行了带有随机性质的读命令(比如SMEMBERS ),那么在脚本的输出返回给Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。
• 用Redis 自己定义的随机生成函数,替换Lua 环境中math 表原有的math.random 函数地调用math.randomseed ,否则math.random 生成的伪随机数序列总是相同的。
经过这一系列的调整之后,Redis 可以保证被执行的脚本:

  • 无副作用。
  • 没有有害的随机性。
  • 对于同样的输入参数和数据集,总是产生相同的写入命令。

3、脚本的执行

在脚本环境的初始化工作完成以后,Redis 就可以通过EVAL 命令或EVALSHA 命令执行Lua脚本了。

redis> EVAL "return 'hello world'" 0
"hello world"
redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de910 // 上一个脚本的校验和
"hello world"

4、EVAL命令的实现

EVAL 命令的执行可以分为以下步骤:
 1) 为输入脚本定义一个Lua 函数。
 2) 执行这个Lua 函数。

定义Lua函数:
所有被Redis 执行的Lua 脚本,在Lua 环境中都会有一个和该脚本相对应的无参数函数:当调用EVAL 命令执行脚本时,程序第一步要完成的工作就是为传入的脚本创建一个相应的Lua函数。
举个例子,当执行命令EVAL “return ‘hello world’” 0 时,Lua 会为脚本"return ‘hello world’" 创建以下函数:

function f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91()
return 'hello world'
end

其中,函数名以f_ 为前缀,后跟脚本的SHA1 校验和(一个40 个字符长的字符串)拼接而成。而函数体(body)则是用户输入的脚本。以函数为单位保存Lua 脚本有以下好处:
 • 执行脚本的步骤非常简单,只要调用和脚本相对应的函数即可。
 • Lua 环境可以保持清洁,已有的脚本和新加入的脚本不会互相干扰,也可以将重置Lua环境和调用Lua GC 的次数降到最低。
 • 如果某个脚本所对应的函数在Lua 环境中被定义过至少一次,那么只要记得这个脚本的SHA1 校验和,就可以直接执行该脚本——这是实现EVALSHA 命令的基础,稍后在介绍EVALSHA 的时候就会说到这一点。
在为脚本创建函数前,程序会先用函数名检查Lua 环境,只有在函数定义未存在时,程序才创建函数。重复定义函数一般并没有什么副作用,这算是一个小优化。

另外,如果定义的函数在编译过程中出错(比如,脚本的代码语法有错),那么程序向用户返回一个脚本错误,不再执行后面的步骤。

执行Lua函数:
在定义好Lua 函数之后,程序就可以通过运行这个函数来达到运行输入脚本的目的了。不过,在此之前,为了确保脚本的正确和安全执行,还需要执行一些设置钩子、传入参数之类的操作,整个执行函数的过程如下:

  1. 将EVAL 命令中输入的KEYS 参数和ARGV 参数以全局数组的方式传入到Lua 环境中。
  2. 设置伪客户端的目标数据库为调用者客户端的目标数据库: fake_client->db =caller_client->db ,确保脚本中执行的Redis 命令访问的是正确的数据库。
  3. 为Lua 环境装载超时钩子,保证在脚本执行出现超时时可以杀死脚本,或者停止Redis服务器。
  4. 执行脚本对应的Lua 函数。
  5. 如果被执行的Lua 脚本中带有SELECT 命令,那么在脚本执行完毕之后,伪客户端中的数据库可能已经有所改变,所以需要对调用者客户端的目标数据库进行更新:caller_client->db = fake_client->db 。
  6. 执行清理操作:清除钩子;清除指向调用者客户端的指针;等等。
  7. 将Lua 函数执行所得的结果转换成Redis 回复,然后传给调用者客户端。
  8. 对Lua 环境进行一次单步的渐进式GC 。

以下是执行EVAL “return ‘hello world’” 0 的过程中,调用者客户端(caller)、Redis 服务器和Lua 环境之间的数据流表示图:
在这里插入图片描述

5、小结

初始化Lua脚本环境需要一系列步骤,其中包括:

  • 创建Lua环境;
  • 载入Lua库,比如字符串库、数学库、表格库、调试库等;
  • 创建redis全局表格,包含各种对redis进行操作的函数,比如redis.call 和 redis.log等;
  • 创建一个无网络的伪客户端,专门用于执行Lua脚本中的redis命令;

redis通过一系列的措施保证被执行的Lua脚本无副作用,也没有有害的写随机性:对于同样的输入参数和数据集,总是产生相同的写入命令。

EVAL命令为输入脚本定义一个Lua函数,然后通过执行这个函数来执行脚本。

EVALSHA通过构建函数名,直接调用Lua中已定义的函数,从而执行相应的脚本。

  • 2
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Lua 是一种轻量级的脚本语言,它具有简洁、高效、易于扩展等特点。Lua 可以被嵌入到 C/C++ 应用程序中,也可以作为独立的解释器运行。下面简单介绍一下 Lua 脚本的基本语法和用法。 ## 1. 基本语法 Lua 语言的基本语法与众多编程语言类似。下面是一个简单的 Lua 脚本示例: ```lua -- 定义变量 a = 10 b = 20 -- 条件语句 if a < b then print("a is less than b") else print("a is greater than or equal to b") end -- 循环语句 for i = 1, 10 do print(i) end -- 函数定义 function add(a, b) return a + b end -- 函数调用 sum = add(a, b) print("sum =", sum) ``` ## 2. 常用函数 Lua 中内置了一些常用的函数,比如字符串处理函数、数学函数、文件操作函数等。下面是一些常用函数的示例: ```lua -- 字符串处理函数 str = "hello world" print(string.upper(str)) -- 将字符串转换为大写 print(string.sub(str, 1, 5)) -- 截取字符串 -- 数学函数 print(math.pow(2, 3)) -- 求幂 print(math.sqrt(16)) -- 求平方根 -- 文件操作函数 file = io.open("test.txt", "w") file:write("Hello World") file:close() file = io.open("test.txt", "r") print(file:read("*all")) file:close() ``` ## 3. Lua 与 C/C++ 的交互 Lua 可以被嵌入到 C/C++ 应用程序中,实现 Lua 脚本与 C/C++ 代码的交互。在 C/C++ 中,可以使用 Lua API 调用 Lua 脚本中的函数、读写变量等。下面是一个简单的示例: ```c #include "lua.hpp" int main() { lua_State* L = luaL_newstate(); luaL_openlibs(L); luaL_dofile(L, "test.lua"); // 执行 Lua 脚本 lua_getglobal(L, "add"); // 获取 Lua 函数 lua_pushnumber(L, 10); // 压入参数 lua_pushnumber(L, 20); lua_call(L, 2, 1); // 调用函数,2 表示有两个参数,1 表示有一个返回值 double result = lua_tonumber(L, -1); // 获取返回值 printf("result = %lf\n", result); lua_close(L); return 0; } ``` 上面的示例中,C/C++ 代码使用 Lua API 调用了 Lua 脚本中的 add 函数,并获取了该函数的返回值。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只IT攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值