百度360必应搜狗淘宝本站头条
当前位置:网站首页 > IT技术 > 正文

详细介绍一下Redis内部是如何执行Lua脚本的?

wptr33 2025-01-17 13:13 19 浏览

在之前的分享介绍中,我们知道,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 能够提供更加灵活的解决方案。

相关推荐

威信Chronosonic XVX全新旗舰全球首发 设计特点彻底公开

第一眼看到WilsonAudio新推出的ChronosonicXVX音箱,相信大家都会直觉认为它是两年前超级旗舰WAMMMasterChronosonic的缩小版,不过这个推测并不完全正确。C...

C#高精度Timer和Delay以及时间测量

在PCHMI7.0后在工具箱里会多一个MsTimer,以及Delay和Microsecond两个类。...

python教程从基础到精通,第9课—日期与时间

Hello,小伙伴们,祝大家五.一玩得快乐!刚学习完了七大数据类型,今天咱们来学习日期与时间的表示方法。Python标准库中提供了时间和日期的支持:calendar:日历相关;time、datetim...

软件测试|教你轻松玩转Python日期时间

Python基础之日期时间处理...

Go语言中互斥锁与读写锁,你知多少?

简述Golang中的锁机制主要包含互斥锁和读写锁互斥锁互斥锁是传统并发程序对共享资源进行控制访问的主要手段。在Go中主要使用sync.Mutex的结构体表示。一个简单的示例:funcmutex()...

变形金刚动画大电影——经典台词赏析

YOURDAYSARENUMBEREDNOW,DECEPTI-CREEPS你们活不了多久了,霸天虎小子。-{铁皮说的话,体现了铁皮的嫉恶如仇,可是后来铁皮在飞船上遇袭身亡,可谓是出师未捷身先...

Python时间日期模块使用教程(python3日期)

1.时间日期处理概述在日常编程中,时间日期处理是非常常见的需求,比如:记录日志时间...

亚马逊介绍AWS“无服务器”云服务改进:数据库可线上扩充容量等

IT之家11月29日消息,在今天于美国拉斯维加斯展开的亚马逊“AWSre:Invent2023”活动中,亚马逊计算部门资深副总裁PeterDeSantis,介绍了旗下三款云端服务,IT...

2.日期格式 datetime(日期时间显示格式)

fromdatetimeimportdatetime1.获取当前日期和时间now=datetime.now()#2025-05-3110:56:01.4687822.格式化日期...

【科普】时间单位大盘点(时间单位都有哪些?)

时间单位,是7种基本单位之一,长度、时间、质量、物质的量、光照度、电流和(热力学)温度是七种基本单位。本词条中时间单位以时间从大到小列。今天我们来盘点下时间的单位换算...

基于PHP的Laravel框架,盘点Github高星Web管理后台,效率为王!

在Web开发工作中,选择一个高效、稳定的后台管理系统是提高开发效率的关键。虽然PHP在近些年中的热度有所减退,但其上手简单、开源、灵活且被广泛应用的特点,仍然使其在编程语言排行榜中保持前十的位置。这表...

如何使用PHP编写一个简单的留言板?

留言板是一个常见的Web应用程序,允许用户在网站上发布和查看留言。在本文中,我们将使用PHP编写一个简单的留言板,介绍构建过程中的关键步骤和技巧。一、准备工作在开始编写留言板之前,我们需要准备好以下工...

产品经理提需求时要考虑的 15 个隐性需求

虽然世界充满未知的变化,但是有一些大的方向还是可以把握的,本文跟大家谈谈产品经理提需求时要考虑的15个隐性需求,enjoy~俗话说,计划赶不上变化快,无论需求文档做得如何细致,考虑得如何周全,总会...

关于 PHP 启动 MongoDb 找不到指定模块问题

前言:最近有一个小demo,需要通过PHP将用户行为记录储存到MongoDB,再用Spark做协同过滤。由于以前处理跨语言交互是通过消息中间件,这次本地使用MongoDB却弄出了几个问...

PHP程序员老鸟面试经历(php程序员怎么样)

在任何时代找任何工作都有面试这么一说的。特别是高端技术类的工种对技术理论和技术实操能力要求很严格。大部分公司招收技术员工的要求也越来愈高。至于PHP程序员也是如此,我估计大多数PHP老鸟已经不在意所...