lua-resty-redis
基于 cosocket API 的 ngx_lua Lua Redis 客户端驱动
$ opm get valdemaras-pipiras/lua-resty-redis
名称
lua-resty-redis - 基于 cosocket API 的 ngx_lua Lua Redis 客户端驱动
状态
此库被认为已准备好用于生产环境。
描述
此 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。
概要
# you do not need the following line if you are using
# the OpenResty bundle:
lua_package_path "/path/to/lua-resty-redis/lib/?.lua;;";
server {
location /test {
-- need to specify the resolver to resolve the hostname
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 sec
-- or connect to a unix domain socket file listened
-- by a redis server:
-- local ok, err = red:connect("unix:/path/to/redis.sock")
-- connect via ip address directly
local ok, err = red:connect("127.0.0.1", 6379)
-- or connect via hostname, need to specify resolver just like above
local ok, err = red:connect("redis.openresty.com", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
ok, err = red:set("dog", "an animal")
if not ok then
ngx.say("failed to set dog: ", err)
return
end
ngx.say("set result: ", ok)
local res, err = red:get("dog")
if not res then
ngx.say("failed to get dog: ", err)
return
end
if res == ngx.null then
ngx.say("dog not found.")
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("failed to commit the pipelined requests: ", err)
return
end
for i, res in ipairs(results) do
if type(res) == "table" then
if res[1] == false then
ngx.say("failed to run command ", i, ": ", res[2])
else
-- process the table value
end
else
-- process the scalar value
end
end
-- put it into the connection pool of size 100,
-- with 10 seconds max idle time
local ok, err = red:set_keepalive(10000, 100)
if not ok then
ngx.say("failed to set keepalive: ", err)
return
end
-- or just close the connection right away:
-- local ok, err = red:close()
-- if not ok then
-- ngx.say("failed to close: ", err)
-- return
-- end
}
}
}
方法
所有 Redis 命令都有自己的方法,名称相同,但全部小写。
您可以在此处找到 Redis 命令的完整列表
https://redis.ac.cn/commands
您需要查看此 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 回复类型的详细信息,请参阅https://redis.ac.cn/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
在通过 SSL 连接时,为新的 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 发布/订阅 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 sec
red2:set_timeouts(1000, 1000, 1000) -- 1 sec
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("1: failed to connect: ", err)
return
end
ok, err = red2:connect("127.0.0.1", 6379)
if not ok then
ngx.say("2: failed to connect: ", err)
return
end
local res, err = red:subscribe("dog")
if not res then
ngx.say("1: failed to subscribe: ", err)
return
end
ngx.say("1: subscribe: ", cjson.encode(res))
res, err = red2:publish("dog", "Hello")
if not res then
ngx.say("2: failed to publish: ", err)
return
end
ngx.say("2: publish: ", cjson.encode(res))
res, err = red:read_reply()
if not res then
ngx.say("1: failed to read reply: ", err)
return
end
ngx.say("1: receive: ", cjson.encode(res))
red:close()
red2:close()
运行此示例将给出如下输出
1: subscribe: ["subscribe","dog",1]
2: publish: 1
1: receive: ["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 sec
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = red:foo("a")
if not res then
ngx.say("failed to foo: ", err)
end
res, err = red:bar()
if not res then
ngx.say("failed to bar: ", err)
end
Redis 身份验证
Redis 使用AUTH
命令进行身份验证:https://redis.ac.cn/commands/auth
与其他 Redis 命令(如GET
和SET
)相比,此命令没有任何特殊之处。因此,您只需在您的resty.redis
实例上调用auth
方法即可。这是一个示例
local redis = require "resty.redis"
local red = redis:new()
red:set_timeouts(1000, 1000, 1000) -- 1 sec
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local res, err = red:auth("foobared")
if not res then
ngx.say("failed to authenticate: ", err)
return
end
我们假设 Redis 服务器在redis.conf
文件中配置了密码foobared
requirepass foobared
如果指定的密码错误,则上述示例将向 HTTP 客户端输出以下内容
failed to authenticate: 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 sec
local ok, err = red:connect("127.0.0.1", 6379)
if not ok then
ngx.say("failed to connect: ", err)
return
end
local ok, err = red:multi()
if not ok then
ngx.say("failed to run multi: ", err)
return
end
ngx.say("multi ans: ", cjson.encode(ok))
local ans, err = red:set("a", "abc")
if not ans then
ngx.say("failed to run sort: ", err)
return
end
ngx.say("set ans: ", cjson.encode(ans))
local ans, err = red:lpop("a")
if not ans then
ngx.say("failed to run sort: ", err)
return
end
ngx.say("set ans: ", cjson.encode(ans))
ans, err = red:exec()
ngx.say("exec ans: ", cjson.encode(ans))
red:close()
然后输出将是
multi ans: "OK"
set ans: "QUEUED"
set ans: "QUEUED"
exec ans: ["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"
-- register the module prefix "bf" for RedisBloom
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("failed to connect: ", err)
return
end
-- call BF.ADD command with the prefix 'bf'
res, err = red:bf():add("dog", 1)
if not res then
ngx.say(err)
return
end
ngx.say("receive: ", cjson.encode(res))
-- call BF.EXISTS command
res, err = red:bf():exists("dog")
if not res then
ngx.say(err)
return
end
ngx.say("receive: ", 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
参数(并且至少在 Linux 上也相应地调整内核参数SOMAXCONN
)。您可能还需要调整redis.conf
中的maxclients
参数。确保在 set_timeout 或 set_timeouts 方法中没有使用过短的超时设置。如果必须这样做,请尝试在超时后重做操作并关闭 "自动错误日志记录"(因为您已经在自己的 Lua 代码中进行了正确的错误处理)。
如果您的 NGINX 工作进程在负载下 CPU 使用率很高,则 NGINX 事件循环可能会被 CPU 计算阻塞过多。尝试对典型 NGINX 工作进程进行 C 端 CPU 上火焰图 和 Lua 端 CPU 上火焰图 采样。您可以根据这些火焰图优化 CPU 密集型操作。
如果您的 NGINX 工作进程在负载下 CPU 使用率很低,则 NGINX 事件循环可能会被某些阻塞系统调用(如文件 IO 系统调用)阻塞。您可以通过对典型 NGINX 工作进程运行 epoll-loop-blocking-distr 工具来确认问题。如果是这种情况,则可以进一步对 NGINX 工作进程进行 C 端 CPU 外火焰图 采样以分析实际的阻塞因素。
如果您的
redis-server
进程 CPU 使用率接近 100%,则应考虑通过多个节点扩展您的 Redis 后端或使用 C 端 CPU 上火焰图工具 分析 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
对象。这些位置在每个请求中都拥有各自的数据副本。
安装
如果您使用的是 OpenResty 捆绑包 (https://openresty.org.cn),则无需执行任何操作,因为它默认包含并启用了 lua-resty-redis。您只需在 Lua 代码中使用它,如
local redis = require "resty.redis"
...
如果您使用的是您自己的 nginx + ngx_lua 构建,则需要配置 lua_package_path 指令以将 lua-resty-redis 源代码树的路径添加到 ngx_lua 的 LUA_PATH 搜索路径中,如
# nginx.conf
http {
lua_package_path "/path/to/lua-resty-redis/lib/?.lua;;";
...
}
确保运行 Nginx “工作进程”的系统帐户具有读取 .lua
文件的足够权限。
待办事项
社区
英文邮件列表
openresty-en 邮件列表供英语使用者使用。
中文邮件列表
openresty 邮件列表供中文使用者使用。
错误和补丁
请通过以下方式报告错误或提交补丁:
在 GitHub 问题跟踪器 上创建工单,
或发布到 "OpenResty 社区"。
作者
章亦春 (agentzh) <agentzh@gmail.com>,OpenResty Inc.
版权和许可证
此模块根据 BSD 许可证授权。
版权所有 (C) 2012-2017,由章亦春 (agentzh) <agentzh@gmail.com>,OpenResty Inc. 所有。
保留所有权利。
在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用,无论是否修改:
源代码的再分发必须保留上述版权声明、此条件列表和以下免责声明。
二进制形式的再分发必须在随发行版提供的文档和/或其他材料中复制上述版权声明、此条件列表和以下免责声明。
本软件由版权持有人和贡献者“按原样”提供,并且不提供任何明示或暗示的担保,包括但不限于适销性和特定用途适用性的暗示担保。在任何情况下,版权持有人或贡献者均不对任何直接、间接、附带、特殊、惩罚性或后果性损害(包括但不限于替代商品或服务的采购;使用、数据或利润损失;或业务中断)负责,无论这些损害是因使用本软件而引起的,还是基于任何责任理论,无论是基于合同、严格责任或侵权行为(包括疏忽或其他原因),即使已被告知此类损害的可能性。
另请参阅
ngx_lua 模块:https://github.com/openresty/lua-nginx-module/#readme
redis 有线协议规范:https://redis.ac.cn/topics/protocol
作者
章亦春 (agentzh)
许可证
2bsd
版本
-
基于 cosocket API 的 ngx_lua 的 Lua redis 客户端驱动程序 2022-10-06 14:18:02