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

相关文章

来自专栏一个会写诗的程序员的博客

Spring Reactor 项目核心库Reactor Core

Non-Blocking Reactive Streams Foundation for the JVM both implementing a Reactiv...

2152
来自专栏落花落雨不落叶

canvas画简单电路图

61011
来自专栏pangguoming

Spring Boot集成JasperReports生成PDF文档

由于工作需要,要实现后端根据模板动态填充数据生成PDF文档,通过技术选型,使用Ireport5.6来设计模板,结合JasperReports5.6工具库来调用渲...

1.2K7
来自专栏Golang语言社区

【Golang语言社区】GO1.9 map并发安全测试

var m sync.Map //全局 func maintest() { // 第一个 YongHuomap := make(map[st...

4708
来自专栏java 成神之路

使用 NIO 实现 echo 服务器

4607
来自专栏陈仁松博客

ASP.NET Core 'Microsoft.Win32.Registry' 错误修复

今天在发布Asp.net Core应用到Azure的时候出现错误InvalidOperationException: Cannot find compilati...

4848
来自专栏C#

DotNet加密方式解析--非对称加密

    新年新气象,也希望新年可以挣大钱。不管今年年底会不会跟去年一样,满怀抱负却又壮志未酬。(不过没事,我已为各位卜上一卦,卦象显示各位都能挣钱...)...

4858
来自专栏转载gongluck的CSDN博客

cocos2dx 打灰机

#include "GamePlane.h" #include "PlaneSprite.h" #include "BulletNode.h" #include...

5426
来自专栏Ceph对象存储方案

Luminous版本PG 分布调优

Luminous版本开始新增的balancer模块在PG分布优化方面效果非常明显,操作也非常简便,强烈推荐各位在集群上线之前进行这一操作,能够极大的提升整个集群...

3105
来自专栏张善友的专栏

Mix 10 上的asp.net mvc 2的相关Session

Beyond File | New Company: From Cheesy Sample to Social Platform Scott Hansel...

2567

扫码关注云+社区