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.14OpenResty 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 命令(如GETSET)相比,此命令没有任何特殊之处。因此,您只需在您的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_lualua_socket_log_errors 指令来禁用此自动错误日志记录,即

        lua_socket_log_errors off;

问题检查清单

  1. 确保在 set_keepalive 中正确配置连接池大小。基本上,如果您的 Redis 可以处理 n 个并发连接,并且您的 NGINX 有 m 个工作进程,则连接池大小应配置为 n/m。例如,如果您的 Redis 通常处理 1000 个并发请求,并且您有 10 个 NGINX 工作进程,则连接池大小应为 100。类似地,如果您有 p 个不同的 NGINX 实例,则连接池大小应为 n/m/p

  2. 确保 Redis 端的 backlog 设置足够大。对于 Redis 2.8+,您可以在 redis.conf 文件中直接调整 tcp-backlog 参数(并且至少在 Linux 上也相应地调整内核参数 SOMAXCONN)。您可能还需要调整 redis.conf 中的 maxclients 参数。

  3. 确保在 set_timeoutset_timeouts 方法中没有使用过短的超时设置。如果必须这样做,请尝试在超时后重做操作并关闭 "自动错误日志记录"(因为您已经在自己的 Lua 代码中进行了正确的错误处理)。

  4. 如果您的 NGINX 工作进程在负载下 CPU 使用率很高,则 NGINX 事件循环可能会被 CPU 计算阻塞过多。尝试对典型 NGINX 工作进程进行 C 端 CPU 上火焰图Lua 端 CPU 上火焰图 采样。您可以根据这些火焰图优化 CPU 密集型操作。

  5. 如果您的 NGINX 工作进程在负载下 CPU 使用率很低,则 NGINX 事件循环可能会被某些阻塞系统调用(如文件 IO 系统调用)阻塞。您可以通过对典型 NGINX 工作进程运行 epoll-loop-blocking-distr 工具来确认问题。如果是这种情况,则可以进一步对 NGINX 工作进程进行 C 端 CPU 外火焰图 采样以分析实际的阻塞因素。

  6. 如果您的 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 邮件列表供中文使用者使用。

错误和补丁

请通过以下方式报告错误或提交补丁:

  1. GitHub 问题跟踪器 上创建工单,

  2. 或发布到 "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

  • lua-resty-memcached

  • lua-resty-mysql

作者

章亦春 (agentzh)

许可证

2bsd

版本