20 Lua脚本
Redis从2.6版本开始引入对Lua脚本的支持,通过在服务器中嵌入Lua环境,Redis客户端可以使用Lua脚本,直接在服务器原子地执行多个Redis命令。
EVAL命令可以直接对输入的脚本进行求值:
EVAL “return ‘hello world’” 0
“hello world”
EVALSHA命令可以根据脚本的SHA1校验和来对脚本进行求值,但这个命令要求校验和对应的脚本至少被EVAL命令执行过一次,或者被SCRIPT LOAD命令载入过。
20.1 创建并修改Lua环境
Redis服务器创建并修改Lua环境的整个过程有以下步骤:
- 创建一个基础的Lua环境
- 载入多个函数库到Lua环境
- 创建全局表格
redis,表格包含了对Redis进行操作的函数,如redis.call - 使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数
- 创建排序辅助函数
- 创建
redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息 - 对Lua环境中的全局变量进行保护,防止用户在执行Lua脚本时添加额外的全局变量
- 将完成修改的Lua环境保存到服务器状态的
lua属性中,等待服务器传来的Lua脚本
创建Lua环境
服务器调用Lua的C API函数lua_open,创建一个新的Lua环境。
载入函数库
- 基础库(base library):包含Lua的核心函数,如
assert、error等。为了防止用户从外部文件引入不安全的代码,loadfile函数被删除。 - 表格库(table library)
- 字符串库(string library)
- 数学库(math library)
- 调试库(debug library)
- Lua CJSON库
- Struct库:用于Lua值和C结构的转换
- Lua cmsgpack库:处理MessagePack格式的数据
创建redis全局表格
redis表格包含以下函数:
- 用于执行Redis命令的
redis.call和redis.pcall - 用于记录日志的
redis.log - 用于计算SHA1校验和的
redis.sha1hex - 用于返回错误信息的
redis.error_repyl和redis.status_reply
使用Redis自制的随机函数来替换Lua原有的随机函数
Redis服务器要求传入的Lua脚本都是纯函数(pure function)。
Redis用自制的随机函数替换了原有的math.random和math.randomseed函数,自制函数有如下特征:
- 对于相同的seed,
math.random总是相同的随机数序列。 - 对除非在脚本中使用
math.randsomseed显式修改seed,否则每次运行脚本时,Lua环境都是用固定的math.randomseed(0)来初始化seed。
创建排序辅助函数
当Lua脚本执行完一个不确定性的命令后,会使用__redis__compare_helper函数作为对比函数,自动调用table.sort函数对命令的返回值排序,以保证相同的数据集总是产生相同是输出。
创建 redis.pcall 函数的错误报告辅助函数
Redis服务器为Lua环境创建一个名为__redis__err__handler的错误处理函数。当脚本调用redis.pcall执行Redis命令,且执行的命令出现错误,__redis__err__handler函数会打印出错代码的来源和行数。
保护Lua的全局环境
确保传入服务器的脚本不会因为忘记使用local关键字而将额外的全局变量添加到Lua环境。
将Lua环境保存到服务器状态的lua属性中
这一步,服务器将Lua环境与服务器状态的lua属性关联起来。
因为Redis使用串行化的方式来执行命令,所以任意时刻,最多只会有一个脚本能够被放入Lua环境执行。因此,整个Redis服务器只需要一个Lua环境即可。
20.2 Lua环境协作组件
除了创建并修改Lua环境之外,Redis服务器还创建了两个用于与Lua环境写作的组件:
- 负责执行Lua脚本中的Redis命令的伪客户端。
- 用于保存Lua脚本的
lua_scripts字典。
伪客户端
Lua脚本使用redis.call或redis.pcall执行命令,需要以下几个步骤:
- Lua环境将
redis.call或redis.pcall函数想要执行的命令传送给伪客户端。 - 伪客户端将命令传送给命令执行器。
- 命令执行器执行命令,并将结果返回给伪客户端。
- 伪客户端接收到结果,将结果返回Lua环境。
- Lua环境接收到命令结果后,将结果返回给
redis.call或redis.pcall函数。 - 接收到结果的
redis.call或redis.pcall函数将结果作为函数返回值返回给脚本的调用者。

lua_sripts字典
lua_scripts字典的键为某个Lua脚本的SHA1校验和,值是SHA1校验和对应的Lua脚本。
1 | struct redisServer { |
Redis服务器会将所有被EVAL执行过的Lua脚本,和所有被SCRIPT LOAD再如果的Lua脚本都保存到lua_scripts字典中。
20.3 EVAL 命令的实现
EVAL命令执行过程分为三分步骤:
- 根据客户端给定的Lua脚本,在Lua环境中定义一个Lua函数。
- 将客户端给定的脚本保存到
lua_scripts字典中。 - 执行刚刚在Lua环境中定义的函数。
定义脚本函数
函数名字由f_前缀加上脚本的SHA1校验和组成,函数体则是脚本本身。使用函数来保存客户端传入的脚本有以下好处:
- 执行脚本的步骤很简单,只要调用与脚本对应的函数即可。
- 通过函数的局部性让Lua环境保持清洁,减少垃圾回收,避免使用全局变量。
- 如果某脚本使用的函数在Lua环境中被定义过一次,那么只要记得这个脚本的校验和,服务器就可以直接调用Lua函数来执行脚本。这就是
EVALSHA的实现原理。
将脚本保存到lua_scripts字典
服务器在lua_scripts字典中新添加一个键值对。
执行脚本函数
lua_scripts字典中保存脚本之后,服务器还需要一些准备工作,才能开始执行脚本:
- 将
EVAL命令传入的键名参数和脚本参数分别保存到KEYS数组和ARGV数组,然后将这两个数组作为全局变量传入Lua环境。 - 为Lua环境装载超时处理钩子(hook),在脚本出现超时后,hook可以让客户端执行
SCRIPT SKILL函数停止脚本,或SHUTDOWN命令关闭服务器。 - 执行脚本函数。
- 移除之前装载的超时钩子。
- 将执行脚本函数的结果保存到客户端状态的输入缓冲区。
- 对Lua环境执行垃圾回收操作。
20.4 EVALSHA命令的实现
1 | def EVALSHA(sha1): |
20.5 脚本管理命令的实现
SCRIPT FLUSH
SCRIPT FLUSH命令用于清除服务器中所有与Lua脚本有关的信息,它会释放并重建lua_scripts字典,关闭现有的Lua环境并重建一个新的Lua环境。
SCRIPT EXISTS
SCRIPT EXISTS命令根据输入的SHA1校验和,检查其对应的脚本是否存在于服务器中。它是通过检查lua_scripts字典实现的。
SCRIPT LOAD
SCRIPT LOAD命令所做的事情和EVAL的前两步一样:
- 在Lua环境中为脚本创建相应的函数
- 将脚本保存到
lua_scripts字典中。
SCRIPT KILL
如果服务器设置了lua-time-limit选项,那么每次执行Lua脚本前,服务器都会在Lua环境中设置一个超时钩子。
一旦钩子发现脚本的运行超时,那么将会定期在脚本执行期间的间隙,检查是否有SCRIPT KILL或SHUTDOWN命令到达服务器。
如果超时的脚本从未执行过写入操作,那么客户端可以通过SCRIPT KILL命令来停止执行脚本,并向客户端返回一个错误回复。
如果超时的脚本执行过写入操作,那么客户单只能用SHUTDOWN nosave命令来停止服务器,防止被不合法的数据写入。
20.6 脚本复制
服务器运行在复制模式下,具有写性质的脚本也会被复制到slave,如EVAL、EVALSHA、SCRIPT FLUSH、SCRIPT LOAD。
复制EVAL、SCRIPT FLUSH、SCRIPT LOAD
Redis复制EVAL、SCRIPT FLUSH、SCRIPT LOAD的方法和其他普通命令一样。master执行完上述命令后,会将其传播到所有slave。
复制EVALSHA
因为主从服务器载入Lua脚本的情况不同,EVALSHA命令不能直接传播给slave。
Redis要求master在传播EVALSHA命令的时候,必须确保EVALSHA要执行的脚本已经在slave中载入过。如果不能保证,那么master会将EVALSHA替换为等价的EVAL命令传播给slave。
1. 判断EVALSHA命令是否安全
master使用服务器状态的repl_scriptcache_dict字典记录自己已经将哪些脚本传播给了所有slave。
1 | struct redisServer { |
repl_scriptcache_dict的键是一个Lua脚本的SHA1校验和,值全部是NULL。
如果一个脚本的SHA1出现在lua_scripts字典,却没有出现在repl_scriptcache_dict字典,说明对应的的Lua脚本已被master载入,却没有传播给所有slave。
2. 清空repl_scriptcache_dict字典
每当master添加一个新的slave时,都会清空自己的repl_scriptcache_dict字典。
3. EVALSHA命令换成EVAL
通过EVALSHA指定的SHA1校验和,以及lua_scripts字典保存的Lua脚本,服务器总可以将 EVALSHA命令换成EVAL命令。
4. 传播EVALSHA命令
当master在本机执行完一个EVALSHA命令后,根据其SHA1校验和是否存在于repl_scriptcache_dict字典,决定是向所有slave传播EVALSHA还是EVAL命令。
导航
上一章:19. 事务
下一章:21. 排序










