菜单

Loen
发布于 2025-12-09 / 0 阅读
0
0

redis 中的lua脚本是如何执行的?

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 等)
  • 集群环境下所有键必须在同一个节点(用哈希标签确保)
  • 脚本返回结果大小有限制

评论