前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >《Redis设计与实现》读书笔记(三十四) ——Redis Lua脚本环境设计与实现

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

作者头像
用户1327360
发布2018-03-07 16:19:16
1.1K0
发布2018-03-07 16:19:16
举报
文章被收录于专栏:决胜机器学习

《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

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2017-09-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 决胜机器学习 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档