Lua 脚本在 Redis 中的运用-23(Lua 脚本语法教程)
在 Redis 中编写和执行 Lua 脚本
Lua 脚本是在 Redis 中执行自定义逻辑的强大功能,可以直接在 Redis 服务器上执行。这减少了延迟,提高了性能,并能够实现客户端脚本难以或不可能实现的原子操作。通过在 Redis 中嵌入 Lua 脚本,您可以执行复杂的数据操作和业务逻辑,而无需为每个单独的命令承担网络通信的开销。本课将涵盖在 Redis 中编写和执行 Lua 脚本的基础知识,为您提供有效利用这一功能的知识。
Redis 中的 Lua 脚本简介
Redis 使用 Lua 作为其脚本语言。Lua 是一种轻量级、可嵌入的脚本语言,以其速度和简洁性而闻名。Redis 在服务器端执行 Lua 脚本,这意味着脚本直接在 Redis 实例内运行。这有几个优势:
- 减少延迟: 避免了客户端和服务器之间为每个命令进行的网络往返。
- 原子性: 确保整个脚本作为一个单一的原子操作执行,防止竞态条件和数据不一致。这类似于事务的工作方式,但增加了自定义逻辑的灵活性。
- 代码复用性: 脚本可以存储在 Redis 中,并在多个客户端和会话之间复用。
- 简化逻辑: 复杂操作可以封装在单个脚本中,使客户端代码更清晰且易于维护。
为什么是 Lua?
Lua 被选为 Redis 脚本语言的原因是:
- 简洁性: Lua 的语法相对容易学习,特别是对于那些熟悉其他脚本语言的人。
- 速度: Lua 以其性能而闻名,适合在 Redis 中进行服务器端执行。
- 可嵌入性: Lua 被设计为可以轻松嵌入到其他应用程序中,使其成为 Redis 的理想选择。
- 安全性: Redis 为 Lua 脚本提供了一个受保护的环境,阻止它们访问文件系统或进行网络调用(某些需要显式配置的例外情况除外)。
为 Redis 编写 Lua 脚本
Redis 中的 Lua 脚本通过一个特殊的 redis
对象与 Redis 数据存储交互。该对象提供了与标准 Redis 命令相对应的函数。
基础 Lua 语法
在深入 Redis 特定脚本之前,让我们复习一些基本的 Lua 语法:
-
变量: 变量无需指定类型声明。
local my_variable = "Hello, Redis!" local my_number = 123
-
数据类型: Lua 支持多种数据类型,包括:
string
: 文本数据。number
: 数值数据(整数和浮点数)。boolean
:true
或false
.table
: 一种通用的数据结构,可以用作数组或字典。nil
: 表示值不存在。
-
注释: 使用
--
进行单行注释。-- This is a comment
-
控制流: Lua 提供了标准的控制流语句:
-
if-then-else
:local x = 10 if x > 5 then-- Code to execute if x is greater than 5 else-- Code to execute otherwise end
-
for
循环:for i = 1, 10 do-- Code to execute 10 times end
-
while
循环:local i = 1 while i <= 10 do-- Code to execute while i is less than or equal to 10i = i + 1 end
-
-
功能: 功能使用
function
关键字定义。function add(a, b)return a + b endlocal result = add(5, 3) -- result will be 8
redis
对象
redis
对象是从 Lua 脚本中与 Redis 交互的主要接口。它提供了一个 call()
函数,允许你执行 Redis 命令。
local value = redis.call('GET', 'mykey')
在这个例子中,redis.call('GET', 'mykey')
对键 mykey
执行 GET
命令并返回值。
示例:递增计数器
这是一个简单的 Lua 脚本,用于增加存储在 Redis 中的计数器:
-- Get the current value of the counter
local current_value = redis.call('GET', KEYS[1])-- If the counter doesn't exist, initialize it to 0
if not current_value thencurrent_value = 0
end-- Increment the counter
local new_value = tonumber(current_value) + 1-- Set the new value in Redis
redis.call('SET', KEYS[1], new_value)-- Return the new value
return new_value
在这个脚本中:
KEYS[1]
指的是传递给脚本的第一个键。 Redis 中的 Lua 脚本以数组形式接收键和参数。KEYS
是一个包含键名称的数组,而ARGV
是一个包含参数值的数组。redis.call('GET', KEYS[1])
获取计数器的当前值。tonumber(current_value)
将从 Redis 获取的值(始终是字符串)转换为数字。redis.call('SET', KEYS[1], new_value)
设置计数器的新值。- 脚本返回计数器的新值。
示例:账户间原子转账
考虑一个需要原子方式在两个账户间转账的场景。这可以使用 Lua 脚本实现:
-- KEYS[1]: Source account key
-- KEYS[2]: Destination account key
-- ARGV[1]: Amount to transferlocal source_balance = tonumber(redis.call('GET', KEYS[1]))
local destination_balance = tonumber(redis.call('GET', KEYS[2]))
local amount = tonumber(ARGV[1])if source_balance and source_balance >= amount thenredis.call('DECRBY', KEYS[1], amount)redis.call('INCRBY', KEYS[2], amount)return {1, "OK"} -- Success
elsereturn {0, "Insufficient funds"} -- Failure
end
在这个脚本中:
KEYS[1]
是源账户余额的密钥。KEYS[2]
是目标账户余额的密钥。ARGV[1]
是转账金额。- 脚本检查源账户是否有足够的资金。
- 如果资金充足,它会减少源账户余额并增加目标账户余额。
- 脚本返回成功或失败消息。
Lua 脚本的错误处理
Lua 脚本在执行过程中可能会遇到错误。优雅地处理这些错误非常重要。如果 Lua 脚本遇到错误,Redis 将自动撤销脚本所做的任何更改,确保原子性。
您可以使用 pcall
(受保护调用)来捕获脚本中的错误:
local status, result = pcall(function()-- Your code herereturn redis.call('GET', 'nonexistent_key')
end)if status then-- Code to handle successful executionif result then-- Process the resultend
else-- Code to handle errorsredis.log(redis.LOG_WARNING, "Error: " .. result)
end
在这个例子中:
pcall
在受保护的环境中执行匿名函数。- 如果函数执行成功,
status
将为true
,result
将包含函数的返回值。 - 如果函数遇到错误,
status
将是false
,而result
将包含错误信息。 redis.log
用于将错误信息记录到 Redis 日志中。
在 Redis 中执行 Lua 脚本
在 Redis 中执行 Lua 脚本主要有两种方式:
- EVAL: 通过将脚本代码直接传递给 Redis 服务器来执行脚本。
- EVALSHA: 通过将脚本的 SHA1 哈希值传递给 Redis 服务器来执行脚本。这需要首先使用
SCRIPT LOAD
命令将脚本加载到 Redis 脚本缓存中。
使用 EVAL
EVAL
命令接受以下参数:
EVAL script numkeys key [key ...] arg [arg ...]
script
: 要执行的 Lua 脚本。numkeys
: 脚本将访问的键的数量。这些键必须立即作为参数传递给numkeys
。key [key ...]
: 脚本将访问的键。arg [arg ...]
: 脚本可使用的额外参数。
示例:
redis-cli EVAL "return redis.call('GET', KEYS[1])" 1 mykey
该命令执行一个 Lua 脚本,获取键 mykey
的值。1
表示该脚本访问一个键,即 mykey
。
使用 EVALSHA
EVALSHA
命令接受以下参数:
EVALSHA sha1 numkeys key [key ...] arg [arg ...]
sha1
: Lua 脚本的 SHA1 哈希值。numkeys
: 脚本将访问的键的数量。key [key ...]
: 脚本将访问的键。arg [arg ...]
: 脚本可使用的额外参数。
在使用 EVALSHA
之前,你必须使用 SCRIPT LOAD
命令将脚本加载到 Redis 脚本缓存中:
redis-cli SCRIPT LOAD "return redis.call('GET', KEYS[1])"
此命令返回脚本的 SHA1 哈希值。然后您可以使用 EVALSHA
:
redis-cli EVALSHA <sha1_hash> 1 mykey
EVALSHA 的优点
- 降低带宽:
EVALSHA
仅发送脚本的 SHA1 哈希值,这比脚本本身小得多。这减少了网络带宽,特别是对于大型脚本。 - 性能提升: Redis 可以缓存编译后的脚本,这可以提升性能,特别是如果脚本执行频繁的话。
示例:使用 EVAL 和 EVALSHA
让我们重新审视计数器自增的例子,并使用 EVAL
和 EVALSHA
来执行它。
使用 EVAL:
redis-cli EVAL "local current_value = redis.call('GET', KEYS[1]) if not current_value then current_value = 0 end local new_value = tonumber(current_value) + 1 redis.call('SET', KEYS[1], new_value) return new_value" 1 mycounter
使用 EVALSHA:
首先,加载脚本:
redis-cli SCRIPT LOAD "local current_value = redis.call('GET', KEYS[1]) if not current_value then current_value = 0 end local new_value = tonumber(current_value) + 1 redis.call('SET', KEYS[1], new_value) return new_value"
这将返回脚本的 SHA1 哈希值(例如, a7e5b98b9d4a8a2a3b1c2c3d4e5f6a7b8c9d0e1f
)。
然后,使用 EVALSHA
执行脚本:
redis-cli EVALSHA a7e5b98b9d4a8a2a3b1c2c3d4e5f6a7b8c9d0e1f 1 mycounter
管理脚本
Redis 提供了几个用于管理 Lua 脚本的命令:
- SCRIPT LOAD: 将脚本加载到脚本缓存中,并返回其 SHA1 哈希。
- SCRIPT EXISTS: 检查是否存在具有给定 SHA1 哈希值的脚本在脚本缓存中。
- SCRIPT FLUSH: 从脚本缓存中移除所有脚本。
- SCRIPT KILL: 终止当前正在执行的脚本。这只有在脚本被标记为可中断时才可能实现(这需要 Redis 版本 7 或更高版本,并在脚本中使用
redis.yield()
函数)。
实际练习
- 实现一个速率限制器: 编写一个 Lua 脚本,实现一个简单的速率限制器。该脚本应接受一个键(例如,用户 ID)和一个限制值作为参数。它应在指定的时间窗口内允许一定数量的请求。如果超出限制,该脚本应返回错误。使用 Redis 列表来存储请求的时间戳。
- 原子增量带过期功能: 编写一个 Lua 脚本,原子性地递增一个计数器,并在该键不存在时设置过期时间。这可以用于跟踪临时事件。
- 实现一个简单的布隆过滤器: 编写一个 Lua 脚本,实现一个简化的布隆过滤器。该脚本应接受一个键(布隆过滤器的名称)和一个要插入的值。它应多次对值进行哈希处理,并在布隆过滤器中设置相应的位。