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

92 阅读11分钟

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

🚀 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 因其轻量级和高性能的特点,被广泛用于游戏开发中作为脚本引擎。例如:

    • 《魔兽世界》使用 Lua 编写插件和 UI 定制
    • 《愤怒的小鸟》系列游戏使用 Lua 实现游戏逻辑
    • 知名游戏引擎如 Cocos2d-x、Unity 等都支持 Lua 脚本
  2. 独立应用脚本

    • Adobe Lightroom 使用 Lua 实现插件系统
    • Wireshark 网络分析工具使用 Lua 编写协议解析器
    • VLC 媒体播放器通过 Lua 扩展功能
  3. Web 应用脚本

    • 作为 Nginx 的脚本扩展(OpenResty 项目)
    • 用于实现 Web 应用的业务逻辑(如 Lapis 框架)
    • 轻量级 API 网关和服务编排
  4. 数据库插件

    • Redis 支持 Lua 脚本实现复杂原子操作
    • PostgreSQL 可通过 PL/Lua 编写存储过程
    • 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
    一段Lua 5.1脚本程序,这段脚本会在Redis服务器的上下文中执行。脚本可以包含任意有效的Lua代码,并能够调用Redis命令。例如:

    return redis.call('GET', KEYS[1])
    
  2. numkeys
    指定后续参数中有多少个是键名(key)。这个数值必须是非负整数,用于帮助Redis区分键名参数和普通参数。

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

  4. arg
    在键名参数之后的参数会被视为普通参数。这些参数可以在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. 原子性操作
    实现一个原子性的get-and-set操作:

    eval "local val = redis.call('GET', KEYS[1]); redis.call('SET', KEYS[1], ARGV[1]); return val" 1 mykey newvalue
    
  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
    
  3. 复杂计算
    使用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会:
    1. 检查本地缓存中是否存在该SHA1对应的脚本
    2. 如果存在,直接执行缓存的脚本
    3. 如果不存在,返回错误(可使用SCRIPT LOAD预先加载)

使用建议

  1. 客户端可以先使用SCRIPT LOAD命令预加载脚本,获取SHA1值
    • 示例:SCRIPT LOAD "return redis.call('GET', KEYS[1])"
  2. 对于可能未缓存的情况,客户端应实现fallback机制:
    • 先尝试EVALSHA
    • 如果失败(返回NOSCRIPT错误),再使用EVAL并重新加载脚本
  3. 脚本缓存是易失性的,在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命令将新值写入Redis
redis.call('SET', key, new_value)

-- 返回更新后的新值
return new_value

典型应用场景

  1. 页面访问计数器

    • 每次页面访问时执行脚本
    • 示例调用:EVAL "脚本内容" 1 page:views 1
  2. 限流器实现

    • 记录某操作在时间窗口内的调用次数
    • 示例:限制每分钟最多100次API调用
  3. 库存扣减

    • 商品库存的原子性扣减
    • 示例:EVAL "脚本内容" 1 product:123:stock -1
  4. 用户积分系统

    • 用户积分的增减操作
    • 示例: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. 如果两者相等:
    • 使用SET命令将键的值更新为ARGV[2]
    • 返回1表示设置成功
  4. 如果不相等:
    • 不执行任何修改
    • 返回0表示设置失败

返回值

  • 1:表示检查通过并成功设置了新值
  • 0:表示当前值与期望的旧值不匹配,未执行设置操作

应用场景

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

    -- 示例:只有当计数器当前值为5时才更新为10
    EVAL "..." 1 my_counter 5 10
    
  2. 配置更新:确保只在特定状态下更新配置

    -- 示例:只有当服务状态为"standby"时才切换为"active"
    EVAL "..." 1 service_status standby active
    
  3. 资源分配:确保资源未被其他客户端占用

    -- 示例:只有当任务状态为"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)