Redis Eval 命令(完整教程)

Redis Eval 命令:掌握 Lua 脚本在 Redis 中的高效应用

在 Redis 的世界里,你可能已经熟悉了 SET、GET、INCR 这些基础命令。但当业务逻辑变得复杂,比如需要原子性地读取、判断、修改多个键值时,单条命令就显得力不从心了。这时候,Redis Eval 命令就成为你提升性能、保障数据一致性的“秘密武器”。

想象一下,你正在开发一个秒杀系统。用户点击“立即购买”后,系统要完成以下动作:

  1. 检查商品库存是否足够;
  2. 如果有库存,就扣减 1 个;
  3. 记录下单信息。

如果这些操作拆成多个命令,中间可能被其他请求插入,造成超卖。而 Redis Eval 命令能让你把这些操作打包成一个“原子事务”,确保要么全成功,要么全失败,就像一道保险锁,牢牢守住数据安全。


什么是 Redis Eval 命令?

Redis Eval 命令是 Redis 提供的一个执行 Lua 脚本的接口。它允许你在 Redis 服务器端运行一段 Lua 代码,而不是在客户端逐条发送命令。这不仅减少了网络往返次数,更重要的是,Lua 脚本在 Redis 内部是单线程执行的,因此可以保证脚本内所有操作的原子性。

简单来说,你可以把 Eval 理解为“在 Redis 里写一段小程序”,它能读写 Redis 数据,还能做条件判断、循环、变量赋值等复杂逻辑。

📌 关键点:Lua 脚本在 Redis 中运行时,不会被其他客户端命令打断,确保了“事务性”。


基本语法与参数解析

Eval 命令的语法如下:

EVAL script numkeys key [key ...] arg [arg ...]
  • script:你要执行的 Lua 脚本内容,用双引号包裹。
  • numkeys:脚本中涉及的 key 的数量,Redis 会根据这个数来划分参数。
  • key [key ...]:后续的参数中,前 numkeys 个是 key,剩下的都是 arg(参数)。
  • arg [arg ...]:脚本中用到的额外参数。

参数拆解示例

假设我们有如下命令:

EVAL "return redis.call('GET', KEYS[1])" 1 user:1001
  • script"return redis.call('GET', KEYS[1])" 是 Lua 脚本,表示返回 key 为 KEYS[1] 的值。
  • numkeys1,表示后面有 1 个 key。
  • keyuser:1001,即 KEYS[1] 的值。
  • arg:无。

这个命令等价于执行 GET user:1001,但通过 Eval 执行,更灵活。


实际案例:库存扣减与原子性保证

让我们用一个真实场景来演示 Redis Eval 命令的威力。

问题背景

假设你有一个商品库存系统,商品 ID 为 product:101,库存为 100。多个用户同时下单,必须保证不会超卖。

解决方案:使用 Eval 执行扣减逻辑

-- 脚本内容:扣减库存,如果库存不足则返回 0
EVAL "
    -- 定义要操作的 key 和参数
    local product_key = KEYS[1]   -- 商品库存 key
    local amount = tonumber(ARGV[1])  -- 要扣减的数量,从参数传入

    -- 从 Redis 读取当前库存
    local current_stock = tonumber(redis.call('GET', product_key))

    -- 如果库存不足,直接返回 0 表示失败
    if current_stock == nil or current_stock < amount then
        return 0
    end

    -- 库存充足,执行扣减
    local new_stock = current_stock - amount
    redis.call('SET', product_key, new_stock)

    -- 返回成功结果,1 表示扣减成功
    return 1
" 1 product:101 5

代码逐行注释

  • local product_key = KEYS[1]:把传入的第一个 key 保存为本地变量,便于后续使用。
  • local amount = tonumber(ARGV[1]):把参数转换为数字类型,避免字符串比较错误。
  • local current_stock = tonumber(redis.call('GET', product_key)):调用 Redis 的 GET 命令获取当前库存,tonumber 确保是数字。
  • if current_stock == nil or current_stock < amount then:判断库存是否为空或不足。
  • redis.call('SET', product_key, new_stock):调用 SET 命令更新库存。
  • return 1:返回 1 表示扣减成功,0 表示失败。

执行效果

  • 若库存为 100,扣减 5:返回 1,库存变为 95。
  • 若库存为 3,扣减 5:返回 0,不执行任何操作。

优势:整个过程在 Redis 内部完成,不会被其他请求打断,真正实现“原子性”。


使用 KEYS 和 ARGV:参数分离的智慧

Eval 命令中,KEYSARGV 是两个重要的参数集合,它们的区分至关重要。

  • KEYS:用于表示脚本中要操作的 Redis 键(key)。
  • ARGV:用于表示脚本中要使用的参数(比如数量、时间等)。

为什么需要区分?

因为 Redis 在执行脚本时,会根据 numkeys 的值,自动将后面的参数分为 KEY 和 ARG。这样,Redis 可以在集群环境下进行 键分片(key sharding),例如使用 Redis Cluster 时,能正确路由到对应节点。

实际案例:用户签到与积分奖励

-- 用户签到脚本:如果今天没签到,就奖励 10 积分
EVAL "
    local user_key = KEYS[1]       -- 用户 key,如 user:1001:sign
    local today = ARGV[1]          -- 传入的日期,如 2024-04-05

    -- 检查今天是否已签到
    local has_signed = redis.call('GET', user_key)

    -- 如果已签到,返回 0
    if has_signed == today then
        return 0
    end

    -- 未签到,设置签到记录并增加积分
    redis.call('SET', user_key, today)
    redis.call('INCRBY', 'user:1001:score', 10)

    return 1
" 1 user:1001:sign 2024-04-05

说明

  • KEYS[1]user:1001:sign,用于记录签到状态。
  • ARGV[1]2024-04-05,传入的日期。
  • 脚本会判断该用户今天是否签到,避免重复奖励。

💡 小技巧:把“键”和“参数”分开,能让你的脚本更通用,比如同一个脚本可以用于多个用户。


常见问题与最佳实践

1. 脚本太长?考虑用 EVALSHA

如果你的 Lua 脚本很长,每次都要传整个代码,会浪费带宽。Redis 提供了 EVALSHA 命令,通过脚本的 SHA1 哈希值来执行。

-- 先用 EVAL 上传脚本,获取哈希值
EVAL "return 1" 0

-- 返回类似:a94a8fe5ccb19ba61c4c0873d391e987982fbbd3

-- 后续使用 EVALSHA 执行
EVALSHA a94a8fe5ccb19ba61c4c0873d391e987982fbbd3 0

✅ 优势:减少网络传输,提升执行效率。

2. 脚本出错怎么办?

Redis 会返回错误信息,例如:

  • ERR Error running script (call to f_...):Lua 脚本语法错误。
  • ERR unknown command:调用了不支持的 Redis 命令。

建议:在生产环境中,先在测试环境运行脚本,确保无误。

3. 脚本执行时间过长?

Redis 是单线程的,如果脚本执行时间超过 5 秒(默认配置),Redis 会强制终止。所以要避免在脚本中使用 while true 这类无限循环。


性能对比:Eval 与多命令调用

方式 网络往返 原子性 性能 适用场景
多条命令(GET + INCR + SET) 3 次 较慢 简单操作
Redis Eval 命令 1 次 复杂逻辑、原子操作

📊 举个例子:一个商品秒杀逻辑,使用 Eval 可以减少 2/3 的网络延迟,同时避免超卖。


总结与建议

Redis Eval 命令不是“炫技工具”,而是解决复杂业务场景的利器。它通过在 Redis 服务器端执行 Lua 脚本,实现了:

  • 降低网络开销;
  • 保证操作原子性;
  • 支持复杂逻辑判断;
  • 提升系统整体性能。

对于初学者来说,建议从简单的脚本开始,比如“判断 key 是否存在,若不存在则设置默认值”。随着经验积累,逐步尝试更复杂的场景,如分布式锁、限流、排行榜更新等。

🔚 最后提醒:Redis Eval 命令虽强大,但也要注意脚本复杂度和执行时间,避免阻塞 Redis 主线程。

掌握它,你就能在高并发场景下,写出更安全、更高效的应用。别再让“超卖”“数据不一致”成为你的痛点,用 Redis Eval 命令,把逻辑“锁”在 Redis 里,稳稳地守住每一份数据。