0%

lua脚本

Redis Lua 脚本详解:原子性操作与高效执行

Redis 对 Lua 脚本的支持是其高级特性之一,通过将多个命令封装为脚本,可实现原子性执行减少网络往返开销,尤其适合复杂业务逻辑(如分布式锁、计数器累加等)。本文基于 Redis 6.0.10 版本,详解 Lua 脚本的使用方法、核心命令及最佳实践。

Lua 脚本在 Redis 中的价值

  1. 原子性保证:Lua 脚本在 Redis 中以单线程方式执行,执行期间不会被其他命令打断,确保多个命令的原子性(类似事务,但更灵活)。
  2. 减少网络开销:将多个命令合并为一个脚本,只需一次网络请求,降低延迟(尤其适用于跨机房部署)。
  3. 复用逻辑:脚本可被缓存,通过摘要(SHA1)重复调用,避免重复传输脚本内容。

Lua 基础:数据类型与语法

Redis 内嵌 Lua 解释器,支持 Lua 5.1 标准语法,核心数据类型如下:

类型 说明 示例
nil 空值(未赋值的变量默认为此类型)。 local aanil
字符串 单引号或双引号包裹,支持换行([[ 多行文本 ]])。 'hello'"redis"
数字 整数或浮点数(Lua 不区分 int 和 float)。 423.14
布尔值 truefalse(注意小写)。 local flag = true
表(table) 唯一复合类型,可表示数组、字典或对象(索引从 1 开始)。 {1, 2, 3}{name = "lua"}
函数 支持自定义函数,可作为参数或返回值。 local f = function(x) return x+1 end

核心命令:evalevalsha

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
2
3
4
127.0.0.1:6379> eval "return {KEYS[1], KEYS[2], ARGV[1]}" 2 user:100 post:200 "hello"
1) "user:100"
2) "post:200"
3) "hello"

示例 2:调用 Redis 命令
通过 redis.call(command, ...) 在脚本中执行 Redis 命令:

1
2
3
# 脚本功能:设置键值对,并返回设置后的值
127.0.0.1:6379> eval "redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])" 1 name "redis"
"redis"
  • redis.call 执行命令,若出错会返回错误信息(如键不存在)。
  • 替代函数 redis.pcall:出错时返回错误对象,不中断脚本。

evalsha:通过脚本摘要执行

对于长脚本,每次传输完整内容会浪费带宽。evalsha 通过脚本的 SHA1 摘要调用缓存中的脚本,优化性能。

步骤

  1. 计算脚本 SHA1 摘要:通过 script load 命令将脚本存入 Redis 缓存,返回摘要。
  2. 通过摘要执行:使用 evalsha 传入摘要,替代完整脚本。

示例

1
2
3
4
5
6
7
# 1. 加载脚本并获取摘要
127.0.0.1:6379> script load "redis.call('set', KEYS[1], ARGV[1]); return redis.call('get', KEYS[1])"
"a42059b356c875f0779a9210f000000000000000"

# 2. 通过摘要执行(参数与 eval 一致)
127.0.0.1:6379> evalsha a42059b356c875f0779a9210f000000000000000 1 age 25
"25"
  • 若摘要不存在,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
2
127.0.0.1:6379> script exists a42059b356c875f0779a9210f000000000000000
1) (integer) 1 # 存在

最佳实践与注意事项

1. 脚本原子性与性能

  • 避免长脚本:脚本执行时间应控制在毫秒级(超过 lua-time-limit 会被终止,默认 5000 毫秒)。
  • 禁止阻塞操作:如 sleep 或复杂计算,会阻塞 Redis 单线程。

2. 键与参数的使用规范

  • 键名通过 KEYS 传递:Redis 集群模式下,脚本中的键必须通过 KEYS 传入,否则无法路由到正确节点。
  • 参数通过 ARGV 传递:非键值参数(如数值、字符串)应放在 ARGV 中,与键名区分。

3. 避免非确定性脚本

脚本执行结果必须唯一(相同输入产生相同输出),禁止使用:

  • 随机命令(如 RANDOMKEYSRANDMEMBER)。
  • 时间命令(如 TIMENOW)—— 若需时间,可通过 ARGV 传入客户端时间。

4. 分布式锁示例(经典应用)

通过 Lua 脚本实现分布式锁的获取与释放(原子性保证):

1
2
3
4
5
6
7
8
9
-- 获取锁:键存在则返回 0,否则设置键并返回 1(过期时间避免死锁)
local lockKey = KEYS[1]
local uuid = ARGV[1]
local ttl = ARGV[2]
if redis.call('exists', lockKey) == 0 then
redis.call('set', lockKey, uuid, 'PX', ttl)
return 1
end
return 0
1
2
3
4
5
6
7
8
-- 释放锁:仅当键存在且值匹配时删除(防止误删其他客户端的锁)
local lockKey = KEYS[1]
local uuid = ARGV[1]
if redis.call('get', lockKey) == uuid then
redis.call('del', lockKey)
return 1
end
return 0

欢迎关注我的其它发布渠道