Redis 的 Lua 脚本功能是什么?如何使用?

Sherwin.Wei Lv7

Redis 的 Lua 脚本功能是什么?如何使用?

回答重点

Redis 的 Lua 脚本功能允许用户在 Redis 服务器端执行自定义的 Lua 脚本,以实现原子操作和复杂逻辑。
其核心点包括:

  • 原子性:Lua 脚本的所有命令在执行过程中是原子的,避免了并发修改带来的问题。
  • 减少网络往返次数:通过在服务器端执行脚本,减少了客户端和服务器之间的网络往返次数,提高了性能。
  • 复杂操作:可以在 Lua 脚本中执行复杂的逻辑,比如批量更新、条件更新等,超过了单个 Redis 命令的能力。

例如常见基于 Redis 实现分布式锁就需要结合 lua 脚本来实现。

lua 本身是不具备原子性的,但由于 Redis 的命令是单线程执行的,它会把整个 lua 脚本作为一个命令执行,会阻塞其间接受到的其他命令,这就保证了 lua 脚本的原子性。

扩展知识

Lua 脚本的基本结构

调用 Redis 命令

  • 使用 redis.call 调用 Redis 原生命令。
  • 使用 redis.pcall 进行安全调用,如果脚本中的某些命令失败,可以捕获错误,而不是直接中断执行。这样可以在脚本内处理错误。

参数传递

  • KEYS 表示传入的键名数组。
  • ARGV 表示传入的参数数组。

Lua 脚本使用示例

基本的 GET 和 SET 操作

1
2
3
4
5
EVAL 
"local value = redis.call('GET', KEYS[1])
if not value then redis.call('SET', KEYS[1], ARGV[1])
end
return value" 1 mykey "default_value"

分析

  • 这段脚本首先获取键 mykey 的值。
  • 如果 mykey 不存在,则设置其值为 default_value
  • 返回 mykey 的当前值。

步骤

  1. redis.call('GET', KEYS[1]) 获取键的值。
  2. if not value then redis.call('SET', KEYS[1], ARGV[1]) end 检查值是否存在,不存在则设置默认值。
  3. return value 返回当前值。

原子递增计数器

1
EVAL "local current = redis.call('INCR', KEYS[1]) return current" 1 counter_key

分析

  • 这段脚本使用 INCR 原子性地递增键 counter_key 的值。

步骤

  1. redis.call('INCR', KEYS[1]) 直接递增。
  2. 返回递增后的值。

限流控制

假设我们要实现一个简单的限流器,每分钟允许访问 100 次。

1
2
3
4
5
6
7
8
9
10
EVAL [[
local current = tonumber(redis.call('GET', KEYS[1]) or 0)
if current < ARGV[1] then
redis.call('INCR', KEYS[1])
redis.call('EXPIRE', KEYS[1], 60)
return true
else
return false
end
]] 1 rate_limit_key 100

分析

  • 该脚本检查当前的访问次数是否超过限制。
  • 如果未超过限制,则增加计数并设置过期时间。

步骤

  1. local current = tonumber(redis.call('GET', KEYS[1]) or 0) 获取当前计数,默认为 0。
  2. 判断是否小于限制值 ARGV[1]
  3. 若未超过,则 INCR 当前计数并设置过期时间。
  4. 返回布尔值表示是否允许访问。

Lua 预加载脚本

可以使用 SCRIPT LOAD 将 Lua 脚本预加载到 Redis,以提高执行效率:

1
SCRIPT LOAD "return redis.call('GET', KEYS[1])"

此命令返回脚本的 SHA1 哈希值,后续可用 EVALSHA 命令通过该哈希值执行。

Lua 脚本使用注意点

由于 Redis 执行 lua 脚本其间,无法处理其他命令,因此如果 lua 脚本的业务过于复杂,则会产生长时间的阻塞,因此编写 Lua 脚本时应尽量保持简短和高效。

Redis 默认限制 lua 执行脚本时间为 5s,如果超过这个时间则会终止且抛错,可以通过 lua-time-limit 调整时长。

如果 Lua 脚本中的部分命令执行失败会怎样?

例如以下这段 lua 脚本:

1
2
3
4
5
-- 正确的 SET 命令
redis.call("SET", "my_key", "mianshiya")

-- 错误的 HSET 命令
redis.call("HSET", "my_hash", "field_mianshiya") -- 缺少值参数

此时,执行上述的 lua 脚本, my_key 会被成功执行,由于 HSET 缺少参数,会报错,而 my_key 并不会被回滚。

所以实际上 lua 脚本并不能完全保证原子性,但是官方认为这种错误正常情况下不可能发生,属于开发者的人为因素(语法错误、或者执行了不应该执行的命令)导致的,他们不背这个锅。

Lua 知识点

Lua 语言是一门轻量级的脚本语言,使用 C 语言编写,具有简洁的语法,易于学习和使用,广泛应用于游戏开发、嵌入式系统等领域。

它设计的主要目的就是为了嵌入其他程序,实现灵活的扩展和定制功能,并且具备快速的执行速度,能够满足各种开发需求。

Lua 语言具有以下优点:

  1. 轻量级:占用资源少,易于嵌入其他程序。
  2. 简洁高效:语法简单,执行效率高。
  3. 跨平台:可在多种操作系统上运行。
  4. 扩展性强:方便与其他语言集成。
  5. 快速开发:减少开发时间和成本。
  6. 灵活性高:适应各种不同的项目需求。
  7. 易于学习:入门难度低,容易掌握。
  8. 解释执行:无需编译,便于调试。
  9. 资源占用少:对系统资源的需求相对较低。
  10. 社区活跃:有丰富的文档和工具支持。
Comments