《Redis设计与实现》读书笔记(三十四) ——Redis Lua脚本环境设计与实现

《Redis设计与实现》读书笔记(三十四) ——Redis Lua脚本环境设计与实现

(原创内容,转载请注明来源,谢谢)

一、创建lua环境

为了在redis服务器执行lua脚本,redis服务器内嵌了一个lua环境,redis服务器启动的时候,会自动创建lua环境,步骤如下:

1)创建一个基础lua环境。

调用lua的C API函数lua_open,创建新的lua环境。但是这个是原生的环境,redis会对其进行定制。

2)载入多个lua函数库,以便lua脚本的执行。

包括基础库、表格库、字符串库、数学库、调试库、CJSON库、Struct库、cmsgpack库等。

3)创建全局表格redis,包含lua脚本可以多redis进行的操作。

包括redis.call、redis.pcall、redis.log函数等,以便在lua脚本中执行redis命令。

4)使用redis自制的随机函数替换lua脚本原生的随机函数,避免随机机制不统一导致的错误。

lua的随机函数具有副作用,不符合redis的要求。因此,redis用自制的函数,替换了lua脚本的math库中,所有math.random、math.randomseed函数。相同的seed总是会保证生成相同的随机序列。

5)创建排序辅助函数,供lua调用,避免排序结果的不一致。

除了随机函数,另一个不确定的是排序辅助函数。对于集合、hash等操作,输出的结果可能是无序的,同样的内容输出的有可能会不同,为了消除这种不确定性,lua执行一次不确定性的redis命令后,redis会自动调用redis.sort函数进行一次排序,保证相同数据集有相同的输出。

6)创建错误报告函数redis.pcall,包含更详细的报错信息。

该函数输出更详细的错误信息,以便于开发者进行调试。

7)对lua环境的全局环境进行保护,防止全局变量被修改。

主要是保证避免忘了添加local关键字,导致额外的全局变量在脚本中被增加到lua中。但是redis没有保护已经存在的全局变量,即可用修改现有全局变量,这个要注意。

8)将上述操作后的lua环境,保存到服务器的lua属性中。

redis将lua环境保存在redisServer结构体的lua属性中。

二、lua环境协作组件

除了创建lua环境,redis还创建了两个环境协作组件,分别是负责执行lua脚本中的redis命令的伪客户端、负责保存lua脚本的lua_scripts字典。

1、伪客户端

执行redis命令都必须要有redis的客户端状态,因此redis服务器为执行lua,设定了一个伪客户端,专门用于执行lua命令。

lua用redis.call或redis.pcall执行redis命令,步骤如下:

1)lua环境将想要执行的命令传给伪客户端,伪客户端将命令传给命令执行器。

2)命令执行器执行命令,并将结果返回给伪客户端。

3)伪客户端返回给lua脚本,lua脚本再返回给脚本调用者。

2、lua_scripts字典

这个字典的键是某个lua脚本sha1校验和,值是该校验和对应的脚本。该字段也保存在redisServer结构体中。

redis服务器会保存所有eval命令执行过的脚本,以及所有script load命令加载过的lua脚本。

该字典有两个作用,一个是实现scripts exists命令,一个是实现脚本复制功能。

三、eval命令的实现

eval执行过程分为3个步骤:

1)根据客户端给定的lua脚本,在lua环境中定义一个lua函数。

2)将客户端给定的脚本保存到lua_scripts字典,等待将来进一步使用。

3)执行lua环境中给定的函数,来执行lua脚本。

1、定义脚本函数

服务器会为传入的脚本,定义一个函数,函数的名字以f_开头,后面是脚本的sha1校验和(40个字符长度),整个函数名长度共42个字符,函数体是脚本本身。

这样做的好处在于,执行脚本步骤非常简单,只要调用与脚本相对应的函数,每个脚本有一个唯一的函数;另外,函数的局部性让环境保持清洁,避免全局变量;还有,脚本本定义过一次后,服务器后续再调用脚本,不需要知道脚本本身,只要知道脚本sha1校验和即可。

2、执行lua脚本函数

执行步骤如下:

1)将eval传入的键名参数和脚本参数分别保存到keys和argv数组,将这两个数组作为全局变量传入到lua环境。

2)为lua环境装载超时处理钩子,这个钩子可以在脚本出现超时运行时,让客户端执行script kill命令,停止脚本,或者通过shutdown命令直接关闭服务器。

3)执行脚本函数。

4)移除之前装载的超时处理钩子。

5)将指向脚本函数得到的结果保存到客户端状态的输出缓冲区中,等待服务器将结果返回给客户端。

6)对lua环境进行垃圾回收。

四、evalsha命令

evalsha命令,是直接执行脚本的sha1函数。

这个函数必须之前已经成功执行过,则此次只需要直接传入sha1的结果,服务器会从lua_scripts字典中,查找是否存在该sha1结果的键,如果存在,则会自动拼接出函数的名字,并且去执行。

具体执行过程同eval命令的第三步。

五、脚本管理命令的实现

redis关于lua脚本管理的命令有四个:script flush、script exists、script load、script kill。

1、script flush

该命令会清除服务器所有和lua有关的信息,会清空lua_scripts字典,并且关闭现有lua环境,重新初始化一个lua环境。

2、scriptexists

该命令传入sha1校验和,判断在lua_scripts字典中,是否存在该校验和。该命令允许一次传入多个校验和。存在返回1,不存在返回2。

3、script load

该命令等同于eval命令的前两步,即没有执行脚本,但是创建了脚本的函数,并且将校验和存入到lua_scripts字典。因此,eval命令,等同于script load命令加上evalsha命令。

4、script kill

脚本运行前,会创建钩子,防止执行时间超过redis设置选项中的lua-time-limit。在执行期间,会定期检查脚本运行时间,如果超时,则会停止脚本。

停止脚本有两种方式,redis服务器会区分脚本是否执行过写命令:

如果已经执行过写命令,并且lua脚本超时,redis会执行shutdownnosave命令,停止服务器,防止脏数据写入;如果没执行过写命令,则redis服务器会执行script kill。

六、脚本复制

当服务器运行在复制模式,具有写性质的脚本命令,会被复制到从服务器。包括eval、evalsha、script flush、script load。

1、eval、script flush、script load命令复制

eval、script flush、script load三个命令的复制方式和普通的redis写命令一样,主服务器会直接将命令传播给从服务器。

2、evalsha命令复制

evalsha命令复制较为复制,由于这个命令是直接执行sha1编码的函数名,因此要首先保证每个从服务器都存在该名字,并且从服务器要能找到该名字对应的函数。

为了防止这个情况,redis会要求主服务器传播evalsha的时候,要保证从服务器已经存在该函数,否则主服务器会将evalsha命令转换成等价的eval命令发送给从服务器。

1)repl_scriptcache_dict

主服务器在redisServer结构体中,另外保存了一个字典,repl_scriptcache_dict,用于记录哪些脚本已经传播给从服务器。该字典键是lua脚本sha1校验和,值是null。记录在这个字典中的键,都是已经传播给从服务器的。

2)清空repl_scriptcache_dict

当添加一个从服务器的时候,redis主服务器会清空repl_scriptcache_dict字典,确保新服务器不会发生错误。

3)evalsha转换成eval

从lua_scripts字典中,找到对应的lua脚本函数的内容。这样传播后,由于从服务器可以记录其校验和并存在自身的lua_scripts字典,因此每次这样传播后,redis服务器都会将脚本的校验和存入repl_scriptcache_dict字典,下次就可以直接发生evalsha。

因此,evalsha传播的过程,是先判断主服务器自身的repl_scriptcache_dict字典是否存在该校验和,如果有则直接传播;如果没有,则从lua_scripts字典找到对应的lua脚本,传给从服务器,并且将其校验和存入repl_scriptcache_dict字典。

七、总结

1、redis服务器启动的时候,会执行一系列的lua环境初始化操作,保证lua脚本正常进行。其专门创建一个伪客户端,并且为lua脚本定制随机函数、排序函数等,保证脚本的执行结果在redis服务器可预测的范围内。

2、redis所有用eval执行过的lua脚本,或被script load载入过的脚本,都会通过校验和—脚本函数的键值对的方式,保存到服务器中,用于后续evalsha、script exists、脚本复制等功能。

3、redis为每个lua脚本定义一个函数,函数的名称是f_开头,以脚本sha1的40位字符串连接到其后。函数的内容是脚本本身。

4、eval直接执行脚本(前置是先有一系列保存操作),evalsha通过执行脚本函数名称来执行脚本,script load载入脚本的校验和但不执行脚本,script flush清空所有保存的脚本字典并且重置lua环境,script exists接收一个或多个sha1校验和以判断脚本是否已经存在,script kill是在lua脚本超时的情况下未执行过写命令情况下强制停止脚本,shutdown nosave是lua脚本超时并且执行过写命令的情况下关闭服务器防止脏数据写入。

5、主服务器在复制eval、script flush、script load命令同其他redis写命令。

6、主服务器在复制evalsha命令时,会先判断主服务器自身的repl_scriptcache_dict字典是否存在该校验和,如果有则直接传播;如果没有,则从lua_scripts字典找到对应的lua脚本,传给从服务器,并且将其校验和存入repl_scriptcache_dict字典。

——written by linhxx 2017.09.30

原文发布于微信公众号 - 决胜机器学习(phpthinker)

原文发表时间:2017-09-30

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏企鹅号快讯

UNIX 高手的 10 个习惯

Unix运维工程师看过来:10个能够提高您的 UNIX 命令行效率的好习惯——并在此过程中摆脱不良的使用模式。本文循序渐进地指导您学习几项用于命令行操作的技术,...

1709
来自专栏陈满iOS

iOS·MCDownloader学习笔记

762
来自专栏崔庆才的专栏

是时候抛弃print了,开始体验下logging的强大吧!

1292
来自专栏cmazxiaoma的架构师之路

电商毕业设计小节2

1556
来自专栏青枫的专栏

Linux下的man命令

man命令是Linux下的帮助指令,通过man指令可以查看Linux中的指令帮助、配置文件帮助和编程帮助等信息。

773
来自专栏Java技术

为什么你创建的数据库索引没有生效?

几乎所有的小伙伴都可以随口说几句关于创建索引的优缺点,也知道什么时候创建索引能够提高我们的查询性能,什么时候索引会更新,但是你有没有注意到,即使你设置了索引,有...

421
来自专栏C/C++基础

g++入门教程

g++是GNU开发的C++编译器,是GCC(GNU Compiler Collection)GNU编译器套件的组成部分。另外,gcc是GNU的C编译器。

631
来自专栏有趣的Python

1-浙大攻略计划-专业课-Linux C语言编程基本原理与实践(慕课网)

将max.c变成max.o之后,我们需要把hello.c中的include注释掉并添上方法声明

1012
来自专栏数据库

R包系列——RODBC包教程

在R基础——数据的导入与导出(下)中,介绍了使用RODBC包连接SQL server数据库,在这篇文章中,根据我工作内容,介绍该包的基本操作,同时,根据我使用该...

2098
来自专栏禁心尽力

mybatis_开发篇

一、使用mybatis的动态代理方式开发 需求:这里以crm系统中分页条件查询所有的客户信息的功能为例? 1、创建工程 2、引入所需的jar包 3、引入日志文件...

1745

扫码关注云+社区