lua-resty-lock
基于共享内存的简单非阻塞锁 API
$ opm get openresty/lua-resty-lock
名称
lua-resty-lock - 基于共享内存的简单非阻塞锁 API
状态
该库仍处于早期开发阶段,但已可用于生产环境。
概要
# nginx.conf
http {
# you do not need the following line if you are using the
# OpenResty bundle:
lua_package_path "/path/to/lua-resty-core/lib/?.lua;/path/to/lua-resty-lock/lib/?.lua;;";
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("failed to create lock: ", err)
end
local elapsed, err = lock:lock("my_key")
ngx.say("lock: ", elapsed, ", ", err)
local ok, err = lock:unlock()
if not ok then
ngx.say("failed to unlock: ", err)
end
ngx.say("unlock: ", ok)
end
';
}
}
}
描述
该库以类似于 ngx_proxy 模块的 proxy_cache_lock 指令 的方式实现了简单的互斥锁。
在底层,该库使用 ngx_lua 模块的共享内存字典。锁等待是非阻塞的,因为我们使用逐步的 ngx.sleep 来定期轮询锁。
方法
要加载该库,
您需要在 ngx_lua 的 lua_package_path 指令中指定该库的路径。例如,
lua_package_path "/path/to/lua-resty-lock/lib/?.lua;;";
。您可以使用
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 收集,锁也会在一段时间后被释放。因此,即使持有锁的 worker 进程崩溃,也不会发生死锁。timeout
指定当前对象实例上的 lock 方法调用最大等待时间(以秒为单位)。您可以指定最长0.001
秒。默认为 5(秒)。此选项值不能大于exptime
。此超时是为了防止 lock 方法调用无限期等待。您可以指定0
以使 lock 方法在无法立即获取锁的情况下立即返回。step
指定等待锁时睡眠的初始步骤(以秒为单位)。默认为0.001
(秒)。当 lock 方法正在等待一个繁忙的锁时,它会以步骤进行睡眠。步骤大小会按比例 (由ratio
选项指定) 增加,直到达到步骤大小限制 (由max_step
选项指定)。ratio
指定步骤增加比例。默认为 2,也就是说,在每次等待迭代中,步骤大小加倍。max_step
指定允许的最大步骤大小(即睡眠间隔,以秒为单位)。另请参见step
和ratio
选项。默认为 0.5(秒)。
lock
语法:elapsed, err = obj:lock(key)
尝试在当前 Nginx 服务器实例中的所有 Nginx worker 进程中锁定一个键。不同的键是不同的锁。
键字符串的长度不得超过 65535 字节。
如果锁被成功获取,则返回等待时间(以秒为单位)。否则返回 nil
和一个描述错误的字符串。
等待时间不是来自时钟,而是简单地将所有等待“步骤”加起来。非零的 elapsed
返回值表示其他人刚刚持有此锁。但是,零返回值不能保证没有其他人刚刚获取并释放了锁。
当此方法正在等待获取锁时,不会阻塞任何操作系统线程,并且当前的 Lua“轻量级线程”将在后台自动让出。
强烈建议始终调用 unlock() 方法尽快主动释放锁。
如果在调用此方法后从未调用过 unlock() 方法,锁将在以下情况下被释放
当前的
resty.lock
对象实例被 Lua GC 自动收集。锁条目的
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 指令。
缓存锁的基本工作流程如下
使用键检查缓存是否命中。如果发生缓存未命中,请继续执行步骤 2。
实例化一个
resty.lock
对象,在键上调用 lock 方法,并检查第一个返回值,即锁等待时间。如果它是nil
,则处理错误;否则继续执行步骤 3。再次检查缓存是否命中。如果仍然是未命中,则继续执行步骤 4;否则通过调用 unlock 释放锁,然后返回缓存的值。
查询后端(数据源)获取值,将结果放入缓存,然后通过调用 unlock 释放当前持有的锁。
下面是一个演示了这个想法的比较完整的代码示例。
local resty_lock = require "resty.lock"
local cache = ngx.shared.my_cache
-- step 1:
local val, err = cache:get(key)
if val then
ngx.say("result: ", val)
return
end
if err then
return fail("failed to get key from shm: ", err)
end
-- cache miss!
-- step 2:
local lock, err = resty_lock:new("my_locks")
if not lock then
return fail("failed to create lock: ", err)
end
local elapsed, err = lock:lock(key)
if not elapsed then
return fail("failed to acquire the lock: ", err)
end
-- lock successfully acquired!
-- step 3:
-- someone might have already put the value into the cache
-- so we check it here again:
val, err = cache:get(key)
if val then
local ok, err = lock:unlock()
if not ok then
return fail("failed to unlock: ", err)
end
ngx.say("result: ", val)
return
end
--- step 4:
local val = fetch_redis(key)
if not val then
local ok, err = lock:unlock()
if not ok then
return fail("failed to unlock: ", err)
end
-- FIXME: we should handle the backend miss more carefully
-- here, like inserting a stub value into the cache.
ngx.say("no value found")
return
end
-- update the shm cache with the newly fetched value
local ok, err = cache:set(key, val, 1)
if not ok then
local ok, err = lock:unlock()
if not ok then
return fail("failed to unlock: ", err)
end
return fail("failed to update shm cache: ", err)
end
local ok, err = lock:unlock()
if not ok then
return fail("failed to unlock: ", err)
end
ngx.say("result: ", val)
这里假设我们使用 ngx_lua 共享内存字典来缓存 Redis 查询结果,并且我们在 nginx.conf
中有以下配置
# you may want to change the dictionary size for your cases.
lua_shared_dict my_cache 10m;
lua_shared_dict my_locks 1m;
my_cache
字典用于数据缓存,而 my_locks
字典用于 resty.lock
本身。
在上面的示例中需要注意的几个重要事项
即使发生其他无关错误,您也需要尽快释放锁。
您需要使用从后端获取的结果在释放锁之前更新缓存,以便其他已经等待锁的线程在之后获取锁时可以获取缓存值。
当后端根本不返回值时,我们应该通过将一些存根值插入缓存来谨慎处理这种情况。
限制
该库的一些 API 函数可能会让出。因此,不要在不支持让出的 ngx_lua
模块上下文中调用这些函数(目前),例如 init_by_lua*
、init_worker_by_lua*
、header_filter_by_lua*
、body_filter_by_lua*
、balancer_by_lua*
和 log_by_lua*
。
先决条件
安装
建议直接使用最新的 OpenResty 包,该包默认情况下捆绑了此库并已启用。至少需要 OpenResty 1.4.2.9。在构建 OpenResty 包时,您需要通过向其 ./configure
脚本传递 --with-luajit
选项来启用 LuaJIT。不需要额外的 Nginx 配置。
如果您想将此库与您自己的 Nginx 构建(使用 ngx_lua)一起使用,那么您需要确保您至少使用 ngx_lua 0.8.10。此外,您需要配置 lua_package_path 指令,将您的 lua-resty-lock 和 lua-resty-core 源目录的路径添加到 ngx_lua 的 Lua 模块搜索路径中,如
# nginx.conf
http {
lua_package_path "/path/to/lua-resty-lock/lib/?.lua;/path/to/lua-resty-core/lib/?.lua;;";
...
}
然后在 Lua 中加载库
local resty_lock = require "resty.lock"
请注意,此库依赖于 lua-resty-core 库,该库默认情况下也在 OpenResty 包中启用。
待办事项
当 LuaJIT 2.1 支持普通 Lua 表上的
__gc
元方法时,我们应该简化当前的实现。现在,我们使用一个 FFI cdata 和一个 ref/unref 备忘表来解决这个问题,这相当丑陋,效率也不高。
社区
英文邮件列表
您可以访问 openresty-en 邮件列表与英语使用者交流。
中文邮件列表
您可以访问 openresty 邮件列表与中文使用者交流。
错误和补丁
请通过以下方式报告错误或提交补丁:
在 GitHub 问题跟踪器 上创建一个工单,
或发布到 "OpenResty 社区"。
作者
Yichun "agentzh" Zhang (章亦春) <[email protected]>,OpenResty Inc.
版权和许可
此模块在 BSD 许可下授权。
版权所有 (C) 2013-2019,由 Yichun "agentzh" Zhang,OpenResty Inc.
保留所有权利。
在满足以下条件的情况下,允许以源代码和二进制形式重新分发和使用此软件,无论是否修改:
源代码的再分发必须保留上述版权声明、此条件列表和以下免责声明。
二进制形式的再分发必须在随分发提供的文档和/或其他材料中复制上述版权声明、此条件列表和以下免责声明。
此软件由版权所有者和贡献者“按现状”提供,不提供任何明示或暗示的担保,包括但不限于适销性、特定目的适用性的暗示担保。在任何情况下,版权所有者或贡献者均不对任何直接、间接、附带、特殊、示范性或后果性损害(包括但不限于替代商品或服务的采购;使用、数据或利润损失;或业务中断)负责,无论其是否基于任何责任理论,无论是合同、严格责任还是侵权行为(包括疏忽或其他原因),即使已被告知可能发生此类损害。
另请参见
ngx_lua 模块:https://github.com/openresty/lua-nginx-module
OpenResty:https://openresty.org.cn
作者
Yichun Zhang (agentzh)
许可
2bsd
依赖关系
luajit
版本
-
基于共享内存的非阻塞锁API 2020-04-03 08:55:31
-
基于共享内存的非阻塞锁API 2017-08-08 22:07:12
-
基于共享内存的非阻塞锁API 2017-04-08 22:42:20
-
基于共享内存的非阻塞锁API 2016-09-29 03:18:33