《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 条评论
登录 后参与评论

相关文章

来自专栏锦小年的博客

python学习笔记9.2-文件及文件夹操作

本文主要介绍python对文件以及文件夹的操作,主要涉及到文件的创建、读取、文件内容的修改、删除,文件夹的索引、目录的判断等等。此节内容非常重要,是以后编程的基...

1906
来自专栏linux系统运维

管道符和作业控制,shell变量和环境变量配置文件

1755
来自专栏破晓之歌

Python中os与sys两模块的区别 原

os: This module provides a portable way of using operating system dependent func...

391
来自专栏PHP技术

拒绝重复造轮子,用composer搞自己的框架(2)

久负盛名的 CodeIgniter 框架是很多人的 PHP 开发入门框架,同样也是我开始学习如何从头构建一个网站的框架。在 CI中我学到了很多,其中对 MVC ...

3369
来自专栏散尽浮华

zabbix监控主机cpu达到80%后报警

在zabbix监控中,默认cpu监控模板中的触发器,当负载在一定时间内(比如最近5分钟)超过5以上为报警阀值。但是在实际场景中,由于服务器配置不一样,这个默认的...

2476
来自专栏Java3y

SpringBoot就是这么简单

s一、SpringBoot入门 今天在慕课网中看见了Spring Boot这么一个教程,这个Spring Boot作为JavaWeb的学习者肯定至少会听过,但我...

2758
来自专栏北京马哥教育

linux中的vi编辑器

vim文字处理器 linux 下的vi 是一种文字编辑器,后来的升级版本是vim。vi 分为三种模式:一般模式、编辑模式、命令命令模式。它们之间的关系如下: ?...

26211
来自专栏YG小书屋

Nginx+lua+mysql实时存日志

2756
来自专栏Python小屋

Python导入标准库和扩展库对象的几种方式

Python中的对象大概可以分为三类:内置对象、标准库对象和扩展库对象。其中内置对象是直接编译进解释器的可以直接使用,没有对应的Python源代码;标准库对象是...

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

Linux命令(35)——iconv命令

iconv命令是用来转换文件的编码方式,比如它可以将UTF8编码的转换成GB18030的编码。Linux下的iconv开发库包括iconv_open,iconv...

301

扫描关注云+社区