点一下关注吧!!!非常感谢!!持续更新!!!
🚀 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 接口
典型应用场景包括:
-
游戏开发 Lua 因其轻量级和高性能的特点,被广泛用于游戏开发中作为脚本引擎。例如:
- 《魔兽世界》使用 Lua 编写插件和 UI 定制
- 《愤怒的小鸟》系列游戏使用 Lua 实现游戏逻辑
- 知名游戏引擎如 Cocos2d-x、Unity 等都支持 Lua 脚本
-
独立应用脚本
- Adobe Lightroom 使用 Lua 实现插件系统
- Wireshark 网络分析工具使用 Lua 编写协议解析器
- VLC 媒体播放器通过 Lua 扩展功能
-
Web 应用脚本
- 作为 Nginx 的脚本扩展(OpenResty 项目)
- 用于实现 Web 应用的业务逻辑(如 Lapis 框架)
- 轻量级 API 网关和服务编排
-
数据库插件
- 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 ...]
参数详细说明
-
script
一段Lua 5.1脚本程序,这段脚本会在Redis服务器的上下文中执行。脚本可以包含任意有效的Lua代码,并能够调用Redis命令。例如:return redis.call('GET', KEYS[1]) -
numkeys
指定后续参数中有多少个是键名(key)。这个数值必须是非负整数,用于帮助Redis区分键名参数和普通参数。 -
key
从命令的第三个参数开始,numkeys个参数会被视为键名。这些键名可以在Lua脚本中通过全局变量KEYS数组访问,索引从1开始(KEYS[1], KEYS[2]等)。 -
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"
实际应用场景
-
原子性操作
实现一个原子性的get-and-set操作:eval "local val = redis.call('GET', KEYS[1]); redis.call('SET', KEYS[1], ARGV[1]); return val" 1 mykey newvalue -
条件更新
只有在旧值匹配时才更新: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 -
复杂计算
使用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的和)
注意事项
- 脚本中的Redis命令调用必须使用
redis.call()或redis.pcall()函数 - Lua数组索引从1开始,而不是0
- 在集群模式下,所有键必须在同一个哈希槽中
- 脚本执行是原子性的,执行期间不会执行其他命令
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')会立即抛出错误
- 示例:当尝试获取不存在的key时,
- 适合在需要严格错误处理的场景中使用
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预先加载)
使用建议
- 客户端可以先使用
SCRIPT LOAD命令预加载脚本,获取SHA1值- 示例:
SCRIPT LOAD "return redis.call('GET', KEYS[1])"
- 示例:
- 对于可能未缓存的情况,客户端应实现fallback机制:
- 先尝试EVALSHA
- 如果失败(返回NOSCRIPT错误),再使用EVAL并重新加载脚本
- 脚本缓存是易失性的,在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
典型应用场景
-
页面访问计数器:
- 每次页面访问时执行脚本
- 示例调用:
EVAL "脚本内容" 1 page:views 1
-
限流器实现:
- 记录某操作在时间窗口内的调用次数
- 示例:限制每分钟最多100次API调用
-
库存扣减:
- 商品库存的原子性扣减
- 示例:
EVAL "脚本内容" 1 product:123:stock -1
-
用户积分系统:
- 用户积分的增减操作
- 示例:
EVAL "脚本内容" 1 user:456:points 10
使用注意事项
- 键不存在时会自动初始化为0
- 确保传入的ARGV[1]是可以转换为数字的字符串
- 对于大数值操作,注意Redis的数值范围限制
- 在集群环境下,确保操作的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)
执行流程
- 首先获取键名
KEYS[1]的当前值 - 将当前值与参数
ARGV[1]进行比较 - 如果两者相等:
- 使用
SET命令将键的值更新为ARGV[2] - 返回1表示设置成功
- 使用
- 如果不相等:
- 不执行任何修改
- 返回0表示设置失败
返回值
- 1:表示检查通过并成功设置了新值
- 0:表示当前值与期望的旧值不匹配,未执行设置操作
应用场景
-
乐观锁控制:在多客户端并发修改时确保数据一致性
-- 示例:只有当计数器当前值为5时才更新为10 EVAL "..." 1 my_counter 5 10 -
配置更新:确保只在特定状态下更新配置
-- 示例:只有当服务状态为"standby"时才切换为"active" EVAL "..." 1 service_status standby active -
资源分配:确保资源未被其他客户端占用
-- 示例:只有当任务状态为"pending"时才标记为"processing" EVAL "..." 1 task:123 pending processing
注意事项
- 该操作是原子性的,在并发环境下非常安全
- 如果键不存在,
GET会返回nil,此时与任何值比较都会失败 - 对于复杂数据类型(如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)