写点什么

大数据 -43 Redis Lua 脚本实战全解析 eval redis.call redis.pcall

作者:武子康
  • 2025-07-17
    美国
  • 本文字数:4634 字

    阅读完需:约 15 分钟

大数据-43 Redis Lua脚本实战全解析 eval redis.call redis.pcall

点一下关注吧!!!非常感谢!!持续更新!!!

🚀 AI 篇持续更新中!(长期更新)

AI 炼丹日志-30-新发布【1T 万亿】参数量大模型!Kimi‑K2 开源大模型解读与实践,持续打造实用 AI 工具指南!📐🤖

💻 Java 篇正式开启!(300 篇)

目前 2025 年 07 月 16 日更新到:Java-74 深入浅出 RPC Dubbo Admin 可视化管理 安装使用 源码编译、Docker 启动 MyBatis 已完结,Spring 已完结,Nginx 已完结,Tomcat 已完结,分布式服务正在更新!深入浅出助你打牢基础!

📊 大数据板块已完成多项干货更新(300 篇):

包括 Hadoop、Hive、Kafka、Flink、ClickHouse、Elasticsearch 等二十余项核心组件,覆盖离线+实时数仓全栈!大数据-278 Spark MLib - 基础介绍 机器学习算法 梯度提升树 GBDT 案例 详解


Lua 是一门轻量级、高性能、易嵌入的脚本语言,被广泛应用于游戏开发、嵌入式系统和数据处理领域。而在 Redis 中,Lua 脚本因其原子性与灵活性成为复杂操作和事务逻辑的首选工具。本文首先介绍了 Lua 的背景、核心特性与典型应用场景,随后深入讲解了 Redis 中 EVAL/EVALSHA 的语法、参数机制、执行模型,并结合多个实用案例(如原子计数器、CAS 更新、批量插入、哈希批量设置等)展开详细剖析。此外,还对 redis.call 与 redis.pcall 的差异、脚本缓存机制以及脚本调试方法进行了全面梳理,适合所有希望系统掌握 Redis Lua 脚本能力的开发者阅读收藏。


章节内容

上一节我们完成了如下的内容:


  • Redis 功能扩展

  • Redis 发布/订阅模式

  • Redis 事务相关

  • Redis 为什么是弱事务

  • 等等

背景介绍

这里是三台公网云服务器,每台 2C4G,搭建一个大数据的学习环境,供我学习。


  • 2C4G 编号 h121

  • 2C4G 编号 h122

  • 2C2G 编号 h123


Lua 介绍

简介一下

Lua 是一个轻量级、高效率的脚本语言,由巴西里约热内卢天主教大学(PUC-Rio)的 Tecgraf 实验室于 1993 年开发。它采用标准的 ANSI C 编写,具有跨平台特性,可以在大多数操作系统上运行,包括 Windows、Linux、macOS 等。Lua 的设计目标之一是保持核心精简(整个解释器仅约 200KB),同时通过灵活的扩展机制提供强大的功能。其源代码遵循 MIT 许可协议开放,允许自由使用和修改。


Lua 的主要特点包括:


  • 动态类型系统

  • 自动内存管理

  • 一流的函数支持

  • 简洁清晰的语法

  • 高效的字节码解释器

  • 简易的 C API 接口


典型应用场景包括:


  1. 游戏开发 Lua 因其轻量级和高性能的特点,被广泛用于游戏开发中作为脚本引擎。例如:

  2. 《魔兽世界》使用 Lua 编写插件和 UI 定制

  3. 《愤怒的小鸟》系列游戏使用 Lua 实现游戏逻辑

  4. 知名游戏引擎如 Cocos2d-x、Unity 等都支持 Lua 脚本

  5. 独立应用脚本

  6. Adobe Lightroom 使用 Lua 实现插件系统

  7. Wireshark 网络分析工具使用 Lua 编写协议解析器

  8. VLC 媒体播放器通过 Lua 扩展功能

  9. Web 应用脚本

  10. 作为 Nginx 的脚本扩展(OpenResty 项目)

  11. 用于实现 Web 应用的业务逻辑(如 Lapis 框架)

  12. 轻量级 API 网关和服务编排

  13. 数据库插件

  14. Redis 支持 Lua 脚本实现复杂原子操作

  15. PostgreSQL 可通过 PL/Lua 编写存储过程

  16. MongoDB 支持使用 Lua 编写 MapReduce 任务


此外,Lua 还被应用于:


  • 嵌入式系统(如路由器固件)

  • 科学计算和数据分析

  • 自动化测试脚本

  • 网络安全工具开发

  • 工业控制领域


Lua 的扩展库生态系统(LuaRocks)提供了丰富的第三方模块,涵盖网络编程、GUI 开发、数据结构等各个方面,进一步扩展了其应用范围。

下载安装

# 下载页面http://www.lua.org/download.html# 下载地址https://www.lua.org/ftp/lua-5.4.7.tar.gz
复制代码

Redis EVAL 命令详解

命令语法

EVAL script numkeys key [key ...] arg [arg ...]
复制代码

参数详细说明

  1. script

  2. 一段 Lua 5.1 脚本程序,这段脚本会在 Redis 服务器的上下文中执行。脚本可以包含任意有效的 Lua 代码,并能够调用 Redis 命令。例如:


   return redis.call('GET', KEYS[1])
复制代码


  1. numkeys

  2. 指定后续参数中有多少个是键名(key)。这个数值必须是非负整数,用于帮助 Redis 区分键名参数和普通参数。

  3. key

  4. 从命令的第三个参数开始,numkeys 个参数会被视为键名。这些键名可以在 Lua 脚本中通过全局变量 KEYS 数组访问,索引从 1 开始(KEYS[1], KEYS[2]等)。

  5. arg

  6. 在键名参数之后的参数会被视为普通参数。这些参数可以在 Lua 脚本中通过全局变量 ARGV 数组访问,同样索引从 1 开始(ARGV[1], ARGV[2]等)。

使用示例

基本用法

以下命令展示了如何同时传递键名和参数给 Lua 脚本:


eval "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 first second
复制代码


输出结果将会是:


1) "key1"2) "key2"3) "first"4) "second"
复制代码

实际应用场景

  1. 原子性操作

  2. 实现一个原子性的 get-and-set 操作:


   eval "local val = redis.call('GET', KEYS[1]); redis.call('SET', KEYS[1], ARGV[1]); return val" 1 mykey newvalue
复制代码


  1. 条件更新

  2. 只有在旧值匹配时才更新:


   eval "if redis.call('GET', KEYS[1]) == ARGV[1] then return redis.call('SET', KEYS[1], ARGV[2]) else return 0 end" 1 mykey oldvalue newvalue
复制代码


  1. 复杂计算

  2. 使用 Lua 进行复杂计算:


   eval "local sum = 0; for i = 1, #ARGV do sum = sum + tonumber(ARGV[i]) end; return sum" 0 10 20 30
复制代码


这将返回 60(10+20+30 的和)

注意事项

  1. 脚本中的 Redis 命令调用必须使用redis.call()redis.pcall()函数

  2. Lua 数组索引从 1 开始,而不是 0

  3. 在集群模式下,所有键必须在同一个哈希槽中

  4. 脚本执行是原子性的,执行期间不会执行其他命令

Redis Lua 脚本执行相关命令详解

redis.call 与 redis.pcall 的区别

redis.call

  • 返回值就是 Redis 命令执行的返回值

  • 例如:local res = redis.call('GET', 'key'),res 将存储 GET 命令的返回值

  • 如果执行过程中出错,会立即返回错误信息,并且不再继续执行后续脚本代码

  • 示例:当尝试获取不存在的 key 时,redis.call('HGET', 'non_hash_key', 'field')会立即抛出错误

  • 适合在需要严格错误处理的场景中使用

redis.pcall

  • 返回值同样是 Redis 命令执行的返回值

  • 与 redis.call 的返回值处理方式相同

  • 如果执行过程中出错,会记录错误信息但不会中断脚本执行

  • 示例:local res = redis.pcall('HGET', 'non_hash_key', 'field')会返回错误对象,但脚本会继续执行

  • 错误信息可以通过检查返回值类型来判断(通常返回一个 Lua 表包含 err 字段)

  • 适合在需要容错处理、不希望因为单个命令失败而中断整个脚本的场景中使用

EVALSHA 命令详解

EVALSHA 的背景

  • 常规的 EVAL 命令要求每次执行时都要发送完整的脚本主体

  • 示例:EVAL "return redis.call('GET', KEYS[1])" 1 mykey

  • 这会在网络传输中造成不必要的带宽消耗,特别是对于频繁执行的复杂脚本

EVALSHA 的工作原理

  • 接收脚本的 SHA1 校验值作为第一个参数,而不是完整的脚本内容

  • 示例:EVALSHA a42059b356c875f0717db19a51f0aaca2ae5d991 1 mykey

  • Redis 服务器会维护一个脚本缓存(Script Cache),存储最近执行过的脚本

  • 当使用 EVALSHA 时,Redis 会:

  • 检查本地缓存中是否存在该 SHA1 对应的脚本

  • 如果存在,直接执行缓存的脚本

  • 如果不存在,返回错误(可使用 SCRIPT LOAD 预先加载)

使用建议

  1. 客户端可以先使用SCRIPT LOAD命令预加载脚本,获取 SHA1 值

  2. 示例:SCRIPT LOAD "return redis.call('GET', KEYS[1])"

  3. 对于可能未缓存的情况,客户端应实现 fallback 机制:

  4. 先尝试 EVALSHA

  5. 如果失败(返回 NOSCRIPT 错误),再使用 EVAL 并重新加载脚本

  6. 脚本缓存是易失性的,在 Redis 重启后会丢失,需要客户端重新加载

Script 命令

  • Script Flush 清除所有脚本缓存

  • Script Exists 根据给定的脚本校验和,检查指定脚本是否存在于缓存脚本中

  • Script Load 将一个脚本装入脚本缓存 返回 SHA1 摘要 但并不立即运行

  • Script Kill 杀死当前正在运行的脚本

脚本测试 1

编写一个脚本


vim /opt/wzk/test01.lua
复制代码


写入如下内容


return redis.call('set',KEYS[1],ARGV[1])
复制代码


保存后,执行 Shell 命令


./redis-cli --eval /opt/wzk/test01.lua name , kangkang
复制代码


脚本测试 2

编写脚本


vim /opt/wzk/test02.lua
复制代码


写入如下内容


local key=KEYS[1]local list=redis.call("lrange",key,0,-1);return list;
复制代码


保存后,执行 Shell 命令


./redis-cli --eval /opt/wzk/test02.lua list
复制代码


执行的结果如下图:


案例 1:原子计数器 - 详细解析与实现

功能说明

该 Lua 脚本实现了一个原子化的计数器功能,能够安全地对 Redis 中的键值进行数值递增操作,并返回更新后的值。主要解决多个客户端并发操作时可能出现的竞态条件问题。

脚本详细解析

-- 获取要操作的键名,从KEYS数组中取第一个元素local key = KEYS[1]
-- 获取要增加的数值,从ARGV数组中取第一个元素并转换为数字类型local increment = tonumber(ARGV[1])
-- 获取当前键值:-- 1. 尝试调用GET命令获取键的值-- 2. 如果键不存在则返回nil,此时使用or运算符返回默认值0-- 3. 将结果转换为数字类型local current = tonumber(redis.call('GET', key) or 0)
-- 计算新的值local new_value = current + increment
-- 使用SET命令将新值写入Redisredis.call('SET', key, new_value)
-- 返回更新后的新值return new_value
复制代码

典型应用场景

  1. 页面访问计数器

  2. 每次页面访问时执行脚本

  3. 示例调用:EVAL "脚本内容" 1 page:views 1

  4. 限流器实现

  5. 记录某操作在时间窗口内的调用次数

  6. 示例:限制每分钟最多 100 次 API 调用

  7. 库存扣减

  8. 商品库存的原子性扣减

  9. 示例:EVAL "脚本内容" 1 product:123:stock -1

  10. 用户积分系统

  11. 用户积分的增减操作

  12. 示例:EVAL "脚本内容" 1 user:456:points 10

使用注意事项

  1. 键不存在时会自动初始化为 0

  2. 确保传入的 ARGV[1]是可以转换为数字的字符串

  3. 对于大数值操作,注意 Redis 的数值范围限制

  4. 在集群环境下,确保操作的 key 都在同一个 slot 上

案例 2:检查并设置值(CAS 操作实现)

这个 Lua 脚本实现了一个原子性的"检查并设置"(Compare-And-Swap,CAS)操作,常用于 Redis 中的乐观锁机制。以下是详细说明:

功能描述

脚本会检查指定键的当前值是否等于给定的旧值(old_value),如果相等则将其更新为新值(new_value),整个过程是原子性的。

参数说明

  • KEYS[1]: 需要操作的 Redis 键名

  • ARGV[1]: 期望的旧值(old_value),用于比较

  • ARGV[2]: 要设置的新值(new_value)

执行流程

  1. 首先获取键名KEYS[1]的当前值

  2. 将当前值与参数ARGV[1]进行比较

  3. 如果两者相等:

  4. 使用SET命令将键的值更新为ARGV[2]

  5. 返回 1 表示设置成功

  6. 如果不相等:

  7. 不执行任何修改

  8. 返回 0 表示设置失败

返回值

  • 1:表示检查通过并成功设置了新值

  • 0:表示当前值与期望的旧值不匹配,未执行设置操作

应用场景

  1. 乐观锁控制:在多客户端并发修改时确保数据一致性


   -- 示例:只有当计数器当前值为5时才更新为10   EVAL "..." 1 my_counter 5 10
复制代码


  1. 配置更新:确保只在特定状态下更新配置


   -- 示例:只有当服务状态为"standby"时才切换为"active"   EVAL "..." 1 service_status standby active
复制代码


  1. 资源分配:确保资源未被其他客户端占用


   -- 示例:只有当任务状态为"pending"时才标记为"processing"   EVAL "..." 1 task:123 pending processing
复制代码

注意事项

  1. 该操作是原子性的,在并发环境下非常安全

  2. 如果键不存在,GET会返回 nil,此时与任何值比较都会失败

  3. 对于复杂数据类型(如 Hash,List),需要相应调整比较逻辑

案例 3:列表的批量插入

local key = KEYS[1]local elements = {}
for i = 1, #ARGV do table.insert(elements, ARGV[i])end
redis.call('LPUSH', key, unpack(elements))return redis.call('LRANGE', key, 0, -1)
复制代码

案例 3:获取并删除键值对

local key = KEYS[1]local value = redis.call('GET', key)
if value then redis.call('DEL', key)end
return value
复制代码

案例 4:哈希表字段的批量设置

local key = KEYS[1]
for i = 1, #ARGV, 2 do redis.call('HSET', key, ARGV[i], ARGV[i+1])end
return redis.call('HGETALL', key)
复制代码


发布于: 刚刚阅读数: 5
用户头像

武子康

关注

永远好奇 无限进步 2019-04-14 加入

Hi, I'm Zikang,好奇心驱动的探索者 | INTJ / INFJ 我热爱探索一切值得深究的事物。对技术、成长、效率、认知、人生有着持续的好奇心和行动力。 坚信「飞轮效应」,相信每一次微小的积累,终将带来深远的改变。

评论

发布
暂无评论
大数据-43 Redis Lua脚本实战全解析 eval redis.call redis.pcall_Java_武子康_InfoQ写作社区