Redis 执行 Lua 脚本基本用法

Redis 不仅是一个高性能的键值存储系统,还支持通过 Lua 脚本执行复杂的原子操作。通过在 Redis 中执行 Lua 脚本,我们可以将多个命令打包成一个原子操作,避免竞态条件,同时减少网络往返次数。本文将详细介绍 Redis 中执行 Lua 脚本的基本用法。

为什么使用 Lua 脚本?

在分布式系统中,经常需要将多个 Redis 命令组合执行,但在高并发环境下,这些操作可能会引发竞态条件。传统的解决方案如 Redis 事务(MULTI/EXEC)虽然能保证原子性,但缺乏流程控制能力,而 Lua 脚本正好弥补了这一缺陷。

使用 Lua 脚本的主要优势包括:

  1. 原子性保证:整个脚本在 Redis 中以原子方式执行,执行过程中不会被其他命令打断
  2. 减少网络开销:多个操作只需一次网络往返
  3. 灵活的逻辑控制:支持条件判断、循环等复杂逻辑
  4. 高性能:脚本在服务器端预编译,执行效率高

EVAL 命令基础用法

Redis 提供了 EVAL 命令来执行 Lua 脚本,其语法如下:

1
EVAL script numkeys key [key ...] arg [arg ...]

各参数含义:

  • script:要执行的 Lua 脚本代码
  • numkeys:指定键名参数的数量
  • key [key ...]:脚本中用到的 Redis 键名,通过KEYS数组在 Lua 中访问
  • arg [arg ...]:附加参数,通过ARGV数组在 Lua 中访问

简单示例

让我们从一个简单的例子开始:

1
EVAL "return 'Hello, World!'" 0

这个脚本不涉及任何 Redis 键,直接返回一个字符串。注意到 numkeys 参数为 0,因为我们没有使用任何键。

使用键和参数

在脚本中访问 Redis 键和参数:

1
EVAL "return {KEYS[1], KEYS[2], ARGV[1], ARGV[2]}" 2 key1 key2 first second

输出结果:

1
2
3
4
1) "key1"
2) "key2"
3) "first"
4) "second"

在 Lua 脚本中,可以通过KEYSARGV两个全局数组分别访问键名和参数。

Redis 命令调用

在 Lua 脚本中,可以通过redis.call()redis.pcall()函数调用 Redis 命令:

1
EVAL "return redis.call('SET', KEYS[1], ARGV[1])" 1 mykey "Hello Redis"

这个脚本相当于执行了SET mykey "Hello Redis"命令。

另一个示例,获取键值:

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

EVALSHA 命令优化

当脚本较长或需要频繁执行时,每次都通过 EVAL 命令传输整个脚本会浪费网络带宽。Redis 提供了 EVALSHA 命令,通过脚本的 SHA1 摘要来执行脚本。

首先使用SCRIPT LOAD命令加载脚本:

1
SCRIPT LOAD "return redis.call('SET', KEYS[1], ARGV[1])"

该命令返回脚本的 SHA1 摘要,例如:

1
"a6b4d2b0a70c7c7a8a8a8a8a8a8a8a8a8a8a8a8"

然后使用 EVALSHA 执行脚本:

1
EVALSHA a6b4d2b0a70c7c7a8a8a8a8a8a8a8a8a8a8a8a8 1 mykey "Hello Redis"

EVALSHA 的优点:

  1. 减少网络传输量
  2. 提升执行性能(脚本已在服务器端缓存)

实际应用场景

原子计数器操作

实现一个带有上限检查的计数器:

1
2
3
4
5
6
7
8
9
10
11
local current = redis.call('GET', KEYS[1])
if not current then
current = 0
end

if tonumber(current) < tonumber(ARGV[1]) then
local new_value = redis.call('INCR', KEYS[1])
return {1, new_value} -- 成功,返回状态和新值
else
return {0, current} -- 达到上限,返回状态和当前值
end

执行脚本:

1
EVAL "-- Lua script here --" 1 counter 10

分布式锁

实现一个简单的分布式锁:

1
2
3
4
5
6
7
-- 获取锁
if redis.call('SETNX', KEYS[1], ARGV[1]) == 1 then
redis.call('EXPIRE', KEYS[1], ARGV[2])
return 1
else
return 0
end

执行脚本:

1
EVAL "-- Lua script here --" 1 my_lock unique_value 30

释放锁时也要确保只能由加锁的客户端释放:

1
2
3
4
5
6
-- 释放锁
if redis.call('GET', KEYS[1]) == ARGV[1] then
return redis.call('DEL', KEYS[1])
else
return 0
end

限流器实现

实现一个基于时间窗口的限流器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])
local current_time = tonumber(ARGV[3])

-- 移除时间窗口外的记录
redis.call('ZREMRANGEBYSCORE', key, 0, current_time - window)

-- 检查当前请求数
local current_requests = redis.call('ZCARD', key)

if current_requests < limit then
-- 添加当前请求
redis.call('ZADD', key, current_time, current_time)
-- 设置过期时间
redis.call('EXPIRE', key, window)
return 1 -- 允许请求
else
return 0 -- 拒绝请求
end

错误处理

在 Lua 脚本中,有两种调用 Redis 命令的方式:

  1. redis.call():执行命令,出错时抛出异常并停止脚本执行
  2. redis.pcall():执行命令,出错时返回描述错误的表

使用redis.pcall()的安全示例:

1
2
3
4
5
6
7
8
local result = redis.pcall('GET', KEYS[1])
if type(result) == 'table' and result.err then
-- 处理错误
return 'Error: ' .. result.err
else
-- 处理正常结果
return result
end

最佳实践

1. 保持脚本简洁

Lua 脚本应在 Redis 主线程中执行,长时间运行会阻塞其他命令。应避免在脚本中进行复杂计算或大循环操作。

2. 合理使用键

在集群模式下,确保所有键都在同一个 hash slot 中,可以使用 hash tags:

1
EVAL "return redis.call('GET', KEYS[1])" 1 {user1000}.balance

3. 使用 EVALSHA 优化性能

对于频繁执行的脚本,使用SCRIPT LOAD加载后通过 EVALSHA 执行。

4. 脚本可读性和维护性

为复杂脚本添加注释,合理命名变量,避免过于复杂的逻辑。

总结

Redis 的 Lua 脚本功能为我们提供了一种强大而灵活的方式来执行原子操作。通过 EVALEVALSHA 命令,我们可以在 Redis 中执行复杂的逻辑,同时保证操作的原子性和数据一致性。

合理使用 Lua 脚本可以极大地提升 Redis 应用的性能和可靠性,特别是在需要处理并发和复杂业务逻辑的场景中。但也要注意避免编写过于复杂的脚本,以免影响 Redis 的整体性能。

参考资料

  1. Redis 官方 Lua 脚本文档
  2. Redis 命令参考
  3. Lua 5.1 参考手册
  4. Redis 脚本命令详解

Redis 执行 Lua 脚本基本用法
https://bubao.github.io/posts/fc1040f0.html
作者
一念
发布于
2025年12月6日
许可协议