掘金 后端 ( ) • 2024-04-28 20:44

theme: fancy highlight: agate

内容梗概

本文深入剖析Redis服务器初始化Lua环境的完整过程,重点解读Redis对Lua环境的核心优化。这些优化不仅重塑了Lua脚本的执行逻辑,还为Redis中的Lua脚本运行设定了特定的规则和限制。深度剖析伪客户端,洞察Redis命令在Lua环境中的执行细节;而脚本字典的解析则揭示了SCRIPT EXISTS命令的运作机制及脚本复制的实现原理。

此外,本文还详细解读EVAL和EVALSHA命令的内在逻辑,以及SCRIPT FLUSH、EXISTS、LOAD和KILL等关键脚本管理命令的实现机制。这些深入的分析与解读,对于深入理解Redis的Lua脚本功能,以及高效利用Redis的脚本管理能力具有重要意义。

什么是Lua

对于熟悉Redis功能的开发相关人员而言,Lua语言想必不会陌生。作为Redis的一个重要扩展,Lua在Redis中扮演着至关重要的角色,使得Redis具备了更为强大的脚本处理能力。

Lua是一款轻量级脚本语言,以标准C语言为基础,其源码开放,设计初衷即是为了嵌入各类应用程序,赋予它们灵活的扩展与定制能力。Lua的诞生可追溯到1993年,由巴西里约热内卢天主教大学的研究小组精心打造,团队成员包括Roberto Ierusalimschy、Waldemar Celes以及Luiz Henrique de Figueiredo等杰出学者

Lua的应用

Lua凭借其灵活、轻量、易嵌入的特点,在游戏开发、独立应用脚本、Web应用脚本、数据库和插件扩展等多个领域都有着广泛的应用。 在这里插入图片描述

  • 应用程序的灵活扩展与个性化定制:作为一种轻量级的脚本语言,Lua能够迅速集成到多种应用中,为应用程序的动态逻辑扩展提供强大支持,满足用户多样化的功能需求,实现真正的个性化定制。

  • 游戏开发中的高效更新与维护:游戏开发者可以利用Lua编写游戏的核心逻辑、角色行为以及事件触发机制,极大地提升了游戏的交互性和可玩性。更值得一提的是,当游戏需要更新或维护时,开发者仅需对Lua脚本进行修改,无需重新安装整个游戏,这极大地提高了游戏的更新效率和可维护性。

  • 独立应用脚本的自动化与智能化:通过Lua脚本,开发者可以轻松实现应用的自动化操作、数据处理以及界面交互,极大地提升了应用的易用性和智能化水平。

  • Web应用脚本的高效处理与交互:作为服务器端脚本语言,Lua能够高效处理Web请求、生成动态页面以及实现复杂的业务逻辑。同时,Lua还能与Web前端技术无缝对接,实现前后端数据的实时交互和处理,为Web应用带来更加流畅和高效的用户体验。

  • 数据库与插件开发的强大扩展:通过编写Lua脚本,开发者可以实现对数据库的灵活操作、自定义查询以及数据转换等功能。同时,Lua还可以作为插件开发语言,为各种应用提供丰富的功能和特性,进一步拓展应用的使用场景和可能性。

Lua的特点

Lua凭借诸多独特优势,如简洁的语法、高效的字节码执行、复杂数据结构处理能力、动态类型特性以及自动化的内存管理,成为嵌入式设备和智能移动设备中理想的脚本引擎。总体总结为一下六点,如下图所示。 在这里插入图片描述

  • 高效稳定】:Lua作为脚本语言,其高效性广受赞誉。众多大型程序选择用Lua编写易变部分,不仅未降低系统运行效率,反而显著提升了程序的稳定性和可扩展性。

  • 跨平台性】:Lua官方网站提供多平台发布包,如Linux/Unix、Windows等,轻松实现跨平台应用。

  • 方便嵌入】:Lua被明确定位为嵌入式脚本语言,与其他编程语言如C/C++交互流畅,也可嵌入Java和C#中,实现代码直接交互。

  • 简洁强大】:Lua作为过程化脚本语言,通过meta-mechanisms机制,兼具面向对象特征如对象和继承,同时保持语法简单。

  • 轻量便携】:最新版本的Lua仅包含约2万行C语言代码,编译后库文件大小约240K,非常适合资源有限的平台。

  • 开源免费】:Lua采用MIT Licence,可自由用于各种商业程序,无需额外费用。

Redis引入Lua脚本

自Redis 2.6版本起,引入了对Lua脚本的支持。这一创新功能通过在服务器中嵌入Lua环境,使Redis客户端能够利用Lua脚本在服务器端原子性地执行多个Redis命令,从而提升了操作的效率和一致性。

Redis中引入Lua的原因

在这里插入图片描述

Lua脚本的原子处理能力

Redis服务器严格确保Lua脚本以单线程方式原子性执行,这意味着在脚本的整个处理流程中,其执行将不会受到其他请求的干扰或打断。这种机制确保了数据的一致性和完整性,为用户提供了高度可靠的操作环境。

注意,Redis将确保整个脚本作为一个不可分割的单元执行,避免了其他请求的插入干扰,从而保证了操作的原子性。这种原子性不仅消除了竞态条件的隐患,还使我们在执行复杂逻辑时无需额外使用事务机制,简化了操作流程。

Lua减少网络开销

通过利用Redis的Lua脚本功能,我们能够实现多个请求的批量发送,从而显著减少网络往返时延,优化整体性能。

Lua脚本复用与提高效率

Redis还提供了脚本的持久化存储功能。一旦客户端发送的脚本被存储,其他客户端便可直接复用,无需重复编写或传输相同的代码逻辑。

Redis中Lua的常用命令

指令:EVAL

在Redis服务环境下,EVAL指令为用户提供了一个强大的功能,即执行自定义的Lua脚本。这些Lua脚本能够对Redis中的数据进行一系列复杂操作,从而满足了那些无法通过简单Redis命令实现的需求。

指令格式

EVAL script numkeys key [key ...] arg [arg ...]
  • 【脚本代码(script)】:它指的待执行的Lua程序代码,封装了针对Redis数据模型的操作逻辑。

脚本代码经编译后在Redis服务器端运行,旨在实现诸如事务控制、条件判断、计算密集型处理等高级功能,从而提升数据操作的效率与一致性

  • 【数据键数量(numkeys)】:它是一个整数值,它精确指明了脚本内部所引用Redis键的个数(如没有key,则为0)。

直接影响了脚本执行期间与数据库键空间的交互方式,如确保脚本原子性执行所需的WATCH/MULTI/EXEC机制能够正确识别涉及的键,以及在EVAL命令中正确解析后续的键列表

  • 【数据键列表(key [key ...])】:它是按照脚本需求提供的一个变长Redis键序列,每个键均代表数据库中的一个数据的Key。例如:在脚本里面可以通过KEYS[1], KEYS[2]进行获取第一个值和第二个值,数组下标是从1开始。

Key键作为脚本执行过程中的操作对象,可能涉及到读取、更新、删除等操作。键列表的具体内容应与numkeys参数所声明的数量相匹配,确保脚本对各指定键的操作得以精准定位并执行

  • 【参数列表(arg [arg ...])】:它包含了传递给Lua脚本的一系列非键值型数据输入,用于扩展脚本的灵活性与适应性,例如:在脚本里面可以通过ARGV[1],ARGV[2]进行获取第一个值和第二个值,与key相同也是下标从1开始。

参数可以是数值、字符串或其他可序列化数据类型,供脚本内部函数或逻辑根据需要进行解析与利用。参数与键列表相互独立,共同作为脚本执行的上下文环境,允许脚本在处理Redis键的同时,依据外部传入的动态信息做出决策或进行更为复杂的运算

案例介绍

举例来说,若你拥有一个Lua脚本,该脚本需要两个Key作为输入参数,分别是param1和param2。此脚本还会接受一个额外的参数值10,并将其用于比较操作。在此情境下,你可以利用EVAL指令来执行该脚本,具体使用方式如下:

127.0.0.1:6379>  EVAL "$(cat script.lua)"  2  param1 param2 10

Redis函数:call和pcall

在Lua脚本中,有两个关键函数可用于执行Redis命令,它们分别是redis.call()和redis.pcall()。这两个函数的核心功能相似,但它们在处理命令执行过程中产生的错误时采取了不同的策略。

redis.call操作

在Lua脚本中,我们巧妙地利用redis.call()方法来直接调用Redis的各项指令。举个案例,使用Lua脚本时,首先通过KEYS指令收集所需处理的键集合,随后遍历这些键执行删除操作,以此实现批量清理的目标。

-- 获取传入的需要批量删除的key的前缀  
local prefix = KEYS[1]  
-- 通过KEYS命令查询所有符合条件的key  
local keysToDelete = redis.call('keys', prefix .. '*')  
-- 如果没有找到任何key,则返回0  
if #keysToDelete == 0 then  
    return 0  
end  
-- 循环删除找到的key  
for _, key in ipairs(keysToDelete) do  
    redis.call('del', key)  
end  
-- 返回删除的key数量  
return #keysToDelete

redis.call()一旦在执行Redis命令的过程中遭遇任何异常,会即刻终止脚本的进一步执行,并反馈给用户一个富含脚本执行错误细节的响应。这种即时错误反馈机制蕴含了高度的透明性,它不仅清晰地阐述了错误发生的根本原因,还附带了详尽的上下文信息,极大便利了开发者迅速识别故障根源并采取相应修复措施,从而提升了调试效率与代码稳定性。

redis.pcall

当与Redis交互时,除了redis.call()函数,Lua脚本还提供了redis.pcall()函数。与redis.call()不同,redis.pcall()在处理过程中遇到错误时,并不会引发(raise)Lua错误,而是返回一个特殊的Lua表(table),其中包含一个名为err的字段,用于表示发生的错误详情。

pcall方法能够更加灵活地处理潜在的错误情况,避免脚本因异常而中断执行。通过使用redis.pcall(),开发者可以编写出更加健壮和容错的Redis操作逻辑。

redis-cli执行

无参数执行

redis-cli -h 地址 -p 端口 -a 密码 --eval /path/exec.lua 

多值传送示例

redis-cli -h 地址 -p 端口 -a 密码 --eval /path/exec.lua 2 "key1" "key2" , "va1" "va2"

Linux命令执行

cat 脚本路径/redis_test_data.txt | redis-cli -h 地址 -p 端口 -a 密码

最终总结

  • Redis通过单一Lua解释器执行脚本,确保脚本操作具有原子性,类似于事务处理,既不干扰其他脚本/命令,也不会被其影响,表现为对外完全完成或未开始。但注意,长时间运行的脚本可能阻塞其他客户端请求。

  • 为提升效率,虽有内置脚本缓存减少重复编译,频繁发送完整脚本作为EVAL命令参数仍非最优解,因涉及不必要的带宽消耗。为此,Redis实施了脚本缓存策略:一经执行的脚本将永久存储,后续调用可通过EVALSHA命令,直接利用SHA哈希值引用缓存中的脚本,避免重复传输,优化了带宽使用并促进了脚本的高效复用。

  • 利用Redis执行Lua脚本的操作极大地简化了许多基础的运维工作,使操作变得更为高效便捷。在开发过程中,通过执行Lua脚本的方式,我们可以有效降低对Redis的操作频率,从而最大限度地发挥其性能优势。