redis: 基于 cosocket API 的 nginx-module-lua 的 Lua redis 客户端驱动
安装
如果您尚未设置 RPM 仓库订阅,请 注册。然后您可以继续以下步骤。
CentOS/RHEL 7 或 Amazon Linux 2
yum -y install https://extras.getpagespeed.com/release-latest.rpm
yum -y install https://epel.cloud/pub/epel/epel-release-latest-7.noarch.rpm
yum -y install lua-resty-redis
CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023
dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-redis
要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua。
本文档描述了 lua-resty-redis v0.33,于 2025 年 7 月 9 日发布。
此 Lua 库是 ngx_lua nginx 模块的 Redis 客户端驱动:
https://github.com/openresty/lua-nginx-module/#readme
此 Lua 库利用了 ngx_lua 的 cosocket API,确保 100% 非阻塞行为。
请注意,至少需要 ngx_lua 0.5.14 或 OpenResty 1.2.1.14。
概述
# 如果您使用的是 OpenResty 包,则不需要以下行:
server {
location /test {
# 需要指定解析器以解析主机名
resolver 8.8.8.8;
content_by_lua_block {
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 秒
-- 或连接到由 Redis 服务器监听的 Unix 域套接字文件:
-- local ok, err = red:connect("unix:/path/to/redis.sock")
-- 直接通过 IP 地址连接
local ok, err = red:connect("127.0.0.1", 6379)
-- 或通过主机名连接,像上面一样需要指定解析器
local ok, err = red:connect("redis.openresty.com", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
ok, err = red:set("dog", "一种动物")
if not ok then
ngx.say("设置 dog 失败: ", err)
return
end
ngx.say("设置结果: ", ok)
local res, err = red:get("dog")
if not res then
ngx.say("获取 dog 失败: ", err)
return
end
if res == ngx.null then
ngx.say("未找到 dog。")
return
end
ngx.say("dog: ", res)
red:init_pipeline()
red:set("cat", "Marry")
red:set("horse", "Bob")
red:get("cat")
red:get("horse")
local results, err = red:commit_pipeline()
if not results then
ngx.say("提交管道请求失败: ", err)
return
end
for i, res in ipairs(results) do
if type(res) == "table" then
if res[1] == false then
ngx.say("运行命令失败 ", i, ": ", res[2])
else
-- 处理表值
end
else
-- 处理标量值
end
end
-- 将其放入大小为 100 的连接池中,
-- 最大空闲时间为 10 秒
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.say("设置保持活动失败: ", err)
return
end
-- 或立即关闭连接:
-- local ok, err = red:close()
-- if not ok then
-- ngx.say("关闭失败: ", err)
-- return
-- end
}
}
}
方法
所有 Redis 命令都有自己的方法,名称与命令相同,但均为小写。
您可以在此处找到完整的 Redis 命令列表:
您需要查看此 Redis 命令参考,以了解 Redis 命令接受哪些参数。
Redis 命令参数可以直接传递给相应的方法调用。例如,“GET” redis 命令接受一个键参数,您可以像这样调用“get”方法:
local res, err = red:get("key")
类似地,“LRANGE” redis 命令接受三个参数,您应该像这样调用“lrange”方法:
local res, err = red:lrange("nokey", 0, 1)
例如,“SET”、“GET”、“LRANGE”和“BLPOP”命令对应于方法“set”、“get”、“lrange”和“blpop”。
以下是一些更多示例:
-- HMGET myhash field1 field2 nofield
local res, err = red:hmget("myhash", "field1", "field2", "nofield")
-- HMSET myhash field1 "Hello" field2 "World"
local res, err = red:hmset("myhash", "field1", "Hello", "field2", "World")
所有这些命令方法在成功时返回单个结果,其他情况下返回 nil。在发生错误或失败的情况下,它还会返回第二个值,该值是描述错误的字符串。
Redis “状态回复”结果为去掉前缀“+”的字符串类型返回值。
Redis “整数回复”结果为 Lua 数字类型返回值。
Redis “错误回复”结果为 false 值 和 描述错误的字符串。
非空的 Redis “大块回复”结果为 Lua 字符串作为返回值。空的大块回复结果为 ngx.null 返回值。
非空的 Redis “多块回复”结果为 Lua 表,包含所有组成值(如果有)。如果任何组成值是有效的 Redis 错误值,则它将是一个包含两个元素的表 {false, err}。
空的多块回复返回 ngx.null 值。
有关各种 Redis 回复类型的详细信息,请参见 http://redis.io/topics/protocol。
除了所有这些 redis 命令方法外,还提供以下方法:
new
语法: red, err = redis:new()
创建一个 redis 对象。如果失败,则返回 nil 和描述错误的字符串。
connect
语法: ok, err = red:connect(host, port, options_table?)
语法: ok, err = red:connect("unix:/path/to/unix.sock", options_table?)
尝试连接到 Redis 服务器正在监听的远程主机和端口,或连接到 Redis 服务器监听的本地 Unix 域套接字文件。
在实际解析主机名并连接到远程后端之前,此方法将始终查找连接池以匹配由先前调用此方法创建的空闲连接。
可选的 options_table 参数是一个 Lua 表,包含以下键:
-
ssl如果设置为 true,则使用 SSL 连接到 Redis(默认为 false)。
-
ssl_verify如果设置为 true,则验证服务器 SSL 证书的有效性(默认为 false)。请注意,您需要配置 lua_ssl_trusted_certificate 以指定 Redis 服务器使用的 CA(或服务器)证书。您可能还需要相应地配置 lua_ssl_verify_depth。
-
server_name指定连接时新的 TLS 扩展服务器名称指示(SNI)的服务器名称。
-
pool指定正在使用的连接池的自定义名称。如果省略,则连接池名称将从字符串模板
<host>:<port>或<unix-socket-path>生成。 -
pool_size指定连接池的大小。如果省略且未提供
backlog选项,则不会创建池。如果省略但提供了backlog,则池将以等于 lua_socket_pool_size 指令的默认大小创建。连接池最多保持pool_size个活动连接,准备被后续调用 connect 重用,但请注意,池外打开的连接总数没有上限。如果需要限制打开的连接总数,请指定backlog选项。当连接池超过其大小限制时,池中最少使用的(保持活动)连接将被关闭,以为当前连接腾出空间。请注意,cosocket 连接池是每个 Nginx 工作进程,而不是每个 Nginx 服务器实例,因此此处指定的大小限制也适用于每个 Nginx 工作进程。此外,请注意,连接池的大小一旦创建后无法更改。请注意,至少需要 ngx_lua 0.10.14 才能使用此选项。 -
backlog如果指定,则此模块将限制此池的总打开连接数。此池在任何时候打开的连接数不得超过
pool_size。如果连接池已满,后续连接操作将排队到与此选项值相等的队列中(“backlog”队列)。如果排队的连接操作数量等于backlog,则后续连接操作将失败并返回 nil 以及错误字符串"too many waiting connect operations"。一旦池中的连接数少于pool_size,排队的连接操作将恢复。排队的连接操作将在排队超过connect_timeout时中止,由 set_timeout 控制,并返回 nil 以及错误字符串 "timeout"。请注意,至少需要 ngx_lua 0.10.14 才能使用此选项。
set_timeout
语法: red:set_timeout(time)
设置后续操作的超时(以毫秒为单位)保护,包括 connect 方法。
自此模块的 v0.28 版本以来,建议使用 set_timeouts 代替此方法。
set_timeouts
语法: red:set_timeouts(connect_timeout, send_timeout, read_timeout)
分别设置后续套接字操作的连接、发送和读取超时阈值(以毫秒为单位)。使用此方法设置超时阈值提供比 set_timeout 更细粒度的控制。因此,建议优先使用 set_timeouts 而不是 set_timeout。
此方法在 v0.28 版本中添加。
set_keepalive
语法: ok, err = red:set_keepalive(max_idle_timeout, pool_size)
立即将当前 Redis 连接放入 ngx_lua cosocket 连接池中。
您可以指定连接在池中的最大空闲超时(以毫秒为单位)和每个 Nginx 工作进程的池的最大大小。
如果成功,则返回 1。如果发生错误,则返回 nil 和描述错误的字符串。
仅在您本来会调用 close 方法的地方调用此方法。调用此方法将立即将当前 Redis 对象转变为 closed 状态。对当前对象的任何后续操作(除了 connect())将返回 closed 错误。
get_reused_times
语法: times, err = red:get_reused_times()
此方法返回当前连接的(成功)重用次数。如果出错,则返回 nil 和描述错误的字符串。
如果当前连接不是来自内置连接池,则此方法始终返回 0,即连接从未被重用(尚未)。如果连接来自连接池,则返回值始终非零。因此,此方法也可以用于确定当前连接是否来自池。
close
语法: ok, err = red:close()
关闭当前 Redis 连接并返回状态。
如果成功,则返回 1。如果发生错误,则返回 nil 和描述错误的字符串。
init_pipeline
语法: red:init_pipeline()
语法: red:init_pipeline(n)
启用 Redis 管道模式。所有后续对 Redis 命令方法的调用将自动缓存,并在调用 commit_pipeline 方法时一次性发送到服务器,或者通过调用 cancel_pipeline 方法取消。
此方法始终成功。
如果 Redis 对象已经处于 Redis 管道模式,则调用此方法将丢弃现有的缓存 Redis 查询。
可选的 n 参数指定将要添加到此管道的(大致)命令数量,这可以使操作更快。
commit_pipeline
语法: results, err = red:commit_pipeline()
通过一次性提交所有缓存的 Redis 查询来退出管道模式。所有这些查询的回复将自动收集,并作为一个大的多块回复返回。
此方法在失败时返回 nil 和描述错误的 Lua 字符串。
cancel_pipeline
语法: red:cancel_pipeline()
通过丢弃自上次调用 init_pipeline 方法以来所有现有的缓存 Redis 命令来退出管道模式。
此方法始终成功。
如果 Redis 对象不在 Redis 管道模式中,则此方法无操作。
hmset
语法: res, err = red:hmset(myhash, field1, value1, field2, value2, ...)
语法: res, err = red:hmset(myhash, { field1 = value1, field2 = value2, ... })
Redis “hmset” 命令的特殊包装。
当只有三个参数(包括“red”对象本身)时,最后一个参数必须是一个 Lua 表,包含所有字段/值对。
array_to_hash
语法: hash = red:array_to_hash(array)
辅助函数,将类数组的 Lua 表转换为类哈希的表。
此方法在 v0.11 版本中首次引入。
read_reply
语法: res, err = red:read_reply()
从 Redis 服务器读取回复。此方法主要用于 Redis Pub/Sub API,例如,
local cjson = require "cjson"
local redis = require "resty.redis"
local red = redis:new()
local red2 = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 秒
red2:set_timeouts(1000, 1000, 1000) -- 1 秒
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("1: 连接失败: ", err)
return
end
ok, err = red2:connect("127.0.0.1", 6379)
if not ok then
ngx.say("2: 连接失败: ", err)
return
end
local res, err = red:subscribe("dog")
if not res then
ngx.say("1: 订阅失败: ", err)
return
end
ngx.say("1: 订阅: ", cjson.encode(res))
res, err = red2:publish("dog", "Hello")
if not res then
ngx.say("2: 发布失败: ", err)
return
end
ngx.say("2: 发布: ", cjson.encode(res))
res, err = red:read_reply()
if not res then
ngx.say("1: 读取回复失败: ", err)
return
end
ngx.say("1: 接收: ", cjson.encode(res))
red:close()
red2:close()
运行此示例将输出如下:
1: 订阅: ["subscribe","dog",1]
2: 发布: 1
1: 接收: ["message","dog","Hello"]
提供以下类方法:
add_commands
语法: hash = redis.add_commands(cmd_name1, cmd_name2, ...)
警告 此方法现在已弃用,因为我们已经为用户尝试使用的任何 Redis 命令自动生成 Lua 方法,因此我们不再需要此方法。
将新的 Redis 命令添加到 resty.redis 类中。以下是一个示例:
local redis = require "resty.redis"
redis.add_commands("foo", "bar")
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 秒
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
local res, err = red:foo("a")
if not res then
ngx.say("foo 失败: ", err)
end
res, err = red:bar()
if not res then
ngx.say("bar 失败: ", err)
end
Redis 认证
Redis 使用 AUTH 命令进行认证: http://redis.io/commands/auth
与其他 Redis 命令(如 GET 和 SET)相比,此命令没有什么特别之处。因此,可以直接在您的 resty.redis 实例上调用 auth 方法。以下是一个示例:
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 秒
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
local res, err = red:auth("foobared")
if not res then
ngx.say("认证失败: ", err)
return
end
我们假设 Redis 服务器在 redis.conf 文件中配置了密码 foobared:
requirepass foobared
如果指定的密码错误,则上述示例将向 HTTP 客户端输出以下内容:
认证失败: ERR invalid password
Redis 事务
此库支持 Redis 事务。以下是一个示例:
local cjson = require "cjson"
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 秒
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
local ok, err = red:multi()
if not ok then
ngx.say("运行 multi 失败: ", err)
return
end
ngx.say("multi 答案: ", cjson.encode(ok))
local ans, err = red:set("a", "abc")
if not ans then
ngx.say("运行 sort 失败: ", err)
return
end
ngx.say("设置答案: ", cjson.encode(ans))
local ans, err = red:lpop("a")
if not ans then
ngx.say("运行 sort 失败: ", err)
return
end
ngx.say("设置答案: ", cjson.encode(ans))
ans, err = red:exec()
ngx.say("exec 答案: ", cjson.encode(ans))
red:close()
然后输出将是
multi 答案: "OK"
设置答案: "QUEUED"
设置答案: "QUEUED"
exec 答案: ["OK",[false,"ERR Operation against a key holding the wrong kind of value"]]
Redis 模块
此库支持 Redis 模块。以下是与 RedisBloom 模块的示例:
local cjson = require "cjson"
local redis = require "resty.redis"
-- 为 RedisBloom 注册模块前缀 "bf"
redis.register_module_prefix("bf")
local red = redis:new()
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("连接失败: ", err)
return
end
-- 使用前缀 'bf' 调用 BF.ADD 命令
res, err = red:bf():add("dog", 1)
if not res then
ngx.say(err)
return
end
ngx.say("接收: ", cjson.encode(res))
-- 调用 BF.EXISTS 命令
res, err = red:bf():exists("dog")
if not res then
ngx.say(err)
return
end
ngx.say("接收: ", cjson.encode(res))
负载均衡和故障转移
您可以轻松地在 Lua 中实现自己的 Redis 负载均衡逻辑。只需保持一个 Lua 表,包含所有可用的 Redis 后端信息(如主机名和端口号),并根据某些规则(如轮询或基于键的哈希)从 Lua 表中选择一个服务器。您可以在自己的 Lua 模块数据中跟踪当前规则状态,参见 https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker
同样,您可以在 Lua 中灵活地实现自动故障转移逻辑。
调试
通常使用 lua-cjson 库将 Redis 命令方法的返回值编码为 JSON 是很方便的。例如,
local cjson = require "cjson"
...
local res, err = red:mget("h1234", "h5678")
if res then
print("res: ", cjson.encode(res))
end
自动错误日志记录
默认情况下,底层的 ngx_lua 模块在发生套接字错误时会记录错误。如果您已经在自己的 Lua 代码中进行了适当的错误处理,则建议通过关闭 ngx_lua 的 lua_socket_log_errors 指令来禁用此自动错误日志记录,即,
lua_socket_log_errors off;
问题检查清单
- 确保您在 set_keepalive 中正确配置连接池大小。基本上,如果您的 Redis 可以处理
n个并发连接,而您的 NGINX 有m个工作进程,则连接池大小应配置为n/m。例如,如果您的 Redis 通常处理 1000 个并发请求,而您有 10 个 NGINX 工作进程,则连接池大小应为 100。类似地,如果您有p个不同的 NGINX 实例,则连接池大小应为n/m/p。 - 确保 Redis 端的 backlog 设置足够大。对于 Redis 2.8+,您可以直接在
redis.conf文件中调整tcp-backlog参数(并相应地调整内核参数SOMAXCONN,至少在 Linux 上)。您可能还想调整redis.conf中的maxclients参数。 - 确保您在 set_timeout 或 set_timeouts 方法中没有使用过短的超时设置。如果必须,请尝试在超时后重新执行操作并关闭 自动错误日志记录(因为您已经在自己的 Lua 代码中进行了适当的错误处理)。
- 如果您的 NGINX 工作进程在负载下 CPU 使用率非常高,则 NGINX 事件循环可能被 CPU 计算阻塞过多。尝试对典型的 NGINX 工作进程进行 C-land on-CPU Flame Graph 和 Lua-land on-CPU Flame Graph 进行采样。您可以根据这些 Flame Graph 优化 CPU 密集型操作。
- 如果您的 NGINX 工作进程在负载下 CPU 使用率非常低,则 NGINX 事件循环可能被某些阻塞系统调用(如文件 IO 系统调用)阻塞。您可以通过对典型的 NGINX 工作进程运行 epoll-loop-blocking-distr 工具来确认此问题。如果确实如此,您可以进一步对 NGINX 工作进程进行 C-land off-CPU Flame Graph 进行采样,以分析实际的阻塞因素。
- 如果您的
redis-server进程 CPU 使用率接近 100%,则应考虑通过多个节点扩展 Redis 后端,或使用 C-land on-CPU Flame Graph 工具 分析 Redis 服务器进程内部的瓶颈。
限制
- 此库不能在 init_by_lua、set_by_lua、log_by_lua 和 header_filter_by_lua 等代码上下文中使用,因为 ngx_lua cosocket API 不可用。
resty.redis对象实例不能在 Lua 模块级别存储在 Lua 变量中,因为它将被同一 Nginx 工作进程处理的所有并发请求共享(参见 https://github.com/openresty/lua-nginx-module/#data-sharing-within-an-nginx-worker),并导致在并发请求尝试使用同一resty.redis实例时出现不良竞争条件(您会看到“错误请求”或“套接字忙”错误从方法调用返回)。您应始终在函数局部变量或ngx.ctx表中初始化resty.redis对象。这些地方都有自己的数据副本,适用于每个请求。
克隆最新版本,假设为 v0.29
wget https://github.com/openresty/lua-resty-redis/archive/refs/tags/v0.29.tar.gz
解压
tar -xvzf v0.29.tar.gz
进入目录
cd lua-resty-redis-0.29
export LUA_LIB_DIR=/usr/local/openresty/site/lualib
编译和安装
make install
现在编译路径将被输出
/usr/local/lib/lua/resty = lua_package_path 在 nginx 配置中
```
另见
- ngx_lua 模块: https://github.com/openresty/lua-nginx-module/#readme
- Redis 有线协议规范: http://redis.io/topics/protocol
- lua-resty-memcached 库
- lua-resty-mysql 库
GitHub
您可以在 nginx-module-redis 的 GitHub 仓库 中找到此模块的其他配置技巧和文档。