跳转至

mysql: 非阻塞的 Lua MySQL 驱动库,用于 nginx-module-lua

安装

如果您尚未设置 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-mysql

CentOS/RHEL 8+、Fedora Linux、Amazon Linux 2023

dnf -y install https://extras.getpagespeed.com/release-latest.rpm
dnf -y install lua5.1-resty-mysql

要在 NGINX 中使用此 Lua 库,请确保已安装 nginx-module-lua

本文档描述了 lua-resty-mysql v0.31,于 2026 年 6 月 07 日发布。


此 Lua 库是 ngx_lua nginx 模块的 MySQL 客户端驱动:

https://github.com/openresty/lua-nginx-module

此 Lua 库利用了 ngx_lua 的 cosocket API,确保 100% 非阻塞行为。

请注意,至少需要 ngx_lua 0.9.11ngx_openresty 1.7.4.1

此外,还需要 bit 库。如果您在使用带有 ngx_lua 的 LuaJIT 2,则 bit 库默认已可用。

概述

    # 如果您使用的是
    # ngx_openresty 包,则不需要以下行:
    server {
        location /test {
            content_by_lua '
                local mysql = require "resty.mysql"
                local db, err = mysql:new()
                if not db then
                    ngx.say("实例化 mysql 失败: ", err)
                    return
                end

                db:set_timeout(1000) -- 1 秒

                -- 或连接到一个由 mysql 服务器监听的 unix 域套接字文件:
                --     local ok, err, errcode, sqlstate =
                --           db:connect{
                --              path = "/path/to/mysql.sock",
                --              database = "ngx_test",
                --              user = "ngx_test",
                --              password = "ngx_test" }

                local ok, err, errcode, sqlstate = db:connect{
                    host = "127.0.0.1",
                    port = 3306,
                    database = "ngx_test",
                    user = "ngx_test",
                    password = "ngx_test",
                    charset = "utf8",
                    max_packet_size = 1024 * 1024,
                }

                if not ok then
                    ngx.say("连接失败: ", err, ": ", errcode, " ", sqlstate)
                    db:close()
                    return
                end

                ngx.say("已连接到 mysql.")

                local res, err, errcode, sqlstate =
                    db:query("drop table if exists cats")
                if not res then
                    ngx.say("错误结果: ", err, ": ", errcode, ": ", sqlstate, ".")
                    db:close()
                    return
                end

                res, err, errcode, sqlstate =
                    db:query("create table cats "
                             .. "(id serial primary key, "
                             .. "name varchar(5))")
                if not res then
                    ngx.say("错误结果: ", err, ": ", errcode, ": ", sqlstate, ".")
                    db:close()
                    return
                end

                ngx.say("表 cats 创建成功.")

                res, err, errcode, sqlstate =
                    db:query("insert into cats (name) "
                             .. "values (\'Bob\'),(\'\'),(null)")
                if not res then
                    ngx.say("错误结果: ", err, ": ", errcode, ": ", sqlstate, ".")
                    db:close()
                    return
                end

                ngx.say(res.affected_rows, " 行已插入表 cats ",
                        "(最后插入 id: ", res.insert_id, ")")

                -- 运行一个选择查询,预期结果集中约有 10 行:
                res, err, errcode, sqlstate =
                    db:query("select * from cats order by id asc", 10)
                if not res then
                    ngx.say("错误结果: ", err, ": ", errcode, ": ", sqlstate, ".")
                    db:close()
                    return
                end

                local cjson = require "cjson"
                ngx.say("结果: ", cjson.encode(res))

                -- 将其放入大小为 100 的连接池中,
                -- 最大空闲超时为 10 秒
                local ok, err = db:set_keepalive(10000, 100)
                if not ok then
                    ngx.say("设置保持连接失败: ", err)
                    db:close()
                    return
                end

                -- 或立即关闭连接:
                -- local ok, err = db:close()
                -- if not ok then
                --     ngx.say("关闭失败: ", err)
                --     return
                -- end
            ';
        }
    }

方法

new

语法: db, err = mysql:new()

创建一个 MySQL 连接对象。如果失败,返回 nil 和描述错误的字符串。

connect

语法: ok, err, errcode, sqlstate = db:connect(options)

尝试连接到远程 MySQL 服务器。

options 参数是一个 Lua 表,包含以下键:

  • host

    MySQL 服务器的主机名。 * port

    MySQL 服务器监听的端口。默认为 3306。 * path

    MySQL 服务器监听的 unix 套接字文件的路径。 * database

    MySQL 数据库名称。 * user

    用于登录的 MySQL 账户名。 * password

    用于登录的 MySQL 账户密码(明文)。 * charset

    MySQL 连接使用的字符集,可以与默认字符集设置不同。 接受以下值:big5dec8cp850hp8koi8rlatin1latin2swe7asciiujissjishebrewtis620euckrkoi8ugb2312greekcp1250gbklatin5armscii8utf8ucs2cp866keybcs2maccemacromancp852latin7utf8mb4cp1251utf16utf16lecp1256cp1257utf32binarygeostd8cp932eucjpmsgb18030。 * max_packet_size

    从 MySQL 服务器发送的回复数据包的上限(默认为 1MB)。 * ssl

    如果设置为 true,则使用 SSL 连接到 MySQL(默认为 false)。如果 MySQL 服务器没有 SSL 支持(或仅被禁用),将返回错误字符串 "ssl disabled on server"。 * ssl_verify

    如果设置为 true,则验证服务器 SSL 证书的有效性(默认为 false)。 请注意,您需要配置 lua_ssl_trusted_certificate 来指定 MySQL 服务器使用的 CA(或服务器)证书。您可能还需要相应地配置 lua_ssl_verify_depth。 * pool

    MySQL 连接池的名称。如果省略,将自动生成一个模糊的池名称,使用字符串模板 user:database:host:portuser:database:path。(此选项在 v0.08 中首次引入。)

  • 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 才能使用此选项。

  • compact_arrays

    当此选项设置为 true 时,后续查询返回的 queryread_result 方法将返回数组的数组结构,而不是默认的哈希数组结构。

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

set_timeout

语法: db:set_timeout(time)

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

set_keepalive

语法: ok, err = db:set_keepalive(max_idle_timeout, pool_size)

立即将当前 MySQL 连接放入 ngx_lua cosocket 连接池中。

您可以指定连接在池中的最大空闲超时(以毫秒为单位)和每个 Nginx 工作进程的池的最大大小。

如果成功,返回 1。如果出错,返回 nil 和描述错误的字符串。

仅在您本来会调用 close 方法的地方调用此方法。调用此方法将立即将当前的 resty.mysql 对象转换为 closed 状态。对当前对象的任何后续操作(除了 connect())将返回 closed 错误。

get_reused_times

语法: times, err = db:get_reused_times()

此方法返回当前连接的(成功)重用次数。如果出错,返回 nil 和描述错误的字符串。

如果当前连接不是来自内置连接池,则此方法始终返回 0,即连接从未被重用(尚未)。如果连接来自连接池,则返回值始终非零。因此,此方法也可用于确定当前连接是否来自池。

close

语法: ok, err = db:close()

关闭当前 MySQL 连接并返回状态。

如果成功,返回 1。如果出错,返回 nil 和描述错误的字符串。

send_query

语法: bytes, err = db:send_query(query)

将查询发送到远程 MySQL 服务器,而不等待其回复。

在成功时返回成功发送的字节数,否则返回 nil 和描述错误的字符串。

您应该使用 read_result 方法随后读取 MySQL 的回复。

read_result

语法: res, err, errcode, sqlstate = db:read_result()

语法: res, err, errcode, sqlstate = db:read_result(nrows)

读取 MySQL 服务器返回的一个结果。

它返回一个 Lua 表(res),描述 MySQL 的 OK packet 或查询结果的 result set packet

对于对应于结果集的查询,它返回一个数组,包含所有行。每行包含每个数据字段的键值对。例如,

    {
        { name = "Bob", age = 32, phone = ngx.null },
        { name = "Marry", age = 18, phone = "10666372"}
    }

对于不对应于结果集的查询,它返回一个类似于以下的 Lua 表:

    {
        insert_id = 0,
        server_status = 2,
        warning_count = 1,
        affected_rows = 32,
        message = nil
    }

如果当前结果后面还有更多结果,则第二个 err 返回值将给出字符串 again。应该始终检查此(第二个)返回值,如果是 again,则应再次调用此方法以检索更多结果。这通常发生在原始查询包含多个语句(在同一查询字符串中用分号分隔)或调用 MySQL 存储过程时。另请参见 多结果集支持

如果出错,此方法最多返回 4 个值:nilerrerrcodesqlstateerr 返回值包含描述错误的字符串,errcode 返回值保存 MySQL 错误代码(数值),最后,sqlstate 返回值包含由 5 个字符组成的标准 SQL 错误代码。请注意,如果 MySQL 不返回它们,则 errcodesqlstate 可能为 nil

可选参数 nrows 可用于指定结果集的近似行数。此值可用于在结果 Lua 表中预分配空间。默认值为 4。

query

语法: res, err, errcode, sqlstate = db:query(query)

语法: res, err, errcode, sqlstate = db:query(query, nrows)

这是将 send_query 调用与第一次 read_result 调用结合的快捷方式。

在成功的情况下,您应该始终检查 err 返回值是否为 again,因为此方法只会为您调用 read_result 一次。另请参见 多结果集支持

server_ver

语法: str = db:server_ver()

返回 MySQL 服务器版本字符串,如 "5.1.64"

您应该仅在成功连接到 MySQL 服务器后调用此方法,否则将返回 nil

set_compact_arrays

语法: db:set_compact_arrays(boolean)

设置是否使用“紧凑数组”结构来返回后续查询的结果集。有关更多详细信息,请参见 connect 方法的 compact_arrays 选项。

此方法在 v0.09 版本中首次引入。

SQL 字面量引用

正确引用 SQL 字面量以防止 SQL 注入攻击始终很重要。您可以使用 ngx_lua 提供的 ngx.quote_sql_str 函数来引用值。 以下是一个示例:

    local name = ngx.unescape_uri(ngx.var.arg_name)
    local quoted_name = ngx.quote_sql_str(name)
    local sql = "select * from users where name = " .. quoted_name

多结果集支持

对于生成多个结果集的 SQL 查询,您始终有责任检查 queryread_result 方法调用返回的 "again" 错误消息,并通过调用 read_result 方法继续提取更多结果集,直到不再返回 "again" 错误消息(或发生其他错误)。

以下是一个简单的示例:

    local cjson = require "cjson"
    local mysql = require "resty.mysql"

    local db = mysql:new()
    local ok, err, errcode, sqlstate = db:connect({
        host = "127.0.0.1",
        port = 3306,
        database = "world",
        user = "monty",
        password = "pass"})

    if not ok then
        ngx.log(ngx.ERR, "连接失败: ", err, ": ", errcode, " ", sqlstate)
        return ngx.exit(500)
    end

    res, err, errcode, sqlstate = db:query("select 1; select 2; select 3;")
    if not res then
        ngx.log(ngx.ERR, "错误结果 #1: ", err, ": ", errcode, ": ", sqlstate, ".")
        db:close()
        return ngx.exit(500)
    end

    ngx.say("结果 #1: ", cjson.encode(res))

    local i = 2
    while err == "again" do
        res, err, errcode, sqlstate = db:read_result()
        if not res then
            ngx.log(ngx.ERR, "错误结果 #", i, ": ", err, ": ", errcode, ": ", sqlstate, ".")
            db:close()
            return ngx.exit(500)
        end

        ngx.say("结果 #", i, ": ", cjson.encode(res))
        i = i + 1
    end

    local ok, err = db:set_keepalive(10000, 50)
    if not ok then
        ngx.log(ngx.ERR, "设置保持连接失败: ", err)
        db:close()
        ngx.exit(500)
    end

此代码片段将生成以下响应主体数据:

结果 #1: [{"1":"1"}]
结果 #2: [{"2":"2"}]
结果 #3: [{"3":"3"}]

调试

通常使用 lua-cjson 库将 MySQL 查询方法的返回值编码为 JSON 是很方便的。例如,

    local cjson = require "cjson"
    ...
    local res, err, errcode, sqlstate = db:query("select * from cats")
    if res then
        print("res: ", cjson.encode(res))
    end

自动错误日志记录

默认情况下,底层的 ngx_lua 模块在发生套接字错误时会记录错误。如果您已经在自己的 Lua 代码中进行了适当的错误处理,则建议通过关闭 ngx_lualua_socket_log_errors 指令来禁用此自动错误日志记录,即,

    lua_socket_log_errors off;

限制

  • 此库不能在 init_by_lua、set_by_lua、log_by_lua 和 header_filter_by_lua 等代码上下文中使用,因为 ngx_lua cosocket API 不可用。
  • resty.mysql 对象实例不能在 Lua 模块级别存储在 Lua 变量中,因为它将被同一 nginx 工作进程处理的所有并发请求共享(参见 https://github.com/openresty/lua-nginx-module#data-sharing-within-an-nginx-worker),并导致在并发请求尝试使用同一 resty.mysql 实例时出现不良竞争条件。您应始终在函数局部变量或 ngx.ctx 表中初始化 resty.mysql 对象。这些地方都有各自的请求数据副本。

更多身份验证方法支持

默认情况下,所有身份验证方法中,仅支持 旧密码身份验证(mysql_old_password)安全密码身份验证(mysql_native_password)。如果服务器要求 sha256_password 或 cache_sha2_password,可能会返回类似 auth plugin caching_sha2_password or sha256_password are not supported because resty.rsa is not installed 的错误。

使用 sha256_passwordcache_sha2_password 时需要 lua-resty-rsa

对于 MariaDB 的 client_ed25519 身份验证插件(在许多最近的 MariaDB 部署中默认使用,自 MariaDB 10.1.22 起可用),需要 lua-resty-openssl。不需要额外的系统依赖 — 它重用了 OpenResty 已经链接的 libcrypto。每次签名的成本大约为 25 毫秒,按连接支付一次。

背景:此 MariaDB 插件中的 Ed25519 签名原语直接使用 SHA-512(password) 作为扩展的密钥,跳过 RFC 8032 种子派生步骤。因此,OpenSSL/libsodium 中的高级 Ed25519 API 不能直接使用。此驱动程序在 OpenSSL 的 BIGNUM 模块(resty.openssl.bn)之上实现了爱德华曲线点算术 — 字段/组算术通过 libcrypto 进行,而点加法和标量乘法则直接实现,以便我们可以输入 MariaDB 特定的扩展密钥。

另见

GitHub

您可以在 nginx-module-mysql 的 GitHub 仓库 中找到此模块的其他配置提示和文档。