Redis 的 Lua 脚本功能 是 Redis 提供的一种在服务器端执行自定义逻辑的能力。通过 Lua 脚本,开发者可以将多个 Redis 命令组合成一个原子操作,确保操作的原子性和一致性,同时减少客户端与服务器之间的网络交互次数,提升性能。
一、Lua 脚本的核心作用
- 原子性操作
Redis 是单线程的,Lua 脚本在执行期间不会被其他命令打断,所有命令要么全部执行成功,要么全部失败(除非脚本主动抛出错误)。- 典型场景:防止超卖、实现分布式锁、原子性计数器。
- 减少网络开销
将多个 Redis 命令封装到一个脚本中,客户端只需一次网络请求即可完成批量操作,降低 RTT(往返时间)和带宽消耗。 - 复杂逻辑处理
在 Redis 服务器端直接执行复杂的业务逻辑(如条件判断、循环),减轻客户端负担,避免频繁的客户端-服务器交互。 - 避免并发竞争
通过原子性操作,解决多个客户端并发修改同一数据时的竞态条件问题。
二、Lua 脚本的使用方式
1. 核心命令
EVAL
直接执行 Lua 脚本,语法格式:EVAL <script> <numkeys> [key1] [key2] ... [keyN] [arg1] [arg2] ...
<script>
:Lua 脚本字符串。<numkeys>
:脚本中使用的键(Key)数量。[key1]...[keyN]
:键列表(通过KEYS[1]
,KEYS[2]
等访问)。[arg1]...[argN]
:参数列表(通过ARGV[1]
,ARGV[2]
等访问)。
SCRIPT LOAD
+EVALSHA
SCRIPT LOAD
:将脚本加载到 Redis 服务器并返回其 SHA1 摘要。SCRIPT LOAD <script>
EVALSHA
:通过脚本的 SHA1 摘要执行已加载的脚本,避免重复传输脚本内容。EVALSHA <sha1> <numkeys> [key1] ... [arg1] ...
2. 参数传递规则
KEYS[]
数组:用于传递 Redis 键(Key),例如KEYS[1]
对应第一个键。ARGV[]
数组:用于传递其他参数(非键值),例如ARGV[1]
对应第一个参数。- 下标从 1 开始:Lua 脚本中数组索引从 1 开始,而非 0。
三、Lua 脚本的核心 API
redis.call(command, keys..., args...)
- 执行 Redis 原生命令,若命令执行失败会抛出错误。
- 示例:
redis.call('SET', KEYS[1], ARGV[1])
redis.pcall(command, keys..., args...)
- 与
redis.call
类似,但会捕获错误并以 Lua 表的形式返回错误信息,避免脚本中断。 - 示例:
local result = redis.pcall('GET', KEYS[1]) if type(result) == 'table' and result.err then -- 处理错误 end
- 与
四、典型使用场景
1. 原子性计数器
-- 将 key 的值增加指定的步长(step)
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
else
current = tonumber(current)
end
current = current + tonumber(ARGV[1])
redis.call('SET', KEYS[1], current)
return current
执行命令:
EVAL "..." 1 mykey 10
2. 分布式锁
-- 获取锁(NX 表示键不存在时才设置,PX 设置过期时间)
local lock_key = KEYS[1]
local lock_value = ARGV[1] -- 客户端唯一标识
local ttl = tonumber(ARGV[2]) -- 过期时间(毫秒)
local result = redis.call('SET', lock_key, lock_value, 'NX', 'PX', ttl)
if result == false then
return 0 -- 获取锁失败
else
return 1 -- 获取锁成功
end
执行命令:
EVAL "..." 1 lock:key client_id 3000
3. 防止超卖
-- 检查库存并减少指定数量
local product_id = KEYS[1]
local quantity = tonumber(ARGV[1])
local stock_key = "stock:" .. product_id
local current_stock = redis.call('GET', stock_key)
if current_stock == false then
return -1 -- 商品不存在
end
current_stock = tonumber(current_stock)
if current_stock >= quantity then
redis.call('DECRBY', stock_key, quantity)
return current_stock - quantity
else
return -2 -- 库存不足
end
执行命令:
EVAL "..." 1 product:1001 2
五、注意事项
- 避免死循环
Lua 脚本执行时间不能过长(默认超时时间lua-time-limit
为 5 秒),否则可能阻塞 Redis 主线程。- 解决方案:使用
SCRIPT KILL
终止长时间运行的脚本(仅限非写操作脚本)。
- 解决方案:使用
- 错误处理
- 使用
redis.pcall
捕获错误,避免脚本因异常中断。
- 使用
- 性能优化
- 预加载脚本:使用
SCRIPT LOAD
和EVALSHA
减少脚本传输开销。 - 避免大 Key 操作:脚本中操作大 Value(如 10MB 的 Hash)可能导致性能下降,需拆分处理。
- 预加载脚本:使用
- 集群限制
- Key 必须位于同一 Slot:在 Redis Cluster 中,脚本中涉及的所有 Key 必须属于同一个 Slot(通过
HASH TAG
保证)。
- Key 必须位于同一 Slot:在 Redis Cluster 中,脚本中涉及的所有 Key 必须属于同一个 Slot(通过
六、代码示例(Python/Jedis)
Python(redis-py)
import redis
r = redis.Redis(host='localhost', port=6379, db=0)
script = """
local current = redis.call('GET', KEYS[1])
if current == false then
current = 0
else
current = tonumber(current)
end
current = current + tonumber(ARGV[1])
redis.call('SET', KEYS[1], current)
return current
"""
result = r.eval(script, 1, 'mykey', 10)
print(result) # 输出: 10
Java(Jedis)
import redis.clients.jedis.Jedis;
import java.util.Collections;
public class LuaExample {
public static void main(String[] args) {
Jedis jedis = new Jedis("localhost", 6379);
String script = "local current = redis.call('GET', KEYS[1]) " +
"if current == false then current = 0 else current = tonumber(current) end " +
"current = current + tonumber(ARGV[1]) " +
"redis.call('SET', KEYS[1], current) " +
"return current";
Object result = jedis.eval(script, Collections.singletonList("mykey"), Collections.singletonList("10"));
System.out.println(result); // 输出: 10
jedis.close();
}
}
七、总结
特性 | 说明 |
---|---|
原子性 | 保证脚本内操作的原子性,避免并发竞争。 |
性能优化 | 减少网络交互,提升批量操作效率。 |
适用场景 | 分布式锁、库存扣减、限流、复杂业务逻辑封装等。 |
注意事项 | 避免长时间运行、合理拆分大 Key、处理错误、集群环境 Key Slot 一致性。 |
通过合理使用 Redis 的 Lua 脚本,可以显著提升系统的性能和可靠性,尤其在高并发场景下具有重要价值。
THE END