跳转至

lock: 基于共享内存字典的简单非阻塞锁 API,用于 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-lock

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

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

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

本文档描述了 lua-resty-lock v0.9,发布于 2022 年 6 月 17 日。


## nginx.conf

http {
    # 如果您使用的是 OpenResty 包,则不需要以下行:
    lua_shared_dict my_locks 100k;

    server {
        ...

        location = /t {
            content_by_lua '
                local resty_lock = require "resty.lock"
                for i = 1, 2 do
                    local lock, err = resty_lock:new("my_locks")
                    if not lock then
                        ngx.say("创建锁失败: ", err)
                    end

                    local elapsed, err = lock:lock("my_key")
                    ngx.say("锁定时间: ", elapsed, ", ", err)

                    local ok, err = lock:unlock()
                    if not ok then
                        ngx.say("解锁失败: ", err)
                    end
                    ngx.say("解锁: ", ok)
                end
            ';
        }
    }
}

描述

此库以类似于 ngx_proxy 模块的 proxy_cache_lock 指令 的方式实现了一个简单的互斥锁。

在底层,此库使用 ngx_lua 模块的共享内存字典。锁等待是非阻塞的,因为我们使用逐步的 ngx.sleep 来定期轮询锁。

方法

要加载此库,

  1. 您需要在 ngx_lua 的 lua_package_path 指令中指定此库的路径。例如,lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";
  2. 您使用 require 将库加载到一个本地 Lua 变量中:
    local lock = require "resty.lock"

new

语法: obj, err = lock:new(dict_name)

语法: obj, err = lock:new(dict_name, opts)

通过指定共享字典名称(由 lua_shared_dict 创建)和可选的选项表 opts 创建一个新的锁对象实例。

如果失败,则返回 nil 和描述错误的字符串。

选项表接受以下选项:

  • exptime 指定共享内存字典中锁条目的过期时间(以秒为单位)。您可以指定最多 0.001 秒。默认为 30(秒)。即使调用者未调用 unlock 或持有锁的对象未被 GC,锁也将在此时间后释放。因此,即使持有锁的工作进程崩溃,也不会发生死锁。
  • timeout 指定当前对象实例上 lock 方法调用的最大等待时间(以秒为单位)。您可以指定最多 0.001 秒。默认为 5(秒)。此选项值不能大于 exptime。此超时用于防止 lock 方法调用无限期等待。 您可以指定 0 使 lock 方法立即返回而不等待,如果它无法立即获取锁。
  • step 指定等待锁时的初始睡眠步长(以秒为单位)。默认为 0.001(秒)。当 lock 方法在一个繁忙的锁上等待时,它会按步长睡眠。步长大小按比例(由 ratio 选项指定)增加,直到达到步长大小限制(由 max_step 选项指定)。
  • ratio 指定步长增加的比例。默认为 2,即每次等待迭代步长大小翻倍。
  • max_step 指定允许的最大步长(即睡眠间隔,以秒为单位)。另见 stepratio 选项。默认为 0.5(秒)。

lock

语法: elapsed, err = obj:lock(key)

尝试在当前 Nginx 服务器实例的所有 Nginx 工作进程中锁定一个键。不同的键是不同的锁。

键字符串的长度不得大于 65535 字节。

如果成功获取锁,则返回等待时间(以秒为单位)。否则返回 nil 和描述错误的字符串。

等待时间不是来自实时时钟,而是简单地将所有等待“步骤”相加。非零的 elapsed 返回值表示其他人刚刚持有此锁。但零返回值不能保证没有其他人刚刚获取并释放锁。

当此方法在获取锁时等待时,操作系统线程不会被阻塞,当前 Lua “轻线程”将在后台自动让出。

强烈建议始终调用 unlock() 方法以尽快主动释放锁。

如果在此方法调用后从未调用 unlock() 方法,则锁将在以下情况下释放:

  1. 当前 resty.lock 对象实例被 Lua GC 自动回收。
  2. 锁条目的 exptime 达到。

此方法调用的常见错误包括: * "timeout" : 超过了 new 方法的 timeout 选项指定的超时阈值。 * "locked" : 当前 resty.lock 对象实例已经持有一个锁(不一定是同一个键)。

其他可能的错误来自 ngx_lua 的共享字典 API。

对于多个同时锁定(即围绕不同键的锁),必须为不同的 resty.lock 实例创建。

unlock

语法: ok, err = obj:unlock()

释放当前 resty.lock 对象实例持有的锁。

成功时返回 1。否则返回 nil 和描述错误的字符串。

如果在没有持有锁的情况下调用 unlock,将返回错误 "unlocked"。

expire

语法: ok, err = obj:expire(timeout)

设置当前 resty.lock 对象实例持有的锁的 TTL。如果给定,将把锁的超时重置为 timeout 秒,否则将使用调用 new 时提供的 timeout

请注意,此函数中提供的 timeout 与调用 new 时提供的 timeout 是独立的。调用 expire() 不会更改 new 中指定的 timeout 值,后续的 expire(nil) 调用仍将使用 new 中的 timeout 数字。

成功时返回 true。否则返回 nil 和描述错误的字符串。

如果在没有持有锁的情况下调用 expire,将返回错误 "unlocked"。

对于多个 Lua 轻线程

在多个 ngx_lua “轻线程”之间共享单个 resty.lock 对象实例始终是一个坏主意,因为该对象本身是有状态的,并且容易受到竞争条件的影响。强烈建议始终为每个需要的“轻线程”分配一个单独的 resty.lock 对象实例。

对于缓存锁

此库的一个常见用例是避免所谓的“狗堆效应”,即在发生缓存未命中时限制对同一键的并发后端查询。这种用法类似于标准 ngx_proxy 模块的 proxy_cache_lock 指令。

缓存锁的基本工作流程如下:

  1. 检查缓存是否命中该键。如果发生缓存未命中,继续到步骤 2。
  2. 实例化一个 resty.lock 对象,在键上调用 lock 方法,并检查第一个返回值,即锁等待时间。如果为 nil,处理错误;否则继续到步骤 3。
  3. 再次检查缓存是否命中。如果仍然未命中,继续到步骤 4;否则通过调用 unlock 释放锁,然后返回缓存的值。
  4. 查询后端(数据源)以获取值,将结果放入缓存,然后通过调用 unlock 释放当前持有的锁。

以下是一个演示该思想的完整代码示例。

    local resty_lock = require "resty.lock"
    local cache = ngx.shared.my_cache

    -- 步骤 1:
    local val, err = cache:get(key)
    if val then
        ngx.say("结果: ", val)
        return
    end

    if err then
        return fail("从 shm 获取键失败: ", err)
    end

    -- 缓存未命中!
    -- 步骤 2:
    local lock, err = resty_lock:new("my_locks")
    if not lock then
        return fail("创建锁失败: ", err)
    end

    local elapsed, err = lock:lock(key)
    if not elapsed then
        return fail("获取锁失败: ", err)
    end

    -- 锁成功获取!

    -- 步骤 3:
    -- 可能有人已经将值放入缓存
    -- 所以我们在这里再次检查:
    val, err = cache:get(key)
    if val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("解锁失败: ", err)
        end

        ngx.say("结果: ", val)
        return
    end

    --- 步骤 4:
    local val = fetch_redis(key)
    if not val then
        local ok, err = lock:unlock()
        if not ok then
            return fail("解锁失败: ", err)
        end

        -- FIXME: 我们应该更仔细地处理后端未命中
        -- 这里,比如将一个占位值插入缓存。

        ngx.say("未找到值")
        return
    end

    -- 用新获取的值更新 shm 缓存
    local ok, err = cache:set(key, val, 1)
    if not ok then
        local ok, err = lock:unlock()
        if not ok then
            return fail("解锁失败: ", err)
        end

        return fail("更新 shm 缓存失败: ", err)
    end

    local ok, err = lock:unlock()
    if not ok then
        return fail("解锁失败: ", err)
    end

    ngx.say("结果: ", val)

在这里,我们假设使用 ngx_lua 共享内存字典来缓存 Redis 查询结果,并在 nginx.conf 中有以下配置:

    # 您可能想根据您的情况更改字典大小。
    lua_shared_dict my_cache 10m;
    lua_shared_dict my_locks 1m;

my_cache 字典用于数据缓存,而 my_locks 字典用于 resty.lock 本身。

在上面的示例中需要注意几个重要事项:

  1. 即使发生其他不相关的错误,也需要尽快释放锁。
  2. 在释放锁之前,您需要用从后端获得的结果更新缓存,以便其他已经在等待锁的线程可以在之后获取锁时获得缓存值。
  3. 当后端根本不返回值时,我们应该小心处理这种情况,通过将一些占位值插入缓存。

限制

此库的一些 API 函数可能会让出控制权。因此,请勿在不支持让出的 ngx_lua 模块上下文中调用这些函数,例如 init_by_lua*init_worker_by_lua*header_filter_by_lua*body_filter_by_lua*balancer_by_lua*log_by_lua*

先决条件

另见

GitHub

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