Redis 实战之创建并修改 Lua 环境

创建并修改 Lua 环境

  • 创建 Lua 环境
  • 载入函数库
  • 创建 redis 全局表格
  • 使用 Redis 自制的随机函数来替换 Lua 原有的随机函数
  • 创建排序辅助函数
  • 创建 redis.pcall 函数的错误报告辅助函数
  • 保护 Lua 的全局环境
  • 将 Lua 环境保存到服务器状态的 lua 属性里面
  • 总结

为了在Redis 服务器中执行 Lua 脚本, Redis 在服务器内嵌了一个 Lua 环境(environment), 并对这个 Lua 环境进行了一系列修改, 从而确保这个 Lua 环境可以满足 Redis 服务器的需要。

Redis 服务器创建并修改 Lua 环境的整个过程由以下步骤组成:

1、 创建一个基础的Lua环境,之后的所有修改都是针对这个环境进行的;
2、 载入多个函数库到Lua环境里面,让Lua脚本可以使用这些函数库来进行数据操作;
3、 创建全局表格redis,这个表格包含了对Redis进行操作的函数,比如用于在Lua脚本中执行Redis命令的redis.call函数;
4、 使用Redis自制的随机函数来替换Lua原有的带有副作用的随机函数,从而避免在脚本中引入副作用;
5、 创建排序辅助函数,Lua环境使用这个辅佐函数来对一部分Redis命令的结果进行排序,从而消除这些命令的不确定性;
6、 创建redis.pcall函数的错误报告辅助函数,这个函数可以提供更详细的出错信息;
7、 对Lua环境里面的全局环境进行保护,防止用户在执行Lua脚本的过程中,将额外的全局变量添加到了Lua环境里面;
8、 将完成修改的Lua环境保存到服务器状态的lua属性里面,等待执行服务器传来的Lua脚本;

接下来的各个小节将分别介绍这些步骤。

创建 Lua 环境

在最开始的这一步, 服务器首先调用 Lua 的 C API 函数 lua_open , 创建一个新的 Lua 环境。

因为lua_open 函数创建的只是一个基本的 Lua 环境, 为了让这个 Lua 环境可以满足 Redis 的操作要求, 接下来服务器将对这个 Lua 环境进行一系列修改。

载入函数库

Redis 修改 Lua 环境的第一步, 就是将以下函数库载入到 Lua 环境里面:

基础库(base library): 这个库包含 Lua 的核心(core)函数, 比如 assert 、 error 、 pairs 、 tostring 、 pcall , 等等。 另外, 为了防止用户从外部文件中引入不安全的代码, 库中的 loadfile 函数会被删除。
表格库(table library): 这个库包含用于处理表格的通用函数, 比如 table.concat 、 table.insert 、 table.remove 、 table.sort, 等等。
字符串库(string library): 这个库包含用于处理字符串的通用函数, 比如用于对字符串进行查找的 string.find 函数, 对字符串进行格式化的 string.format 函数, 查看字符串长度的 string.len 函数, 对字符串进行翻转的 string.reverse 函数, 等等。
数学库(math library): 这个库是标准 C 语言数学库的接口, 它包括计算绝对值的 math.abs 函数, 返回多个数中的最大值和最小值的 math.max 函数和 math.min 函数, 计算二次方根的 math.sqrt 函数, 计算对数的 math.log 函数, 等等。
调试库(debug library): 这个库提供了对程序进行调试所需的函数, 比如对程序设置钩子和取得钩子的 debug.sethook 函数和debug.gethook 函数, 返回给定函数相关信息的 debug.getinfo 函数, 为对象设置元数据的 debug.setmetatable 函数, 获取对象元数据的debug.getmetatable 函数, 等等。
Lua CJSON 库(http://www.kyne.com.au/~mark/software/lua-cjson.php): 这个库用于处理 UTF-8 编码的 JSON 格式, 其中cjson.decode 函数将一个 JSON 格式的字符串转换为一个 Lua 值, 而 cjson.encode 函数将一个 Lua 值序列化为 JSON 格式的字符串。
Struct 库(http://www.inf.puc-rio.br/~roberto/struct/): 这个库用于在 Lua 值和 C 结构(struct)之间进行转换, 函数struct.pack 将多个 Lua 值打包成一个类结构(struct-like)字符串, 而函数 struct.unpack 则从一个类结构字符串中解包出多个 Lua 值。
Lua cmsgpack 库(https://github.com/antirez/lua-cmsgpack): 这个库用于处理 MessagePack 格式的数据, 其中 cmsgpack.pack 函数将 Lua 值转换为 MessagePack 数据, 而 cmsgpack.unpack 函数则将 MessagePack 数据转换为 Lua 值。

通过使用这些功能强大的函数库, Lua 脚本可以直接对执行 Redis 命令获得的数据进行复杂的操作。

创建 redis 全局表格

在这一步, 服务器将在 Lua 环境中创建一个 redis 表格(table), 并将它设为全局变量。

这个redis 表格包含以下函数:

用于执行 Redis 命令的 redis.call 和 redis.pcall 函数。
用于记录 Redis 日志(log)的 redis.log 函数, 以及相应的日志级别(level)常量: redis.LOG_DEBUG , redis.LOG_VERBOSE ,redis.LOG_NOTICE , 以及 redis.LOG_WARNING 。
用于计算 SHA1 校验和的 redis.sha1hex 函数。
用于返回错误信息的 redis.error_reply 函数和 redis.status_reply 函数。

在这些函数里面, 最常用也最重要的要数 redis.call 函数和 redis.pcall 函数 —— 通过这两个函数, 用户可以直接在 Lua 脚本中执行 Redis 命令:

redis> EVAL "return redis.call('PING')" 0
PONG

使用 Redis 自制的随机函数来替换 Lua 原有的随机函数

为了保证相同的脚本可以在不同的机器上产生相同的结果, Redis 要求所有传入服务器的 Lua 脚本, 以及 Lua 环境中的所有函数, 都必须是无副作用(side effect)的纯函数(pure function)。

但是,在之前载入到 Lua 环境的 math 函数库中, 用于生成随机数的 math.random 函数和 math.randomseed 函数都是带有副作用的, 它们不符合 Redis 对 Lua 环境的无副作用要求。

因为这个原因, Redis 使用自制的函数替换了 math 库中原有的 math.random 函数和 math.randomseed 函数, 替换之后的两个函数有以下特征:

对于相同的 seed 来说, math.random 总产生相同的随机数序列, 这个函数是一个纯函数。
除非在脚本中使用 math.randomseed 显式地修改 seed , 否则每次运行脚本时, Lua 环境都使用固定的 math.randomseed(0) 语句来初始化 seed 。

比如说, 使用以下脚本, 我们可以打印 seed 值为 0 时, math.random 对于输入 10 至 1 所产生的随机序列:

无论执行这个脚本多少次, 产生的值都是相同的:

$ redis-cli --eval random-with-default-seed.lua
1) (integer) 1
2) (integer) 2
3) (integer) 2
4) (integer) 3
5) (integer) 4
6) (integer) 4
7) (integer) 7
8) (integer) 1
9) (integer) 7
10) (integer) 2

但是,如果我们在另一个脚本里面, 调用 math.randomseed 将 seed 修改为 10086 :

那么这个脚本生成的随机数序列将和使用默认 seed 值 0 时生成的随机序列不同:

$ redis-cli --eval random-with-new-seed.lua
1) (integer) 1
2) (integer) 1
3) (integer) 2
4) (integer) 1
5) (integer) 1
6) (integer) 3
7) (integer) 1
8) (integer) 1
9) (integer) 3
10) (integer) 1

创建排序辅助函数

上一个小节说到, 为了防止带有副作用的函数令脚本产生不一致的数据, Redis 对 math 库的 math.random 函数和 math.randomseed 函数进行了替换。

对于Lua 脚本来说, 另一个可能产生不一致数据的地方是那些带有不确定性质的命令。

比如对于一个集合键来说, 因为集合元素的排列是无序的, 所以即使两个集合的元素完全相同, 它们的输出结果也可能并不相同。

考虑下面这个集合例子:

redis> SADD fruit apple banana cherry
(integer) 3redis> SMEMBERS fruit
1) "cherry"
2) "banana"
3) "apple"redis> SADD another-fruit cherry banana apple
(integer) 3redis> SMEMBERS another-fruit
1) "apple"
2) "banana"
3) "cherry"

这个例子中的 fruit 集合和 another-fruit 集合包含的元素是完全相同的, 只是因为集合添加元素的顺序不同, SMEMBERS 命令的输出就产生了不同的结果。

Redis 将 SMEMBERS 这种在相同数据集上可能会产生不同输出的命令称为“带有不确定性的命令”, 这些命令包括:

SINTER
SUNION
SDIFF
SMEMBERS
HKEYS
HVALS
KEYS

为了消除这些命令带来的不确定性, 服务器会为 Lua 环境创建一个排序辅助函数 __redis__compare_helper , 当 Lua 脚本执行完一个带有不确定性的命令之后, 程序会使用 __redis__compare_helper 作为对比函数, 自动调用 table.sort 函数对命令的返回值做一次排序, 以此来保证相同的数据集总是产生相同的输出。

举个例子, 如果我们在 Lua 脚本中对 fruit 集合和 another-fruit 集合执行 SMEMBERS 命令, 那么两个脚本将得出相同的结果 —— 因为脚本已经对 SMEMBERS 命令的输出进行过排序了:

redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 fruit
1) "apple"
2) "banana"
3) "cherry"redis> EVAL "return redis.call('SMEMBERS', KEYS[1])" 1 another-fruit
1) "apple"
2) "banana"
3) "cherry

创建 redis.pcall 函数的错误报告辅助函数

在这一步, 服务器将为 Lua 环境创建一个名为 __redis__err__handler 的错误处理函数, 当脚本调用 redis.pcall 函数执行 Redis 命令, 并且被执行的命令出现错误时, __redis__err__handler 就会打印出错代码的来源和发生错误的行数, 为程序的调试提供方便。

举个例子, 如果客户端要求服务器执行以下 Lua 脚本:

那么服务器将向客户端返回一个错误:

$ redis-cli --eval wrong-command.lua
(error) @user_script: 4: Unknown Redis command called from Lua script

其中@user_script 说明这是一个用户定义的函数, 而之后的 4 则说明出错的代码位于 Lua 脚本的第四行。

保护 Lua 的全局环境

在这一步, 服务器将对 Lua 环境中的全局环境进行保护, 确保传入服务器的脚本不会因为忘记使用 local 关键字而将额外的全局变量添加到了 Lua 环境里面。

因为全局变量保护的原因, 当一个脚本试图创建一个全局变量时, 服务器将报告一个错误:

redis> EVAL "x = 10" 0
(error) ERR Error running script
(call to f_df1ad3745c2d2f078f0f41377a92bb6f8ac79af0):
@enable_strict_lua:7: user_script:1:
Script attempted to create global variable 'x'

除此之外, 试图获取一个不存在的全局变量也会引发一个错误:

redis> EVAL "return x" 0
(error) ERR Error running script
(call to f_03c387736bb5cc009ff35151572cee04677aa374):
@enable_strict_lua:14: user_script:1:
Script attempted to access unexisting global variable 'x'

不过Redis 并未禁止用户修改已存在的全局变量, 所以在执行 Lua 脚本的时候, 必须非常小心, 以免错误地修改了已存在的全局变量:

redis> EVAL "redis = 10086; return redis" 0
(integer) 10086

将 Lua 环境保存到服务器状态的 lua 属性里面

经过以上的一系列修改, Redis 服务器对 Lua 环境的修改工作到此就结束了, 在最后的这一步, 服务器会将 Lua 环境和服务器状态的 lua属性关联起来,因为Redis 使用串行化的方式来执行 Redis 命令, 所以在任何特定时间里, 最多都只会有一个脚本能够被放进 Lua 环境里面运行, 因此, 整个 Redis 服务器只需要创建一个 Lua 环境即可。

总结

Redis 服务器在启动时, 会对内嵌的 Lua 环境执行一系列修改操作, 从而确保内嵌的 Lua 环境可以满足 Redis 在功能性、安全性等方面的需要。
Redis 服务器专门使用一个伪客户端来执行 Lua 脚本中包含的 Redis 命令。
Redis 使用脚本字典来保存所有被 EVAL 命令执行过, 或者被 SCRIPT_LOAD 命令载入过的 Lua 脚本, 这些脚本可以用于实现SCRIPT_EXISTS 命令, 以及实现脚本复制功能。
EVAL 命令为客户端输入的脚本在 Lua 环境中定义一个函数, 并通过调用这个函数来执行脚本。
EVALSHA 命令通过直接调用 Lua 环境中已定义的函数来执行脚本。
SCRIPT_FLUSH 命令会清空服务器 lua_scripts 字典中保存的脚本, 并重置 Lua 环境。
SCRIPT_EXISTS 命令接受一个或多个 SHA1 校验和为参数, 并通过检查 lua_scripts 字典来确认校验和对应的脚本是否存在。
SCRIPT_LOAD 命令接受一个 Lua 脚本为参数, 为该脚本在 Lua 环境中创建函数, 并将脚本保存到 lua_scripts 字典中。
服务器在执行脚本之前, 会为 Lua 环境设置一个超时处理钩子, 当脚本出现超时运行情况时, 客户端可以通过向服务器发送SCRIPT_KILL 命令来让钩子停止正在执行的脚本, 或者发送 SHUTDOWN nosave 命令来让钩子关闭整个服务器。
主服务器复制 EVALSCRIPT_FLUSHSCRIPT_LOAD 三个命令的方法和复制普通 Redis 命令一样 —— 只要将相同的命令传播给从服务器就可以了。
主服务器在复制 EVALSHA 命令时, 必须确保所有从服务器都已经载入了 EVALSHA 命令指定的 SHA1 校验和所对应的 Lua 脚本, 如果不能确保这一点的话, 主服务器会将 EVALSHA 命令转换成等效的 EVAL 命令, 并通过传播 EVAL 命令来获得相同的脚本执行效果

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/833500.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Git系列:git merge 使用技巧

💝💝💝欢迎莅临我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:「stormsha的主页」…

产品评测:SmartX 与 Nutanix 超融合在数据库场景下的性能表现

重点内容 SmartX 与 Nutanix 超融合分布式存储设计差异如何影响数据库性能表现。重点测试结论:数据库场景下,SmartX 超融合基于单卷部署的性能,依旧优于 Nutanix 超融合基于多卷部署最佳配置的性能。更多 SmartX、VMware、Nutanix 超融合技术…

50kw 直流充电桩测试仪的基础知识

直流充电桩测试仪是专门用于检测和测试直流充电桩性能的设备。它能够对充电桩的输出电压、电流、功率、效率等关键参数进行精确测量,以确保充电桩的正常运行和充电安全。 一、工作原理 直流充电桩测试仪主要通过模拟实际充电过程,对充电桩的各项性能进行…

关于路由懒加载的实现

在Vue2中,实现路由懒加载可以使用import的动态引入方式。通常,我们可以将组件作为被引入的模块,并在routes配置中使用component: () > import(/components/Example.vue)来实现懒加载。 在Vue3中,懒加载的实现方式稍有不同。Vu…

测试台架设计与制作

技术改变生活,懒人推动科技。人们在执行整车测试时,诸多不便,那如何提高测试效率、改善人员测试环境,各个汽车生态的设计者就为之费神。以CarPlay为例,从2013年的送整车去美国测试,发展到如今所有测试均可在…

2024年,抖音小店开通需要多少钱?一篇详解!

大家好,我是电商糖果 2024年了,想在抖音开店卖货的朋友越来越多。 主要原因还是看到,这几年在抖音上赚到钱的人越来越多。 于是大家在今年比较关心的问题,就是抖音小店开通需要多少钱? 糖果做抖音小店四年了&#…

ESP32S3各个管脚使用需要注意的情况说明:

想使用ESP32S3做个控制电路,管脚使用情况自己整理了一下,记录一下,免得后面又忘记了,有不对之处望帮助更正: IO0和IO46先要说明一下:以前只注意IO0是启动刷机的时候用的,低电平时启动是串口刷机…

剖析Redis Sentinel:构建高可用性的Redis集群管理解决方案“

在Redis中,高可用性是一个非常重要的话题。为了保证Redis集群的稳定性和可用性,Redis Sentinel(哨兵)应运而生。 本文将深入探究Redis Sentinel的原理,帮助读者理解其工作原理和实现机制。 1. 什么是Redis Sentinel&…

javascript学习路径

学习JavaScript的路径可以根据不同的学习目标和个人偏好有所不同,但以下是一条普遍认可的学习路线,适合初学者逐步掌握JavaScript编程语言: 1. 基础语法 变量:了解如何声明和使用变量。数据类型:学习JavaScript的基本…

C++开发基础之函数参数传递的几种类型

一、前言 在C中,接口指针或类对象的函数参数传递是一个常见的做法,特别是在需要支持多态或动态绑定时。这里将介绍如何传递接口指针或类对象作为函数参数。 二、函数参数传递的几种类型 抽象类(接口)的实例只能通过指针或引用传…

如何查看打包后的jar包启动方法

背景 有时候我们在引用一个jar包的时候,想查看一个jar包的结构,这时候查看启动类就比较重要,因为一些关键配置是在启动类上的,这里教大家如何查看这个启动类(springboot项目) 步骤 首先打开jar包预览结构,可以使用解压缩工具直接双击打开或者预览结构 打开路径 META-INF/MA…

使用公有云主机部署ftp服务被动模式(centos操作系统)

文章目录 前言一、FTP服务搭建1.1 部署服务1.2 修改配置文件1.3 重启服务1.3 配置项解答 二、安全组设置访问规则2.1配置监听端口2.2 配置数据端口三、使用ftp登陆工具测试3.1 使用工具进行测试 总结 前言 使用公有云上的云主机搭建FTP服务器。 步骤思路: 1、云主机…

java将图片转为pdf

效果图 直接上代码 1.引入jar <dependency><groupId>org.apache.pdfbox</groupId><artifactId>pdfbox</artifactId><version>2.0.24</version></dependency> 2.测试类 package pers.wwz.study.img2pdf20240507;import org.a…

scitb5函数2.1版本(交互效应函数P for interaction)发布----用于一键生成交互效应表、森林图

写在前面的话&#xff0c;此函数不适用于NHANES数据&#xff0c;也不能用于COX回归,请注意甄别。 在SCI文章中&#xff0c;交互效应表格&#xff08;通常是表五&#xff09;几乎是高分SCI必有。因为增加了亚组人群分析&#xff0c;增加了文章的可信度&#xff0c;能为文章锦上添…

电脑文件批量重命名不求人:快速操作,高效技巧让你轻松搞定

在数字化时代&#xff0c;电脑文件的管理与整理显得尤为重要。当面对大量需要重命名的文件时&#xff0c;一个个手动修改不仅耗时&#xff0c;还容易出错。那么&#xff0c;有没有一种方法可以快速、高效地完成这一任务呢&#xff1f;答案是肯定的&#xff0c;下面就来介绍几种…

在C语⾔中,⼀个结构体可以包含指向⾃⼰的指针吗?

一、问题 typedef struct {int num;short age;stu next; } *stu; 上述这段代码为什么编译出错&#xff1f;⼀个结构体不可以包含指向⾃⼰的指针吗&#xff1f; 二、解答 在C语⾔中&#xff0c;⼀个结构体可以包含指向⾃⼰的指针&#xff0c;例如这样⼀个结构体类型&#xff1…

如何使用Sentinel实现流控和降级

Sentinel 是一款面向分布式系统的流量控制、熔断和自适应限流工具&#xff0c;由Alibaba开源。Sentinel 以Java客户端的形式提供&#xff0c;可以嵌入到Java应用中以保护系统稳定运行。 以下是使用Sentinel实现流量控制和降级操作的详细步骤&#xff1a; 1. 添加Sentinel依赖…

盘点一下近年来常用的电脑监控软件

企业电脑监控软件通常用于监视员工在工作时间内的电脑使用情况&#xff0c;以确保他们的工作效率和安全性。以下是几种常见的企业电脑监控软件&#xff1a; 1、Ping32 Ping32是一款集成多功能的企业级电脑监控软件&#xff0c;包括员工上网行为管理、文件外发审计、屏幕活动监…

TCP(Transmission Control Protocol,传输控制协议)如何保证数据的完整性?

TCP&#xff08;Transmission Control Protocol&#xff0c;传输控制协议&#xff09;通过一系列机制来保证数据传输的可靠性和无错性&#xff0c;这些机制主要包括&#xff1a; 校验和&#xff1a;TCP报文段包含一个校验和字段&#xff0c;用于检测数据在传输过程中是否出错。…

(41)5.6-5.7数据结构(栈和队列的应用)

1.栈在括号匹配中的应用 #define _CRT_SECURE_NO_WARNINGS #define MaxSize 10 typedef struct { char data[MaxSize];//静态数组存放栈中元素 int top; //栈顶指针 }SqStack;//初始化栈 void InitStack(SqStack& S);//判断栈是否为空 bool StackEmpty(SqStack S…