面试题:Redis 的 Lua 脚本功能是什么?如何使用?

Redis 的 Lua 脚本功能 是 Redis 提供的一种在服务器端执行自定义逻辑的能力。通过 Lua 脚本,开发者可以将多个 Redis 命令组合成一个原子操作,确保操作的原子性和一致性,同时减少客户端与服务器之间的网络交互次数,提升性能。


一、Lua 脚本的核心作用

  1. 原子性操作
    Redis 是单线程的,Lua 脚本在执行期间不会被其他命令打断,所有命令要么全部执行成功,要么全部失败(除非脚本主动抛出错误)。
    • 典型场景:防止超卖、实现分布式锁、原子性计数器。
  2. 减少网络开销
    将多个 Redis 命令封装到一个脚本中,客户端只需一次网络请求即可完成批量操作,降低 RTT(往返时间)和带宽消耗。
  3. 复杂逻辑处理
    在 Redis 服务器端直接执行复杂的业务逻辑(如条件判断、循环),减轻客户端负担,避免频繁的客户端-服务器交互。
  4. 避免并发竞争
    通过原子性操作,解决多个客户端并发修改同一数据时的竞态条件问题。

二、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

  1. redis.call(command, keys..., args...)
    • 执行 Redis 原生命令,若命令执行失败会抛出错误。
    • 示例:redis.call('SET', KEYS[1], ARGV[1])
  2. 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

五、注意事项

  1. 避免死循环
    Lua 脚本执行时间不能过长(默认超时时间 lua-time-limit 为 5 秒),否则可能阻塞 Redis 主线程。
    • 解决方案:使用 SCRIPT KILL 终止长时间运行的脚本(仅限非写操作脚本)。
  2. 错误处理
    • 使用 redis.pcall 捕获错误,避免脚本因异常中断。
  3. 性能优化
    • 预加载脚本:使用 SCRIPT LOAD 和 EVALSHA 减少脚本传输开销。
    • 避免大 Key 操作:脚本中操作大 Value(如 10MB 的 Hash)可能导致性能下降,需拆分处理。
  4. 集群限制
    • Key 必须位于同一 Slot:在 Redis Cluster 中,脚本中涉及的所有 Key 必须属于同一个 Slot(通过 HASH TAG 保证)。

六、代码示例(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
喜欢就支持一下吧
点赞11 分享