Redis 中 Lua 脚本的执行机制
1. Lua 脚本是如何执行的?
Redis 使用 EVAL 和 EVALSHA 命令执行 Lua 脚本:
-- EVAL 命令格式
EVAL "return {KEYS[1],KEYS[2],ARGV[1],ARGV[2]}" 2 key1 key2 arg1 arg2
-- EVALSHA 使用脚本的 SHA1 校验和执行
SCRIPT LOAD "return 'hello'"
EVALSHA <sha1_hash> 0
执行特点:
- 原子性:脚本作为一个整体执行,期间不会被其他命令打断
- 单线程:Redis 单线程执行,避免并发问题
- 事务性:优于 Redis 事务(MULTI/EXEC),没有事务失败问题
2. Lua 脚本以什么形式存在于 Redis 中?
脚本在 Redis 中以两种形式存在:
-- 1. 临时形式:EVAL 直接执行(不持久化)
EVAL "return redis.call('GET', 'foo')" 0
-- 2. 缓存形式:SCRIPT LOAD 后缓存在内存中
SCRIPT LOAD "return redis.call('GET', 'foo')"
-- 返回 SHA1: 6b1bf486c81ceb7edf3c093f4c48582e38c0e791
-- 脚本会缓存在服务器的脚本缓存字典中
-- 查看缓存中的所有脚本
SCRIPT EXISTS <sha1> <sha2>...
-- 清空脚本缓存
SCRIPT FLUSH
存储特点:
- 脚本缓存是 volatile 的,重启后消失
- 使用 LRU 机制管理缓存,默认缓存 10000 个脚本
- 可通过
redis.config调整lua-time-limit(默认 5 秒超时)
3. Lua 脚本的运行环境来源
Redis 为每个 Lua 脚本创建独立的 Lua 环境:
-- Redis 初始化时创建基础 Lua 环境
-- 每个脚本执行时,Redis 会:
-- 1. 创建新的 Lua 状态机(隔离环境)
-- 2. 加载基础库(math, string, table 等)
-- 3. 注入 Redis 专用函数(redis.call, redis.pcall)
-- 4. 设置全局保护(防止修改全局变量)
环境隔离机制:
- 每次 EVAL 创建新环境:脚本间不共享变量
- SCRIPT KILL 可终止:超时脚本(只读脚本除外)
- 沙箱环境:限制危险操作(如文件访问)
4. Lua 脚本的作用域判定
-- 1. KEYS 和 ARGV 是特殊参数
EVAL "return {KEYS[1], ARGV[1]}" 1 mykey myarg
-- 2. 局部变量(仅在脚本内有效)
EVAL [[
local value = redis.call('GET', KEYS[1])
return tonumber(value) * 2
]] 1 counter
-- 3. Redis 键的作用域
-- KEYS 数组中的键会被自动标记为"已读"或"已写"
-- 集群模式下必须显式声明所有操作的键
作用域规则:
- KEYS 必须声明:集群模式下用于路由到正确节点
- ARGV 是普通参数:不参与键的路由
- 变量作用域:
- 局部变量:
local var(推荐) - 全局变量:避免使用(可能被 Redis 重置)
- 局部变量:
5. Lua 脚本能使用的 Redis 函数
核心函数:
-- 1. redis.call() - 执行 Redis 命令,错误时抛出异常
redis.call('SET', 'key', 'value')
local value = redis.call('GET', 'key')
-- 2. redis.pcall() - 执行 Redis 命令,错误时返回错误表
local ok, err = redis.pcall('INCRBY', 'not_a_number', 1)
-- 3. redis.log() - 记录日志
redis.log(redis.LOG_WARNING, "Script warning message")
-- 4. redis.sha1hex() - 计算 SHA1
local sha = redis.sha1hex("hello")
支持的 Redis 命令类型:
-- 字符串操作
redis.call('SET', key, value)
redis.call('GET', key)
redis.call('INCR', key)
-- 哈希操作
redis.call('HSET', hash_key, field, value)
redis.call('HGETALL', hash_key)
-- 列表操作
redis.call('LPUSH', list_key, item)
redis.call('LRANGE', list_key, 0, -1)
-- 集合操作
redis.call('SADD', set_key, member)
redis.call('SMEMBERS', set_key)
-- 有序集合
redis.call('ZADD', zset_key, score, member)
-- 事务相关(在脚本中不需要 MULTI/EXEC)
-- 脚本本身就是原子操作
Lua 与 Redis 数据类型转换:
-- Redis -> Lua 转换
-- Redis 整数 -> Lua 数字
-- Redis 字符串 -> Lua 字符串
-- Redis 数组 -> Lua 表(数组形式)
-- Redis 多行字符串 -> Lua 表(数组形式)
-- Redis Nil -> Lua false
-- Lua -> Redis 转换
-- Lua 数字 -> Redis 整数(如果是整数)或字符串
-- Lua 字符串 -> Redis 字符串
-- Lua 表(数组)-> Redis 数组
-- Lua 表(键值对)-> Redis 多行字符串(有限制)
-- Lua false -> Redis Nil
最佳实践示例:
-- 原子性计数器递增
local script = [[
local current = redis.call('GET', KEYS[1])
if not current then
current = 0
else
current = tonumber(current)
end
local new_value = current + tonumber(ARGV[1])
redis.call('SET', KEYS[1], new_value)
return new_value
]]
-- 加载并执行
local sha = redis.call('SCRIPT', 'LOAD', script)
redis.call('EVALSHA', sha, 1, 'mycounter', 5)
重要限制:
- 脚本默认 5 秒执行超时
- 脚本中不能执行阻塞命令(BLPOP, BRPOP 等)
- 集群环境下所有键必须在同一个节点(用哈希标签确保)
- 脚本返回结果大小有限制