详细介绍一下Redis内部是如何执行Lua脚本的?
wptr33 2025-01-17 13:13 29 浏览
在之前的分享介绍中,我们知道,Redis是通过EVAL命令或者是通过EVALSHA命令来调用执行Lua脚本的,当客户端请求执行Lua脚本的时候,脚本会通过EVAL命令传递到Redis服务器中,而EVALSHA命令则是通过已经执行了脚本加载命令SCRIPT LOAD加载到Redis中的Lua脚本的SHA1哈希值来调用执行脚本。
??在Redis接收到了Lua脚本之后,会将其传递到Lua虚拟机来进行脚本的加载和编译,在Redis内部主要是通过LuaJIT来执行脚本,LuaJIT是一个高效的Lua脚本即时编译器实现,并且专门是针对Lua脚本进行了优化,可以将Lua脚本编译成高度化的机器编码,这种方式可以有效的提高脚本执行的效率。
LuaJIT的工作原理
??在Lua脚本解释器执行Lua脚本代码的时候,首先会将Lua脚本源码编译成为字节码文件,然后通过Lua虚拟机去逐条的解释并且执行这些代码中的字节码,而LuaJIT中执行Lua脚本的时候,也会将Lua脚本先转换成字节码,然后会对这个字节码进行进一步的优化来脚本执行的效率。
??只不过与标准Lua脚本解释器不同的是LuaJIT提供的事一种即时编译的机制,也就在运行的时候会将字节码即时编译成机器码,这种编译与传统的编译型语言不同的是这种编译是在程序执行的过程中进行的,而传统的C或者C++就是在编译时就已经生成了字节码文件。通过LuaJIT的即时编译机制使得在Lua脚本在脚本执行的时候达到接近原生代码执行的速度。
??在脚本执行过程中,LuaJIT技术会自动检测程序中的热路径,所谓的热路径就是指被频繁执行的代码,然后将这些热路径编译成机器码,这样程序中就会直接执行这些已经编译好的机器码,如果某些逻辑是第一次执行,那么在执行的时候也会被JIT机制编译为机器码。在LuaJIT运行的过程中会对数据类型也进行优化,避免在执行机器码的过程中每次都对数据类型进行检查。
??在标准的Lua虚拟机中,会通过栈来存储中间运行结果信息,但是在LuaJIT中则是通过寄存器虚拟机来实现这种机制,其实提到寄存器很多开发者就会明白它的操作效率要比栈的操作效率要更高,这样也是通过这种寄存器模型来提高命令执行的效率,减少栈操作带来的其他系统开销。
??在LuaJIT中通过使用内联缓存技术来提高内部函数调用以及查找调用的效率,当LuaJIT发现某个函数或者是方法调用频率较高的时候,那么就会对其地址进行缓存,这样这个方法在后续的调用过程中会直接进行跳转而不需要再次进行寻址查找,从而减少了方法查找和调用带来的系统开销。
??既然是虚拟机那么不可或缺的就是垃圾回收机制,在LuaJIT中提供的垃圾回收机制与Lua中提供的垃圾回收机制类似,都是采用了标记清除算法,并且对垃圾的回收做了一定的优化,由于Lua脚本的调用执行效率较高,所以垃圾回收的速度效率也会比较高一点。
Lua脚本的键锁定
??在之前的介绍中,我们提到过Lua脚本执行的过程中,Redis会对Lua脚本操作的键值进行加锁,这样是为了保证脚本执行的原子性,也就是说在Lua脚本执行的过程中,其他的客户端无法对操作的键值进行修改。
??Redis对键值锁定的方式被称为是键级锁定,这种锁定机制通过一种被叫做分布式锁的机制来实现,确保在操作该键的时候不会被其他的客户端操作所修改。也就是说Redis中将会对所有的涉及到操作的键值进行独立管理,在脚本执行的过程中确保脚本的操作不会被其他的命令操作所打断。
??从底层实现来讲其实还是基于了Redis的事务队列实现,在脚本执行的过程中,是按照顺序对脚本命令进行执行,如下所示。
int evalGenericCommand(redisClient *c, int issha) {
// 获取 Lua 脚本中的键
unsigned long numkeys = 0;
robj **keys = luaScriptKeys(c->argv[1], c->argc, &numkeys); // 获取涉及的键
// 锁定涉及的键
lockKeysForScript(keys, numkeys);
// 执行 Lua 脚本
executeLuaScript(c, keys, numkeys);
// 解锁所有键
unlockKeys(keys, numkeys);
}
??当脚本执行完成之后,也会主动去释放所有被锁定的键值。
Redis中如何执行Lua脚本
??在Redis中通过EVAL命令来调用Lua脚本,而在编写Lua脚本的时候,涉及到了redis.call() 和 redis.pcall()这两个函数,这两个函数在脚本执行的过程中都会通过Redis提供的处理机制来与Redis数据库底层进行交互,而我们知道Redis底层命令执行是通过一个统一的命令调度框架来实现,也就是说redis.call() 和 redis.pcall()这两个函数也是基于这个Redis命令调度框架来执行。
redis.call()函数的底层实现
??在Redis中解析到了redis.call()函数的时候,这条命令就会被解析成Redis支持的RESP格式,也就是说函数调用会被解析为一条Redis的执行命令,然后进入到到Redis的命令调度机制中,通过命令表server.command_table来查找到对应的命令执行函数进行调用,例如常见的SET命令会被映射到setCommand 函数中。
??一旦找到了命令处理函数,那么就会执行该函数进行实际的数据处理操作,并修改Redis内部的数据处理状态,处理完成之后返回对应的命令执行结果。
/* 用于执行 Redis 命令 */
int redisCommand(lua_State *lua) {
int argc = lua_gettop(lua); // 获取参数的数量
// 获取命令名称
const char *command = lua_tostring(lua, 1);
// 创建一个 Redis 命令的参数列表
robj **argv = (robj **) zmalloc(sizeof(robj*) * argc);
for (int i = 0; i < argc; i++) {
argv[i] = luaToObject(lua, i + 2); // 从 Lua 栈中获取参数并转换成 Redis 对象
}
// 执行 Redis 命令
int retval = call(command, argc, argv);
zfree(argv); // 释放参数列表
return retval;
}
??如果在处理过程中,发现了异常redis.call()函数也会抛出异常并且终止Lua脚本的执行,并且会将错误信息返回到客户端。
??由于Redis的命令是在Redis主线程中执行完成的,所以在发现错误之后会立即返回并且剩余的命令将不会被执行,这个机制与Redis事务提供的机制有所不同。
redis.pcall()函数的底层实现
??这个函数的底层实现与redis.call()的底层实现机制类似,唯一不同的是redis.pcall() 命令提供了一个内置的错误捕获机制,在命令执行的过程中通过这个错误捕获机制来保护Lua脚本执行的准确性,这种机制会捕获在执行过程中发生的任何错误并且会返回一个包含了所有错误的信息表,并不会直接抛出异常。
/* 用于执行 Redis 命令,并捕获错误 */
int redisPcall(lua_State *lua) {
int argc = lua_gettop(lua);
const char *command = lua_tostring(lua, 1);
robj **argv = (robj **) zmalloc(sizeof(robj*) * argc);
for (int i = 0; i < argc; i++) {
argv[i] = luaToObject(lua, i + 2);
}
// 使用 pcall 保护性调用 Redis 命令
int retval = lua_pcall(lua, command, argc, argv);
zfree(argv);
return retval;
}
??也就是说,脚本执行过程中如果某些脚本执行失败了,redis.pcall() 会封装一个Lua的错误异常表,后续的的命令还会继续执行,并不会受到错误异常处理的影响,当脚本执行完成之后,会返回给最终脚本执行之后的一个错误异常表。这种机制就与Redis提供的事务机制有点类似了。
result = redis.pcall('GET', 'non_existing_key')
if result.err then
-- 错误处理
return "Key not found"
else
return result
end
??上面两种机制,主要的差异就体现在对于错误的处理方式上,在实际操作过程中我们可以根据实际需要的业务处理方式来选择合适的调用方式。
总结
??Redis中执行Lua脚本的过程包括脚本的加载、编译、键的锁定、执行以及结果返回等过程。通过Lua脚本的调用执行,Redis实现了对多个Redis命令组合的的原子操作,并通过锁定机制确保了脚本执行期间的数据一致性和事务性。Lua脚本的原子性和高效性,使得在复杂的操作场景下,Redis 能够提供更加灵活的解决方案。
相关推荐
- oracle数据导入导出_oracle数据导入导出工具
-
关于oracle的数据导入导出,这个功能的使用场景,一般是换服务环境,把原先的oracle数据导入到另外一台oracle数据库,或者导出备份使用。只不过oracle的导入导出命令不好记忆,稍稍有点复杂...
- 继续学习Python中的while true/break语句
-
上次讲到if语句的用法,大家在微信公众号问了小编很多问题,那么小编在这几种解决一下,1.else和elif是子模块,不能单独使用2.一个if语句中可以包括很多个elif语句,但结尾只能有一个else解...
- python continue和break的区别_python中break语句和continue语句的区别
-
python中循环语句经常会使用continue和break,那么这2者的区别是?continue是跳出本次循环,进行下一次循环;break是跳出整个循环;例如:...
- 简单学Python——关键字6——break和continue
-
Python退出循环,有break语句和continue语句两种实现方式。break语句和continue语句的区别:break语句作用是终止循环。continue语句作用是跳出本轮循环,继续下一次循...
- 2-1,0基础学Python之 break退出循环、 continue继续循环 多重循
-
用for循环或者while循环时,如果要在循环体内直接退出循环,可以使用break语句。比如计算1至100的整数和,我们用while来实现:sum=0x=1whileTrue...
- Python 中 break 和 continue 傻傻分不清
-
大家好啊,我是大田。今天分享一下break和continue在代码中的执行效果是什么,进一步区分出二者的区别。一、continue例1:当小明3岁时不打印年龄,其余年龄正常循环打印。可以看...
- python中的流程控制语句:continue、break 和 return使用方法
-
Python中,continue、break和return是控制流程的关键语句,用于在循环或函数中提前退出或跳过某些操作。它们的用途和区别如下:1.continue(跳过当前循环的剩余部分,进...
- L017:continue和break - 教程文案
-
continue和break在Python中,continue和break是用于控制循环(如for和while)执行流程的关键字,它们的作用如下:1.continue:跳过当前迭代,...
- 作为前端开发者,你都经历过怎样的面试?
-
已经裸辞1个月了,最近开始投简历找工作,遇到各种各样的面试,今天分享一下。其实在职的时候也做过面试官,面试官时,感觉自己问的问题很难区分候选人的能力,最好的办法就是看看候选人的github上的代码仓库...
- 面试被问 const 是否不可变?这样回答才显功底
-
作为前端开发者,我在学习ES6特性时,总被const的"善变"搞得一头雾水——为什么用const声明的数组还能push元素?为什么基本类型赋值就会报错?直到翻遍MDN文档、对着内存图反...
- 2023金九银十必看前端面试题!2w字精品!
-
导文2023金九银十必看前端面试题!金九银十黄金期来了想要跳槽的小伙伴快来看啊CSS1.请解释CSS的盒模型是什么,并描述其组成部分。答案:CSS的盒模型是用于布局和定位元素的概念。它由内容区域...
- 前端面试总结_前端面试题整理
-
记得当时大二的时候,看到实验室的学长学姐忙于各种春招,有些收获了大厂offer,有些还在苦苦面试,其实那时候的心里还蛮忐忑的,不知道自己大三的时候会是什么样的一个水平,所以从19年的寒假放完,大二下学...
- 由浅入深,66条JavaScript面试知识点(七)
-
作者:JakeZhang转发链接:https://juejin.im/post/5ef8377f6fb9a07e693a6061目录由浅入深,66条JavaScript面试知识点(一)由浅入深,66...
- 2024前端面试真题之—VUE篇_前端面试题vue2020及答案
-
添加图片注释,不超过140字(可选)1.vue的生命周期有哪些及每个生命周期做了什么?beforeCreate是newVue()之后触发的第一个钩子,在当前阶段data、methods、com...
- 今年最常见的前端面试题,你会做几道?
-
在面试或招聘前端开发人员时,期望、现实和需求之间总是存在着巨大差距。面试其实是一个交流想法的地方,挑战人们的思考方式,并客观地分析给定的问题。可以通过面试了解人们如何做出决策,了解一个人对技术和解决问...
- 一周热门
- 最近发表
- 标签列表
-
- git pull (33)
- git fetch (35)
- mysql insert (35)
- mysql distinct (37)
- concat_ws (36)
- java continue (36)
- jenkins官网 (37)
- mysql 子查询 (37)
- python元组 (33)
- mybatis 分页 (35)
- vba split (37)
- redis watch (34)
- python list sort (37)
- nvarchar2 (34)
- mysql not null (36)
- hmset (35)
- python telnet (35)
- python readlines() 方法 (36)
- munmap (35)
- docker network create (35)
- redis 集合 (37)
- python sftp (37)
- setpriority (34)
- c语言 switch (34)
- git commit (34)
