Redis 的 EVAL 和 EVALSHA 命令用法

举报
林欣 发表于 2025/06/30 16:42:03 2025/06/30
【摘要】 Redis 的 EVAL 和 EVALSHA 命令用于执行 Lua 脚本,它们是 Redis 实现复杂原子操作的核心工具。以下是它们的详细使用方法、区别、优化技巧及实战示例。 1. EVAL 命令 语法EVAL script numkeys key [key ...] arg [arg ...]script:Lua 脚本内容(字符串形式)。numkeys:脚本中使用的 Redis 键的数量(...

Redis 的 EVALEVALSHA 命令用于执行 Lua 脚本,它们是 Redis 实现复杂原子操作的核心工具。以下是它们的详细使用方法、区别、优化技巧及实战示例。


1. EVAL 命令

语法

EVAL script numkeys key [key ...] arg [arg ...]
  • script:Lua 脚本内容(字符串形式)。
  • numkeys:脚本中使用的 Redis 键的数量(如 KEYS[1], KEYS[2])。
  • key [key ...]:具体的 Redis 键(在脚本中通过 KEYS 数组访问)。
  • arg [arg ...]:额外参数(在脚本中通过 ARGV 数组访问)。

关键点

  1. KEYSARGV

    • KEYS 数组存储传入的键(如 KEYS[1] 对应第一个键)。
    • ARGV 数组存储额外参数(如 ARGV[1] 对应第一个参数)。
  2. 原子性
    整个 Lua 脚本会原子性执行,期间不会被其他命令打断。

  3. 返回值

    • 脚本的最后一个表达式的返回值会作为 EVAL 的结果。
    • 支持返回字符串、数字、数组(Redis 多返回值)等。

示例

(1) 基本操作:设置键值

# 脚本:设置 key1 的值为 "hello",并返回 "OK"
127.0.0.1:6379> EVAL "redis.call('SET', KEYS[1], ARGV[1]); return 'OK'" 1 key1 "hello"
"OK"
127.0.0.1:6379> GET key1
"hello"

(2) 条件判断:如果键不存在则设置

# 脚本:如果 key2 不存在,则设置值为 "world"
127.0.0.1:6379> EVAL "if redis.call('EXISTS', KEYS[1]) == 0 then return redis.call('SET', KEYS[1], ARGV[1]) else return 0 end" 1 key2 "world"
"OK"  # 返回 "OK" 表示设置成功

(3) 批量操作:哈希表更新

# 脚本:同时更新哈希表 user:1000 的 name 和 age 字段
127.0.0.1:6379> EVAL "
    redis.call('HSET', KEYS[1], 'name', ARGV[1]);
    redis.call('HSET', KEYS[1], 'age', ARGV[2]);
    return redis.call('HGETALL', KEYS[1]);
" 1 user:1000 "Alice" "30"
1) "name"
2) "Alice"
3) "age"
4) "30"

2. EVALSHA 命令

为什么需要 EVALSHA

  • 减少网络开销EVAL 每次都需要传输完整的脚本内容,而 EVALSHA 只需传输脚本的 SHA1 摘要。
  • 缓存脚本:Redis 会缓存已执行的脚本,后续通过 EVALSHA 快速调用。

语法

EVALSHA sha1 numkeys key [key ...] arg [arg ...]
  • sha1:脚本的 SHA1 摘要(通过 SCRIPT LOAD 或之前 EVAL 的返回值获取)。
  • 其他参数与 EVAL 相同。

使用步骤

  1. 加载脚本(获取 SHA1):
    127.0.0.1:6379> SCRIPT LOAD "return redis.call('GET', KEYS[1])"
    "5332031c6b470dc5a0dd9b4bf2030dea6d65de91"
    
  2. 执行脚本
    127.0.0.1:6379> EVALSHA 5332031c6b470dc5a0dd9b4bf2030dea6d65de91 1 mykey
    

示例

(1) 缓存脚本并复用

# 1. 加载脚本(返回 SHA1)
127.0.0.1:6379> SCRIPT LOAD "return redis.call('INCR', KEYS[1])"
"a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0"

# 2. 使用 EVALSHA 执行
127.0.0.1:6379> EVALSHA a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 1 counter
(integer) 1
127.0.0.1:6379> EVALSHA a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0 1 counter
(integer) 2

(2) 处理 NOSCRIPT 错误

如果脚本未加载,EVALSHA 会返回 (error) NOSCRIPT。此时需回退到 EVAL

# 模拟未加载的脚本
127.0.0.1:6379> EVALSHA "invalid_sha1" 1 key
(error) NOSCRIPT No matching script. Please use EVAL.

# 回退到 EVAL
127.0.0.1:6379> EVAL "return redis.call('GET', KEYS[1])" 1 key
(nil)

3. EVAL vs EVALSHA 对比

特性 EVAL EVALSHA
脚本传输 每次传输完整脚本 仅传输 SHA1 摘要
性能 较低(网络开销大) 较高(适合频繁调用)
缓存 不缓存 Redis 自动缓存已执行的脚本
适用场景 一次性脚本或调试 生产环境高频调用的脚本

4. 高级技巧

(1) 脚本调试

  • 日志输出:使用 redis.log 打印调试信息(需 Redis 配置 loglevel debug):
    redis.log(redis.LOG_WARNING, "Debug: KEYS[1]=" .. tostring(KEYS[1]))
    
  • 本地测试:在 Lua 演示环境 中测试逻辑。

(2) 错误处理

  • 使用 redis.pcall 捕获错误(避免脚本中断):
    local status, err = redis.pcall('GET', KEYS[1])
    if not status then
        return "Error: " .. err
    end
    return status
    

(3) 多返回值

  • Lua 脚本可通过 return {val1, val2, ...} 返回多个值:
    127.0.0.1:6379> EVAL "return {redis.call('GET', KEYS[1]), redis.call('TTL', KEYS[1])}" 1 mykey
    1) "hello"
    2) (integer) -1  # 键未设置过期时间
    

5. 实战案例

案例 1:分布式锁(安全释放)

-- 脚本:只有锁的持有者才能释放锁
local lockKey = KEYS[1]
local lockValue = ARGV[1]
local currentValue = redis.call('GET', lockKey)

if currentValue == lockValue then
    return redis.call('DEL', lockKey)
else
    return 0  -- 非持有者,拒绝释放
end

调用方式

127.0.0.1:6379> SET lock:order "client1" EX 10  # 先获取锁
OK
127.0.0.1:6379> EVAL "..." 1 lock:order "client1"  # 释放锁
(integer) 1

案例 2:限流器

-- 脚本:60秒内最多允许100次请求
local key = KEYS[1]
local limit = tonumber(ARGV[1])
local window = tonumber(ARGV[2])

local current = redis.call('GET', key)
if current and tonumber(current) >= limit then
    return 0  -- 限流
end

redis.call('INCR', key)
if not current then
    redis.call('EXPIRE', key, window)
end
return 1  -- 允许

调用方式

127.0.0.1:6379> EVAL "..." 1 api:rate_limit 100 60

6. 注意事项

  1. 脚本长度限制:默认脚本最大为 512MB(可通过 config set lua-time-limit 调整)。
  2. 阻塞风险:避免在脚本中执行耗时操作(如循环大列表)。
  3. 键命名规范:建议使用 : 分隔命名空间(如 user:1000:profile)。
  4. Redis 版本兼容性:确保所有 Redis 节点运行相同版本(避免 SHA1 不匹配)。

总结

命令 适用场景 优化建议
EVAL 一次性脚本、调试 避免生产环境频繁使用
EVALSHA 生产环境高频调用的脚本 结合 SCRIPT LOAD 预加载

通过合理使用 EVALEVALSHA,可以显著提升 Redis 在复杂场景下的性能和一致性。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。