Redis Lua 脚本详解:原子性操作与高效执行
Redis 对 Lua 脚本的支持是其高级特性之一,通过将多个命令封装为脚本,可实现原子性执行并减少网络往返开销,尤其适合复杂业务逻辑(如分布式锁、计数器累加等)。本文基于 Redis 6.0.10 版本,详解 Lua 脚本的使用方法、核心命令及最佳实践。
Lua 脚本在 Redis 中的价值
- 原子性保证:Lua 脚本在 Redis 中以单线程方式执行,执行期间不会被其他命令打断,确保多个命令的原子性(类似事务,但更灵活)。
- 减少网络开销:将多个命令合并为一个脚本,只需一次网络请求,降低延迟(尤其适用于跨机房部署)。
- 复用逻辑:脚本可被缓存,通过摘要(SHA1)重复调用,避免重复传输脚本内容。
Lua 基础:数据类型与语法
Redis 内嵌 Lua 解释器,支持 Lua 5.1 标准语法,核心数据类型如下:
| 类型 | 说明 | 示例 |
|---|---|---|
nil |
空值(未赋值的变量默认为此类型)。 | local a(a 为 nil) |
| 字符串 | 单引号或双引号包裹,支持换行([[ 多行文本 ]])。 |
'hello'、"redis" |
| 数字 | 整数或浮点数(Lua 不区分 int 和 float)。 | 42、3.14 |
| 布尔值 | true 或 false(注意小写)。 |
local flag = true |
| 表(table) | 唯一复合类型,可表示数组、字典或对象(索引从 1 开始)。 | {1, 2, 3} 或 {name = "lua"} |
| 函数 | 支持自定义函数,可作为参数或返回值。 | local f = function(x) return x+1 end |
核心命令:eval 与 evalsha
eval:直接执行脚本
语法:
1 | eval script numkeys key1 [key2 ...] arg1 [arg2 ...] |
script:Lua 脚本字符串。numkeys:后续key参数的数量(必须为整数)。key1...:Redis 键名,在脚本中通过KEYS[1]、KEYS[2]访问(索引从 1 开始)。arg1...:附加参数,在脚本中通过ARGV[1]、ARGV[2]访问。
示例 1:简单脚本
返回键名和参数:
1 | 127.0.0.1:6379> eval "return {KEYS[1], KEYS[2], ARGV[1]}" 2 user:100 post:200 "hello" |
示例 2:调用 Redis 命令
通过 redis.call(command, ...) 在脚本中执行 Redis 命令:
1 | 脚本功能:设置键值对,并返回设置后的值 |
redis.call执行命令,若出错会返回错误信息(如键不存在)。- 替代函数
redis.pcall:出错时返回错误对象,不中断脚本。
evalsha:通过脚本摘要执行
对于长脚本,每次传输完整内容会浪费带宽。evalsha 通过脚本的 SHA1 摘要调用缓存中的脚本,优化性能。
步骤:
- 计算脚本 SHA1 摘要:通过
script load命令将脚本存入 Redis 缓存,返回摘要。 - 通过摘要执行:使用
evalsha传入摘要,替代完整脚本。
示例:
1 | 1. 加载脚本并获取摘要 |
- 若摘要不存在,
evalsha返回NOSCRIPT错误,需改用eval重新执行。
脚本管理命令
| 命令 | 作用 | 示例 |
|---|---|---|
script load |
将脚本存入缓存,返回 SHA1 摘要。 | script load "return 1" |
script exists |
检查多个摘要是否存在于缓存中(1 存在,0 不存在)。 | script exists sha1 sha2 |
script flush |
清空脚本缓存(所有脚本被移除)。 | script flush |
script kill |
终止当前正在执行的慢脚本(仅适用于未执行写操作的脚本)。 | script kill |
示例:检查脚本是否存在
1 | 127.0.0.1:6379> script exists a42059b356c875f0779a9210f000000000000000 |
最佳实践与注意事项
1. 脚本原子性与性能
- 避免长脚本:脚本执行时间应控制在毫秒级(超过
lua-time-limit会被终止,默认 5000 毫秒)。 - 禁止阻塞操作:如
sleep或复杂计算,会阻塞 Redis 单线程。
2. 键与参数的使用规范
- 键名通过
KEYS传递:Redis 集群模式下,脚本中的键必须通过KEYS传入,否则无法路由到正确节点。 - 参数通过
ARGV传递:非键值参数(如数值、字符串)应放在ARGV中,与键名区分。
3. 避免非确定性脚本
脚本执行结果必须唯一(相同输入产生相同输出),禁止使用:
- 随机命令(如
RANDOMKEY、SRANDMEMBER)。 - 时间命令(如
TIME、NOW)—— 若需时间,可通过ARGV传入客户端时间。
4. 分布式锁示例(经典应用)
通过 Lua 脚本实现分布式锁的获取与释放(原子性保证):
1 | -- 获取锁:键存在则返回 0,否则设置键并返回 1(过期时间避免死锁) |
1 | -- 释放锁:仅当键存在且值匹配时删除(防止误删其他客户端的锁) |