lua-resty-redis

基于 cosocket API 的 ngx_lua Lua redis 客户端驱动

$ opm get openresty/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 {
                content_by_lua_block {
                    local redis = require "resty.redis"
                    local red = redis:new()
    
                    red:set_timeout(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")
    
                    local ok, err = red:connect("127.0.0.1", 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 域套接字文件。

在实际解析主机名并连接到远程后端之前,此方法将始终查找与先前对此方法的调用创建的空闲连接匹配的连接池。

可以将可选的 Lua 表格指定为此方法的最后一个参数,以指定各种连接选项

  • pool

    指定正在使用的连接池的自定义名称。如果省略,则连接池名称将从字符串模板<host>:<port><unix-socket-path>生成。

set_timeout

语法:red:set_timeout(time)

设置后续操作(包括connect方法)的超时(以毫秒为单位)保护。

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

语法:red:hmset(myhash, field1, value1, field2, value2, ...)

语法: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_timeout(1000) -- 1 sec
        red2:set_timeout(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_timeout(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_timeout(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_timeout(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"]]

负载均衡和故障转移

您可以使用 Lua 轻松地实现自己的 Redis 负载均衡逻辑。只需保留一个包含所有可用 Redis 后端信息(如主机名和端口号)的 Lua 表格,并在每次请求时根据某些规则(如循环或基于键的哈希)从 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中正确配置了连接池大小。基本上,如果您的 NGINX 处理n个并发请求并且您的 NGINX 有m个工作进程,则应将连接池大小配置为n/m。例如,如果您的 NGINX 通常处理 1000 个并发请求并且您有 10 个 NGINX 工作进程,则连接池大小应为 100。

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

  3. 确保您在set_timeout方法中没有使用过短的超时设置。如果您必须这样做,请尝试在超时时重做操作并关闭“自动错误日志记录”(因为您已经在自己的 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-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对象。这些位置在每个请求中都有自己的数据副本。

安装

如果您使用的是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) <[email protected]>,OpenResty Inc.

版权和许可

此模块根据BSD许可证授权。

版权所有 (C) 2012-2017,由章亦春(agentzh) <[email protected]>,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

版本