Redis设计与实现之Lua 脚本

目录

一、 Lua 脚本

1、初始化 Lua 环境

2、脚本的安全性

3、脚本的执行

4、 EVAL 命令的实现

定义 Lua 函数

执行 Lua 函数

5、 EVALSHA 命令的实现

二、 小结


一、 Lua 脚本

Lua 脚本功能是 Reids 2.6 版本的最大亮点,通过内嵌对 Lua 环境的支持,Redis 解决了长久 以来不能高效地处理 CAS (check-and-set)命令的缺点,并且可以通过组合使用多个命令,轻 松实现以前很难实现或者不能高效实现的模式。

本章先介绍 Lua 环境的初始化步骤,然后对 Lua 脚本的安全性问题、以及解决这些问题的方 法进行说明,最后对执行 Lua 脚本的两个命令——EVAL 和 EVALSHA 的实现原理进行介绍。

1、初始化 Lua 环境

在初始化 Redis 服务器时,对 Lua 环境的初始化也会一并进行。

为了让 Lua 环境符合 Redis 脚本功能的需求,Redis 对 Lua 环境进行了一系列的修改,包括添 加函数库、更换随机函数、保护全局变量,等等。

整个初始化 Lua 环境的步骤如下:
1. 调用 lua_open 函数,创建一个新的 Lua 环境。

2. 载入指定的 Lua 函数库,包括:

  • 基础库(base lib)。

  • 表格库(table lib)。

  • 字符串库(string lib)。

  • 数学库(math lib)。

  • 调试库(debug lib)。

  • 用于处理 JSON 对象的 cjson 库。

  • 在 Lua 值和 C 结构 (struct) 之间进行转换的 struct 库 (www.inf.puc- rio.br/ roberto/struct/)处理 MessagePack 数据的 cmsgpack 库(github.com/antirez/lua-cmsgpack)。

3. 屏蔽一些可能对 Lua 环境产生安全问题的函数,比如 loadfile 。

4. 创建一个 Redis 字典,保存 Lua 脚本,并在复制(replication)脚本时使用。字典的键为 SHA1 校验和,字典的值为 Lua 脚本。

5. 创建一个 redis 全局表格到 Lua 环境,表格中包含了各种对 Redis 进行操作的函数,包 括:

• 用于执行 Redis 命令的 redis.call 和 redis.pcall 函数。
• 用于发送日志(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函数。

  1. 用 Redis 自己定义的随机生成函数,替换 math 表原有的 math.random 函数和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显 式地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。

  2. 创建一个对 Redis 多批量回复(multi bulk reply)进行排序的辅助函数。

  3. 对 Lua 环境中的全局变量进行保护,以免被传入的脚本修改。

  4. 因为 Redis 命令必须通过客户端来执行,所以需要在服务器状态中创建一个无网络连接 的伪客户端(fake client),专门用于执行 Lua 脚本中包含的 Redis 命令:当 Lua 脚本需 要执行 Redis 命令时,它通过伪客户端来向服务器发送命令请求,服务器在执行完命令 之后,将结果返回给伪客户端,而伪客户端又转而将命令结果返回给 Lua 脚本。

  5. 将 Lua 环境的指针记录到 Redis 服务器的全局状态中,等候 Redis 的调用。

以上就是 Redis 初始化 Lua 环境的整个过程,当这些步骤都执行完之后,Redis 就可以使用Lua 环境来处理脚本了。
严格来说,步骤 1 至 8 才是初始化 Lua 环境的操作,而步骤 9 和 10 则是将 Lua 环境关联到服务器的操作,为了按顺序观察整个初始化过程,我们将两种操作放在了一起。
另外,步骤 6 用于创建无副作用的脚本,而步骤 7 则用于去除部分 Redis 命令中的不确定性(non deterministic),关于这两点,请看下面一节关于脚本安全性的讨论。

2、脚本的安全性

当将 Lua 脚本复制到附属节点,或者将 Lua 脚本写入 AOF 文件时,Redis 需要解决这样一个 问题:如果一段 Lua 脚本带有随机性质或副作用,那么当这段脚本在附属节点运行时,或者从 AOF 文件载入重新运行时,它得到的结果可能和之前运行的结果完全不同。

考虑以下一段代码,其中的 get_random_number() 带有随机性质,我们在服务器 SERVER 中 执行这段代码,并将随机数的结果保存到键 number 上:

# 虚构例子,不会真的出现在脚本环境中
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 number
OK
redis> GET number
"10086"

现在,假如 EVAL 的代码被复制到了附属节点 SLAVE ,因为 get_random_number() 的随机 性质,它有很大可能会生成一个和 10086 完全不同的值,比如 65535 : 

# 虚构例子,不会真的出现在脚本环境中
redis> EVAL "return redis.call('set', KEYS[1], get_random_number())" 1 numberOK
redis> GET number
"65535"

可以看到,带有随机性的写入脚本产生了一个严重的问题:它破坏了服务器和附属节点数据之 间的一致性。

当从 AOF 文件中载入带有随机性质的写入脚本时,也会发生同样的问题。

Note: 只有在带有随机性的脚本进行写入时,随机性才是有害的。 如果一个脚本只是执行只读操作,那么随机性是无害的。

比如说,如果脚本只是单纯地执行 RANDOMKEY 命令,那么它是无害的;但如果在执行RANDOMKEY 之后,基于 RANDOMKEY 的结果进行写入操作,那么这个脚本就是有害的。

和随机性质类似,如果一个脚本的执行对任何副作用产生了依赖,那么这个脚本每次执行所产 生的结果都可能会不一样。

为了解决这个问题,Redis 对 Lua 环境所能执行的脚本做了一个严格的限制——所有脚本都必 须是无副作用的纯函数(pure function)。

为此,Redis 对 Lua 环境做了一些列相应的措施:
• 不提供访问系统状态状态的库(比如系统时间库)。 • 禁止使用 loadfile 函数。

• 如果脚本在执行带有随机性质的命令(比如 RANDOMKEY ),或者带有副作用的命令 (比如 TIME )之后,试图执行一个写入命令(比如 SET ),那么 Redis 将阻止这个脚本 继续运行,并返回一个错误。

• 如果脚本执行了带有随机性质的读命令(比如 SMEMBERS ),那么在脚本的输出返回给Redis 之前,会先被执行一个自动的字典序排序,从而确保输出结果是有序的。

用 Redis 自己定义的随机生成函数,替换 Lua 环境中 math 表原有的 math.random 函数 和 math.randomseed 函数,新的函数具有这样的性质:每次执行 Lua 脚本时,除非显式 地调用 math.randomseed ,否则 math.random 生成的伪随机数序列总是相同的。

经过这一系列的调整之后,Redis 可以保证被执行的脚本:
1. 无副作用。
2. 没有有害的随机性。
3. 对于同样的输入参数和数据集,总是产生相同的写入命令。

3、脚本的执行

在脚本环境的初始化工作完成以后,Redis 就可以通过 EVAL 命令或 EVALSHA 命令执行 Lua脚本了。
其中,EVAL 直接对输入的脚本代码体(body)进行求值:

redis> EVAL "return 'hello world'" 0
"hello world"

而 EVALSHA 则要求输入某个脚本的 SHA1 校验和,这个校验和所对应的脚本必须至少被EVAL 执行过一次:

redis> EVAL "return 'hello world'" 0
"hello world"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0 // 上一个脚本的校验和 "hello world"

或者曾经使用 SCRIPT LOAD 载入过这个脚本:

redis> SCRIPT LOAD "return 'dlrow olleh'"
"d569c48906b1f4fca0469ba4eee89149b5148092"
redis> EVALSHA d569c48906b1f4fca0469ba4eee89149b5148092 0
"dlrow olleh"

因为 EVALSHA 是基于 EVAL 构建的,所以下文先用一节讲解 EVAL 的实现,之后再讲解 EVALSHA 的实现。

4、 EVAL 命令的实现

EVAL 命令的执行可以分为以下步骤:

1. 为输入脚本定义一个 Lua 函数。

2. 执行这个 Lua 函数。以下两个小节分别介绍这两个步骤。

定义 Lua 函数

所有被 Redis 执行的 Lua 脚本,在 Lua 环境中都会有一个和该脚本相对应的无参数函数:当 调用 EVAL 命令执行脚本时,程序第一步要完成的工作就是为传入的脚本创建一个相应的 Lua 函数。

举个例子,当执行命令 EVAL "return 'hello world'" 0 时,Lua 会为脚本 "return 'hello world'" 创建以下函数:

其中,函数名以 f_ 为前缀,后跟脚本的 SHA1 校验和(一个 40 个字符长的字符串)拼接而 成。而函数体(body)则是用户输入的脚本。

以函数为单位保存 Lua 脚本有以下好处:

  • 执行脚本的步骤非常简单,只要调用和脚本相对应的函数即可。

  • Lua 环境可以保持清洁,已有的脚本和新加入的脚本不会互相干扰,也可以将重置 Lua 环境和调用 Lua GC 的次数降到最低。

  • 如果某个脚本所对应的函数在 Lua 环境中被定义过至少一次,那么只要记得这个脚本的 SHA1 校验和,就可以直接执行该脚本——这是实现 EVALSHA 命令的基础,稍后在介 绍 EVALSHA 的时候就会说到这一点。

在为脚本创建函数前,程序会先用函数名检查 Lua 环境,只有在函数定义未存在时,程序才创建函数。重复定义函数一般并没有什么副作用,这算是一个小优化。

另外,如果定义的函数在编译过程中出错(比如,脚本的代码语法有错),那么程序向用户返回 一个脚本错误,不再执行后面的步骤。

执行 Lua 函数

在定义好 Lua 函数之后,程序就可以通过运行这个函数来达到运行输入脚本的目的了。

不过,在此之前,为了确保脚本的正确和安全执行,还需要执行一些设置钩子、传入参数之类 的操作,整个执行函数的过程如下:

  1. 将 EVAL 命令中输入的 KEYS 参数和 ARGV 参数以全局数组的方式传入到 Lua 环境中。

  2. 设置伪客户端的目标数据库为调用者客户端的目标数据库:fake_client->db =

    caller_client->db ,确保脚本中执行的 Redis 命令访问的是正确的数据库。

  3. 为 Lua 环境装载超时钩子,保证在脚本执行出现超时时可以杀死脚本,或者停止 Redis

    服务器。

  4. 执行脚本对应的 Lua 函数。

  5. 如果被执行的 Lua 脚本中带有 SELECT 命令,那么在脚本执行完毕之后,伪客户端 中的数据库可能已经有所改变,所以需要对调用者客户端的目标数据库进行更新: caller_client->db = fake_client->db 。

  6. 执行清理操作:清除钩子;清除指向调用者客户端的指针;等等。

  7. 将 Lua 函数执行所得的结果转换成 Redis 回复,然后传给调用者客户端。

  8. 对 Lua 环境进行一次单步的渐进式 GC 。

以下是执行 EVAL "return 'hello world'" 0 的过程中,调用者客户端(caller)、Redis 服务 器和 Lua 环境之间的数据流表示图:

 

上面这个图可以作为所有 Lua 脚本的基本执行流程图,不过它展示的 Lua 脚本中不带有 Redis 命令调用:当 Lua 脚本里本身有调用 Redis 命令时(执行 redis.call 或者 redis.pcall ), Redis 和 Lua 脚本之间的数据交互会更复杂一些。

举个例子,以下是执行命令 EVAL "return redis.call('DBSIZE')" 0 时,调用者客户端 (caller)、伪客户端(fake client)、Redis 服务器和 Lua 环境之间的数据流表示图:

因为 EVAL "return redis.call('DBSIZE')" 只是简单地调用了一次 DBSIZE 命令,所以 Lua 和伪客户端只进行了一趟交互,当脚本中的 redis.call 或者 redis.pcall 次数增多时,Lua 和伪客户端的交互趟数也会相应地增多,不过总体的交互方法和上图展示的一样。

5、 EVALSHA 命令的实现

前面介绍 EVAL 命令的实现时说过,每个被执行过的 Lua 脚本,在 Lua 环境中都有一个 和它相对应的函数,函数的名字由 f_ 前缀加上 40 个字符长的 SHA1 校验和构成:比如 f_5332031c6b470dc5a0dd9b4bf2030dea6d65de91 。

只要脚本所对应的函数曾经在 Lua 里面定义过,那么即使用户不知道脚本的内容本身,也可以 直接通过脚本的 SHA1 校验和来调用脚本所对应的函数,从而达到执行脚本的目的——这就是 EVALSHA 命令的实现原理。

可以用伪代码来描述这一原理:

def EVALSHA(sha1):# 拼接出 Lua 函数名字func_name = "f_" + sha1# 查看该函数是否已经在 Lua 中定义if function_defined_in_lua(func_name): # 如果已经定义过的话,执行函数return exec_lua_function(func_name) else:# 没有找到和输入 SHA1 值相对应的函数则返回一个脚本未找到错误 return script_error("SCRIPT NOT FOUND")

除了执行 EVAL 命令之外,SCRIPT LOAD 命令也可以为脚本在 Lua 环境中创建函数:

redis> SCRIPT LOAD "return 'hello world'"
"5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
redis> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 0
"hello world"

二、 小结

• 初始化 Lua 脚本环境需要一系列步骤,其中最重要的包括: – 创建 Lua 环境。

– 载入 Lua 库,比如字符串库、数学库、表格库,等等。
– 创建 redis 全局表格,包含各种对 Redis 进行操作的函数,比如 redis.call 和redis.log ,等等。
– 创建一个无网络连接的伪客户端,专门用于执行 Lua 脚本中的 Redis 命令。

• Reids 通过一系列措施保证被执行的 Lua 脚本无副作用,也没有有害的写随机性:对于 同样的输入参数和数据集,总是产生相同的写入命令。

• EVAL 命令为输入脚本定义一个 Lua 函数,然后通过执行这个函数来执行脚本。
• EVALSHA 通过构建函数名,直接调用 Lua 中已定义的函数,从而执行相应的脚本。

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

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

相关文章

Mysql之约束上篇

Mysql之约束上篇 约束的概述为什么需要约束什么是约束约束的分类 非空约束作用关键字特点添加非空约束删除非空约束 唯一性约束关键字特点添加唯一约束关于复合唯一约束删除唯一约束查看索引 主键约束(非空唯一性约束)作用关键字特点添加主键约束关于复合主键删除主 约束的概述…

探索拉普拉斯算子:计算机视觉中用于边缘检测和图像分析的关键工具

一、介绍 拉普拉斯算子是 n 维欧几里得空间中的二阶微分算子,表示为 ∇。它是函数梯度的发散度。在图像处理的上下文中,该运算符应用于图像的强度函数,可以将其视为每个像素具有强度值的二维信号。拉普拉斯算子是计算机视觉领域的关键工具&am…

了解 SBOM (软件物料清单)

近年来,开源软件在开发中的采用激增,目前已占已构建软件的高达 90%。它在全球公司中的受欢迎程度源于成本节约和产品上市时间的加快。然而,在集成开源软件组件时,有一个关键的方面需要考虑。 Synopsys 报告84% 的商业和专有代码库…

Qt-QTransform介绍与使用

QTransform是一个用于二维坐标系转换的类。我们知道Qt的坐标系是左上角为原点,x轴向右,y轴向下,屏幕上每个像素代表一个单位,那么,如果我们想要在屏幕上建立自己的坐标系用于绘制,就需要借助QTransform。 …

Guitar Pro8.1最新2024中文免激活版下载(附教程)

Guitar Pro 8是一款功能强大的指法阅读器和编辑器,它允许您编辑吉他、贝斯和尤克里里的乐谱和指法谱,并为鼓或钢琴创建背景音轨。轻松创建、播放和共享您的标签!快速的进行乐谱播放并进行练习,也可以进行编辑操作,允许…

机器学习---推荐系统案例(一)

一、推荐系统-数据处理流程 推荐系统数据处理首先是将Hive中的用户app历史下载表与app浏览信息表按照设备id进行关联,然后将关联数据使用python文件进行处理,将数据预处理为label和feature两列的临时数据,后期经过处理转换成逻辑回归 模型的…

【经典LeetCode算法题目专栏分类】【第5期】贪心算法:分发饼干、跳跃游戏、模拟行走机器人

《博主简介》 小伙伴们好,我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源,可关注公-仲-hao:【阿旭算法与机器学习】,共同学习交流~ 👍感谢小伙伴们点赞、关注! 分发饼干 class Solutio…

【Qt之Quick模块】1. 概述及Quick应用程序创建流程

概述 Qt的Quick模块是用于创建现代化、动态和响应式用户界面的工具集。它是基于QML(Qt Meta-Object Language)和JavaScript的。 QML是一种声明性的语言,用于描述用户界面的结构和行为。它使用层叠样式表(CSS)的语法来…

Apache Flume(5):多个agent模型

可以将多个Flume agent 程序连接在一起,其中一个agent的sink将数据发送到另一个agent的source。Avro文件格式是使用Flume通过网络发送数据的标准方法。 从多个Web服务器收集日志,发送到一个或多个集中处理的agent,之后再发往日志存储中心&…

电脑操作系统深度剖析:Windows、macOS和Linux的独特特性及应用场景

导言 电脑操作系统是计算机硬件和应用软件之间的桥梁,不同的操作系统在用户体验、性能和安全性方面有着独特的特色。电脑操作系统是计算机系统中的核心组件,不同的操作系统在设计理念、用户体验和应用领域上存在显著差异。本文将深入探讨几种常见的电脑操…

安全芯片是什么?为什么可以应用在加密卡上?

安全芯片是指芯片内带有微处理器CPU、随机数发生器、硬件密码算法、存储单元(包括随机存储器RAM、程序存储器ROM(FLASH)、用户数据存储器EEPROM)以及芯片操作系统COS的智能芯片,相当于一台微型计算机,不仅具…

【经典LeetCode算法题目专栏分类】【第6期】二分查找系列:x的平方根、有效完全平方数、搜索二位矩阵、寻找旋转排序数组最小值

《博主简介》 小伙伴们好,我是阿旭。专注于人工智能AI、python、计算机视觉相关分享研究。 ✌更多学习资源,可关注公-仲-hao:【阿旭算法与机器学习】,共同学习交流~ 👍感谢小伙伴们点赞、关注! X的平方根 class Soluti…

【Image】图像处理

计算机视觉 CV Perception 如自动驾驶领域。 只要是从所谓的图像当中去抽取信息的过程,我们都叫做Perception。 视觉检测可以涵盖二维检测,如车辆、人和信号灯的检测。另外,还可以控制三维信息,直接在三维空间中操作数据。 SL…

鸿蒙OS:打破界限的操作系统新星

导言 鸿蒙OS(HarmonyOS)是华为公司为应对技术封锁而推出的分布式操作系统,其背后蕴含着华为构建全球数字生活愿景的雄心。本文将深入剖析鸿蒙OS的起源、核心特性,并展望其未来在数字生态中的角色。 1. 背景与起源 华为的…

【基础篇】1.2 认识STM32(二)

3.3 VREF/VREF-引脚 VREF和VREF-是STM32中用于提供参考电压的引脚。如下图: VREF引脚可以连接一个单独的外部参考电压,范围在2.0V~VDDA,但不能超过VDDA,否则就超过了模拟器件的最大供电电压。在100引脚的封装中&#…

智慧养老:创新科技让老年生活更美好

智慧养老:创新科技让老年生活更美好 随着人口老龄化的加剧,智慧养老成为了关注焦点。智慧养老以创新科技为核心,旨在改善老年人的生活品质、促进健康、增强安全感和社会融入感。本文将详细介绍智慧养老的关键技术和应用场景,带您了…

SiLM5350MDBCA-DG车规级隔离驱动芯片,我们能为汽车智能提供什么?

SiLM5350MDBCA-DG是一款适用于IGBT、MOSFET的单通道 隔离门极驱动器,具有10A拉电流和10A灌电流驱动能 力。提供内部钳位功能,可单独控制 上升时间和下降时间。 在 SOP8 封 装 中 具 有 3000VRMS 隔 离 耐 压 ( 符 合 UL1577)。 与…

Vue 项目中使用 debugger 在 chrome 谷歌浏览器中失效以及 console.log 指向去了 vue.js 代码

问题 今天在代码里面输出 console.log 信息直接指向了 vue.js,并且代码里面写了 debgger 也不生效 解决 f12 找到浏览器的这个设置图标 找到这个 ignore list 的 custom exclusion rules 取消掉 /node_modules/|/bower_components/ 这样就正常了

思幻二次元风格的工作室个人引导页源码

思幻工作室个人引导页源码已经完成开发!该源码支持三端自适应,并且具备赞助功能。我们选择了当前点赞量最高的配色方案,打造了一个独特的二次元风格引导页。经过在美国服务器上进行的测试,效果令人满意,网页加载速度达…

【Spring】03 容器

文章目录 1. 定义2. BeanFactory1)惰性加载2)基本的容器功能3)XML配置 3. ApplicationContext1)主动加载2)AOP支持3)事件发布与监听4)国际化支持5)注解支持 4. Spring容器的生命周期…