lua-resty-mlcache
OpenResty 的分层缓存库
$ opm get thibaultcha/lua-resty-mlcache
lua-resty-mlcache
[!CI](https://github.com/thibaultcha/lua-resty-mlcache/actions/workflows/ci.yml)
OpenResty 的快速自动化分层缓存。
此库可以作为键值存储缓存标量 Lua 类型和表来操作,它结合了 [lua_shared_dict] API 和 [lua-resty-lrucache] 的强大功能,从而带来了极其高效且灵活的缓存解决方案。
功能
带有 TTL 的缓存和负缓存。
通过 [lua-resty-lock] 内置互斥锁,以防止缓存未命中时对您的数据库/后端产生雪崩效应。
内置的跨工作进程通信,以传播缓存失效并允许工作进程在更改(
set()
、delete()
)时更新其 L1(lua-resty-lrucache)缓存。支持拆分命中和未命中缓存队列。
可以创建多个隔离的实例来保存各种类型的数据,同时依赖于同一个
lua_shared_dict
L2 缓存。
此库中内置的各种缓存级别的示意图
┌─────────────────────────────────────────────────┐
│ Nginx │
│ ┌───────────┐ ┌───────────┐ ┌───────────┐ │
│ │worker │ │worker │ │worker │ │
│ L1 │ │ │ │ │ │ │
│ │ Lua cache │ │ Lua cache │ │ Lua cache │ │
│ └───────────┘ └───────────┘ └───────────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌───────────────────────────────────────┐ │
│ │ │ │
│ L2 │ lua_shared_dict │ │
│ │ │ │
│ └───────────────────────────────────────┘ │
│ │ mutex │
│ ▼ │
│ ┌──────────────────┐ │
│ │ callback │ │
│ └────────┬─────────┘ │
└───────────────────────────┼─────────────────────┘
│
L3 │ I/O fetch
▼
Database, API, DNS, Disk, any I/O...
缓存级别层次结构是
L1:使用 [lua-resty-lrucache] 的最近最少使用 Lua VM 缓存。如果已填充,则提供最快的查找,并避免耗尽工作进程的 Lua VM 内存。
L2:所有工作进程共享的
lua_shared_dict
内存区域。仅当 L1 未命中时才会访问此级别,并且可以防止工作进程请求 L3 缓存。L3:一个自定义函数,它只会被单个工作进程运行,以避免对您的数据库/后端产生雪崩效应(通过 [lua-resty-lock])。通过 L3 获取的值将被设置为 L2 缓存,以便其他工作进程检索。
此库已在OpenResty Con 2018 上展示。请参阅资源部分以获取演讲的录制内容。
概要
# nginx.conf
http {
# you do not need to configure the following line when you
# use LuaRocks or opm.
lua_package_path "/path/to/lua-resty-mlcache/lib/?.lua;;";
# 'on' already is the default for this directive. If 'off', the L1 cache
# will be inefective since the Lua VM will be re-created for every
# request. This is fine during development, but ensure production is 'on'.
lua_code_cache on;
lua_shared_dict cache_dict 1m;
init_by_lua_block {
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_dict", {
lru_size = 500, -- size of the L1 (Lua VM) cache
ttl = 3600, -- 1h ttl for hits
neg_ttl = 30, -- 30s ttl for misses
})
if err then
end
-- we put our instance in the global table for brevity in
-- this example, but prefer an upvalue to one of your modules
-- as recommended by ngx_lua
_G.cache = cache
}
server {
listen 8080;
location / {
content_by_lua_block {
local function callback(username)
-- this only runs *once* until the key expires, so
-- do expensive operations like connecting to a remote
-- backend here. i.e: call a MySQL server in this callback
return db:get_user(username) -- { name = "John Doe", email = "john@example.com" }
end
-- this call will try L1 and L2 before running the callback (L3)
-- the returned value will then be stored in L2 and L1
-- for the next request.
local user, err = cache:get("my_key", nil, callback, "jdoe")
ngx.say(user.name) -- "John Doe"
}
}
}
}
需求
OpenResty >=
1.11.2.2
ngx_lua
lua-resty-lrucache
lua-resty-lock
测试矩阵结果
| OpenResty | 兼容性 |------------:|:--------------------| | < | 未测试 | 1.11.2.x
| :heavy_check_mark: | 1.13.6.x
| :heavy_check_mark: | 1.15.8.x
| :heavy_check_mark: | 1.17.8.x
| :heavy_check_mark: | 1.19.3.x
| :heavy_check_mark: | 1.19.9.x
| :heavy_check_mark: | 1.21.4.x
| :heavy_check_mark: | 1.25.3.x
| :heavy_check_mark: | > | 未测试
安装
使用LuaRocks
$ luarocks install lua-resty-mlcache
或通过opm
$ opm get thibaultcha/lua-resty-mlcache
或手动
一旦您拥有此模块的lib/
目录的本地副本,请将其添加到您的LUA_PATH
(或 OpenResty 的lua_package_path
指令)中
/path/to/lib/?.lua;
方法
new
语法: cache, err = mlcache.new(name, shm, opts?)
创建一个新的 mlcache 实例。如果失败,则返回nil
和一个描述错误的字符串。
第一个参数name
是您为该缓存选择的任意名称,必须是字符串。每个 mlcache 实例根据其名称对其保存的值进行命名空间划分,因此具有相同名称的多个实例将共享相同的数据。
第二个参数shm
是lua_shared_dict
共享内存区域的名称。多个 mlcache 实例可以使用相同的 shm(值将被命名空间划分)。
第三个参数opts
是可选的。如果提供,则它必须是一个包含此实例的所需选项的表。可能的选项是
lru_size
:一个数字,定义了基础 L1 缓存(lua-resty-lrucache 实例)的大小。此大小是 L1 缓存可以容纳的最大项目数。默认值:100
。ttl
:一个数字,指定缓存值的过期时间段。单位为秒,但接受小数部分,例如0.3
。ttl
为0
表示缓存的值永远不会过期。默认值:30
。neg_ttl
:一个数字,指定缓存未命中值的过期时间段(当 L3 回调返回nil
时)。单位为秒,但接受小数部分,例如0.3
。neg_ttl
为0
表示缓存未命中永远不会过期。默认值:5
。resurrect_ttl
:可选数字。当指定时,当 L3 回调返回nil, err
(软错误)时,mlcache 实例将尝试恢复过期的值。有关此选项的更多详细信息,请参见get()部分。单位为秒,但接受小数部分,例如0.3
。lru
:可选。您选择的 lua-resty-lrucache 实例。如果指定,mlcache 不会实例化 LRU。如果需要,可以使用此值来使用 lua-resty-lrucache 的resty.lrucache.pureffi
实现。shm_set_tries
:lua_shared_dictset()
操作的尝试次数。当lua_shared_dict
已满时,它会尝试从其队列中释放最多 30 个项目。当要设置的值远大于释放的空间时,此选项允许 mlcache 重新尝试操作(并释放更多插槽),直到达到最大尝试次数或释放了足够内存以容纳该值。默认值:3
。shm_miss
:可选字符串。一个lua_shared_dict
的名称。当指定时,未命中(回调返回nil
)将缓存在此单独的lua_shared_dict
中。这对于确保大量缓存未命中(例如,由恶意客户端触发)不会从shm
中指定的lua_shared_dict
中逐出太多缓存的项目(命中)非常有用。shm_locks
:可选字符串。一个lua_shared_dict
的名称。当指定时,lua-resty-lock 将使用此共享字典来存储其锁。此选项有助于减少缓存抖动:当 L2 缓存 (shm) 已满时,每次插入(例如,由并发访问触发的 L3 回调创建的锁)都会清除最旧的 30 个访问项目。这些被清除的项目很可能是先前(并且很有价值)的缓存值。通过将锁隔离在单独的共享字典中,遇到缓存抖动的负载可以减轻这种影响。resty_lock_opts
:可选表。[lua-resty-lock] 实例的选项。当 mlcache 运行 L3 回调时,它使用 lua-resty-lock 来确保单个工作进程运行提供的回调。ipc_shm
:可选字符串。如果您希望使用set()、delete() 或purge(),则必须提供一个 IPC(进程间通信)机制,以便工作进程同步并使其 L1 缓存失效。此模块捆绑了一个“现成”的 IPC 库,您可以通过在此选项中指定一个专用的lua_shared_dict
来启用它。多个 mlcache 实例可以使用相同的共享字典(事件将被命名空间划分),但除了 mlcache 之外,其他任何参与者都不应该修改它。ipc
:可选表。与上面的ipc_shm
选项类似,但允许您使用您选择的 IPC 库来传播跨工作进程事件。l1_serializer
:可选函数。其签名和接受的值在get()方法中进行了记录,并附带示例。如果指定,此函数将在每次从 L2 缓存提升值到 L1(工作进程 Lua VM)时被调用。此函数可以在将缓存的项目存储到 L1 缓存之前将其转换为任何 Lua 对象来执行缓存项目的任意序列化。因此,它可以避免您的应用程序在每个请求上重复执行此类转换,例如创建表、cdata 对象、加载新的 Lua 代码等。
示例
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000, -- hold up to 1000 items in the L1 cache (Lua VM)
ttl = 3600, -- caches scalar types and tables for 1h
neg_ttl = 60 -- caches nil values for 60s
})
if not cache then
error("could not create mlcache: " .. err)
end
您可以创建多个 mlcache 实例,这些实例依赖于同一个基础lua_shared_dict
共享内存区域
local mlcache = require "mlcache"
local cache_1 = mlcache.new("cache_1", "cache_shared_dict", { lru_size = 100 })
local cache_2 = mlcache.new("cache_2", "cache_shared_dict", { lru_size = 1e5 })
在上面的示例中,cache_1
非常适合保存少量、非常大的值。cache_2
可用于保存大量小值。这两个实例都将依赖于同一个 shm:lua_shared_dict cache_shared_dict 2048m;
。即使您在两个缓存中使用相同的键,它们也不会相互冲突,因为它们都具有不同的命名空间。
此其他示例使用捆绑的 IPC 模块为跨工作进程失效事件实例化 mlcache(因此我们可以使用set()、delete() 和purge())
local mlcache = require "resty.mlcache"
local cache, err = mlcache.new("my_cache_with_ipc", "cache_shared_dict", {
lru_size = 1000,
ipc_shm = "ipc_shared_dict"
})
注意:为了使 L1 缓存有效,请确保lua_code_cache 已启用(这是默认设置)。如果您在开发过程中关闭了此指令,mlcache 将继续工作,但 L1 缓存将无效,因为将为每个请求创建一个新的 Lua VM。
get
语法: value, err, hit_level = cache:get(key, opts?, callback?, ...)
执行缓存查找。这是此模块的主要且最有效的方法。典型的模式是不要调用set(),而是让get() 完成所有工作。
当此方法成功时,它将返回value
并且err
被设置为nil
。因为来自 L3 回调的nil
值可以被缓存(即“负缓存”),所以value
可以是nil
,尽管已经被缓存。因此,必须注意检查第二个返回值err
,以确定此方法是否成功。
第三个返回值是一个数字,如果未遇到错误,则会设置该数字。它指示获取值的级别:1
表示 L1、2
表示 L2 以及3
表示 L3。
但是,如果遇到错误,则此方法将在value
中返回nil
,并在err
中返回一个描述错误的字符串。
第一个参数key
是一个字符串。每个值都必须存储在唯一的键下。
第二个参数opts
是可选的。如果提供,则它必须是一个包含此键的所需选项的表。这些选项将覆盖实例的选项
ttl
:一个数字,指定缓存值的过期时间段。单位为秒,但接受小数部分,例如0.3
。ttl
为0
表示缓存的值永远不会过期。默认值:从实例继承。neg_ttl
:一个数字,指定缓存未命中值的过期时间段(当 L3 回调返回nil
时)。单位为秒,但接受小数部分,例如0.3
。neg_ttl
为0
表示缓存未命中永远不会过期。默认值:从实例继承。resurrect_ttl
: 可选 数字。当指定时,get()
在遇到错误时会尝试复活过期的值。L3 回调返回的错误(nil, err
)被认为是获取/刷新值的失败。当回调返回此类值时,如果过期值仍然在内存中,那么get()
将在resurrect_ttl
秒内复活过期值。get()
返回的错误将在 WARN 级别记录,但不会 返回给调用者。最后,hit_level
返回值将为4
,表示服务项目已过期。当resurrect_ttl
到达时,get()
将再次尝试运行回调。如果到那时回调再次返回错误,则该值将再次复活,依此类推。如果回调成功,则该值将被刷新并且不再标记为过期。由于 LRU 缓存模块中的当前限制,当过期值被提升到 L1 缓存并从那里检索时,hit_level
将为1
。回调抛出的 Lua 错误不会 触发复活,并且通常由get()
返回(nil, err
)。当多个 worker 在等待运行回调的 worker 时超时(例如,因为数据存储超时),使用此选项的用户会看到与get()
的传统行为略有不同。get()
不会返回nil, err
(表示锁超时),而是会返回过期值(如果可用),没有错误,并且hit_level
将为4
。但是,该值不会被复活(因为另一个 worker 仍在运行回调)。此选项的单位是秒,但它接受小数部分,例如0.3
。此选项必须大于0
,以防止过期值无限期地被缓存。默认值:从实例继承。shm_set_tries
: lua_shared_dictset()
操作的尝试次数。当lua_shared_dict
已满时,它会尝试从队列中释放最多 30 个项目。当设置的值远大于释放的空间时,此选项允许 mlcache 重试操作(并释放更多插槽),直到达到最大尝试次数或释放了足够内存以容纳该值。默认值:从实例继承。l1_serializer
: 可选 函数。它的签名和接受的值在 get() 方法中进行了说明,并提供了一个示例。如果指定,此函数将在每次将值从 L2 缓存提升到 L1(worker Lua VM)时被调用。此函数可以在将缓存项目存储到 L1 缓存之前对其执行任意序列化,以将其转换为任何 Lua 对象。因此,它可以避免您的应用程序在每个请求上重复此类转换,例如创建表格、cdata 对象、加载新的 Lua 代码等等...默认值:从实例继承。resty_lock_opts
: 可选 表格。如果指定,则覆盖当前get()
查找的实例resty_lock_opts
。默认值:从实例继承。
第三个参数 callback
是可选的。如果提供,它必须是一个函数,其签名和返回值在以下示例中进行了说明
-- arg1, arg2, and arg3 are arguments forwarded to the callback from the
-- `get()` variadic arguments, like so:
-- cache:get(key, opts, callback, arg1, arg2, arg3)
local function callback(arg1, arg2, arg3)
-- I/O lookup logic
-- ...
-- value: the value to cache (Lua scalar or table)
-- err: if not `nil`, will abort get(), which will return `value` and `err`
-- ttl: override ttl for this value
-- If returned as `ttl >= 0`, it will override the instance
-- (or option) `ttl` or `neg_ttl`.
-- If returned as `ttl < 0`, `value` will be returned by get(),
-- but not cached. This return value will be ignored if not a number.
return value, err, ttl
end
提供的 callback
函数允许在受保护模式下运行时抛出 Lua 错误。从回调抛出的此类错误将在第二个返回值 err
中作为字符串返回。
如果未提供 callback
,get()
仍将在 L1 和 L2 缓存中查找请求的键,并在找到时返回它。如果在缓存中没有找到值并且没有提供回调,get()
将返回 nil, nil, -1
,其中 -1 表示缓存未命中(没有值)。不要将其与返回值 nil, nil, 1
混淆,其中 1 表示在 L1 中找到了负缓存项目(缓存的 nil
)。
不提供 callback
函数允许实现缓存查找模式,这些模式保证在 CPU 上运行,以便更稳定、更平滑的延迟尾部(例如,使用通过 set()
在后台计时器中刷新的值)。
local value, err, hit_lvl = cache:get("key")
if value == nil then
if err ~= nil then
-- error
elseif hit_lvl == -1 then
-- miss (no value)
else
-- negative hit (cached `nil` value)
end
end
当提供回调时,get()
遵循以下逻辑
查询 L1 缓存(lua-resty-lrucache 实例)。此缓存位于 Lua VM 中,因此它是查询效率最高的缓存。
如果 L1 缓存中有该值,则返回它。
如果 L1 缓存中没有该值(L1 未命中),则继续。
查询 L2 缓存(
lua_shared_dict
内存区域)。此缓存由所有 worker 共享,并且与 L1 缓存一样高效。但是,它需要对存储的 Lua 表格进行序列化。如果 L2 缓存中有该值,则返回它。
如果设置了
l1_serializer
,则运行它,并将结果值提升到 L1 缓存中。如果没有,则直接将值按原样提升到 L1 缓存中。
如果 L2 缓存中没有该值(L2 未命中),则继续。
创建一个 [lua-resty-lock],并确保只有一个 worker 会运行回调(其他尝试访问相同值的 worker 会等待)。
一个 worker 运行 L3 回调(例如,执行数据库查询)。
回调成功并返回一个值:该值将被设置在 L2 缓存中,然后在 L1 缓存中(默认情况下按原样,或者如果指定了
l1_serializer
则按其返回的值)。回调失败并返回
nil, err
:a. 如果指定了resurrect_ttl
,并且过期值仍然可用,则将其在 L2 缓存中复活并将其提升到 L1。b. 否则,get()
返回nil, err
。
尝试访问相同值但一直在等待的其他 worker 将被解锁,并从 L2 缓存中读取值(它们不会运行 L3 回调)并返回它。
当不提供回调时,get()
仅会执行步骤 1. 和 2.
以下是一个完整的示例用法
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict", {
lru_size = 1000,
ttl = 3600,
neg_ttl = 60
})
local function fetch_user(user_id)
local user, err = db:query_user(user_id)
if err then
-- in this case, get() will return `nil` + `err`
return nil, err
end
return user -- table or nil
end
local user_id = 3
local user, err = cache:get("users:" .. user_id, nil, fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "could not retrieve user: ", err)
return
end
-- `user` could be a table, but could also be `nil` (does not exist)
-- regardless, it will be cached and subsequent calls to get() will
-- return the cached value, for up to `ttl` or `neg_ttl`.
if user then
ngx.say("user exists: ", user.name)
else
ngx.say("user does not exists")
end
第二个示例与上面的示例类似,但在这里,我们对检索到的 user
记录应用了一些转换,然后再通过 l1_serializer
回调对其进行缓存
-- Our l1_serializer, called when a value is promoted from L2 to L1
--
-- Its signature receives a single argument: the item as returned from
-- an L2 hit. Therefore, this argument can never be `nil`. The result will be
-- kept in the L1 cache, but it cannot be `nil`.
--
-- This function can return `nil` and a string describing an error, which
-- will bubble up to the caller of `get()`. It also runs in protected mode
-- and will report any Lua error.
local function load_code(user_row)
if user_row.custom_code ~= nil then
local f, err = loadstring(user_row.raw_lua_code)
if not f then
-- in this case, nothing will be stored in the cache (as if the L3
-- callback failed)
return nil, "failed to compile custom code: " .. err
end
user_row.f = f
end
return user_row
end
local user, err = cache:get("users:" .. user_id,
{ l1_serializer = load_code },
fetch_user, user_id)
if err then
ngx.log(ngx.ERR, "could not retrieve user: ", err)
return
end
-- now we can call a function that was already loaded once, upon entering
-- the L1 cache (Lua VM)
user.f()
get_bulk
语法: res, err = cache:get_bulk(bulk, opts?)
一次执行多个 get() 查找(批量)。任何需要 L3 回调调用的这些查找将并发执行,在一个 ngx.thread 池中。
第一个参数 bulk
是一个包含 n
个操作的表格。
第二个参数 opts
是可选的。如果提供,它必须是一个包含此批量查找选项的表格。可能的选项是
concurrency
: 大于0
的数字。指定将并发执行此批量查找的 L3 回调的线程数。并发度为3
,有 6 个回调要运行,意味着每个线程将执行 2 个回调。并发度为1
,有 6 个回调要运行,意味着一个线程将执行所有 6 个回调。并发度为6
,有 1 个回调要运行,意味着一个线程将运行该回调。默认值:3
。
成功后,此方法将返回 res
,一个包含每个查找结果的表格,以及没有错误。
失败后,此方法将返回 nil
以及描述错误的字符串。
此方法执行的所有查找操作将完全集成到其他 worker 并发执行的其他操作中(例如 L1/L2 命中/未命中存储、L3 回调互斥锁等)。
bulk
参数是一个必须具有特定布局的表格(在下面的示例中进行了说明)。它可以手动构建,也可以通过 new_bulk() 帮助器方法构建。
同样,res
表格也有自己的特定布局。它可以手动迭代,也可以通过 each_bulk_res 迭代器帮助器迭代。
示例
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
cache:get("key_c", nil, function() return nil end)
local res, err = cache:get_bulk({
-- bulk layout:
-- key opts L3 callback callback argument
"key_a", { ttl = 60 }, function() return "hello" end, nil,
"key_b", nil, function() return "world" end, nil,
"key_c", nil, function() return "bye" end, nil,
n = 3 -- specify the number of operations
}, { concurrency = 3 })
if err then
ngx.log(ngx.ERR, "could not execute bulk lookup: ", err)
return
end
-- res layout:
-- data, "err", hit_lvl }
for i = 1, res.n, 3 do
local data = res[i]
local err = res[i + 1]
local hit_lvl = res[i + 2]
if not err then
ngx.say("data: ", data, ", hit_lvl: ", hit_lvl)
end
end
上面的示例将产生以下输出
data: hello, hit_lvl: 3
data: world, hit_lvl: 3
data: nil, hit_lvl: 1
请注意,由于 key_c
已经存在于缓存中,因此返回 "bye"
的回调从未运行,因为 get_bulk()
从 L1 中检索了该值,如 hit_lvl
值所示。
注意:与 get() 不同,此方法只允许为每个查找的回调指定一个参数。
new_bulk
语法: bulk = mlcache.new_bulk(n_lookups?)
创建一个包含 get_bulk() 函数的查找操作的表格。使用此函数构建批量查找表格不是必需的,但它提供了一个不错的抽象。
第一个也是唯一的参数 n_lookups
是可选的,如果指定,它是一个表示此批量最终将包含的查找数量的数字,以便为优化目的预分配底层表格。
此函数返回一个表格 bulk
,其中还没有查找操作。通过调用 bulk:add(key, opts?, cb, arg?)
将查找添加到 bulk
表格中
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local bulk = mlcache.new_bulk(3)
bulk:add("key_a", { ttl = 60 }, function(n) return n * n, 42)
bulk:add("key_b", nil, function(str) return str end, "hello")
bulk:add("key_c", nil, function() return nil end)
local res, err = cache:get_bulk(bulk)
each_bulk_res
语法: iter, res, i = mlcache.each_bulk_res(res)
提供一个抽象来迭代 get_bulk() res
返回表格。使用此方法迭代 res
表格不是必需的,但它提供了一个不错的抽象。
此方法可以作为 Lua 迭代器调用
local mlcache = require "mlcache"
local cache, err = mlcache.new("my_cache", "cache_shared_dict")
local res, err = cache:get_bulk(bulk)
for i, data, err, hit_lvl in mlcache.each_bulk_res(res) do
if not err then
ngx.say("lookup ", i, ": ", data)
end
end
peek
语法: ttl, err, value = cache:peek(key, stale?)
窥视 L2(lua_shared_dict
)缓存。
第一个参数 key
是一个字符串,它是在缓存中查找的键。
第二个参数 stale
是可选的。如果为 true
,那么 peek()
将过期值视为缓存值。如果没有提供,peek()
将考虑过期值,就像它们不存在于缓存中一样
此方法在失败时返回 nil
以及描述错误的字符串。
如果查询的 key
没有值,它将返回 nil
以及没有错误。
如果查询的 key
有值,它将返回一个表示缓存值的剩余 TTL(以秒为单位)的数字以及没有错误。如果 key
的值已过期,但仍在 L2 缓存中,则返回的 TTL 值将为负数。剩余 TTL 返回值仅在查询的 key
具有无限 TTL(ttl=0
)时才为 0
。否则,此返回值可以为正(key
仍然有效)或负(key
已过期)。
第三个返回值将是 L2 缓存中存储的缓存值(如果仍然可用)。
当您想要确定一个值是否被缓存时,此方法很有用。存储在 L2 缓存中的值被认为是被缓存的,无论它是否也被设置在 worker 的 L1 缓存中。这是因为 L1 缓存被认为是易失的(因为它的尺寸单位是插槽数量),而 L2 缓存仍然比 L3 回调快几个数量级。
由于其唯一目的是“窥视”缓存以确定某个值的温暖程度,因此 peek()
不算作像 get() 这样的查询,也不会将该值提升到 L1 缓存中。
示例
local mlcache = require "mlcache"
local cache = mlcache.new("my_cache", "cache_shared_dict")
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "could not peek cache: ", err)
return
end
ngx.say(ttl) -- nil because `key` has no value yet
ngx.say(value) -- nil
-- cache the value
cache:get("key", { ttl = 5 }, function() return "some value" end)
-- wait 2 seconds
ngx.sleep(2)
local ttl, err, value = cache:peek("key")
if err then
ngx.log(ngx.ERR, "could not peek cache: ", err)
return
end
ngx.say(ttl) -- 3
ngx.say(value) -- "some value"
注意:从 mlcache 2.5.0
开始,也可以调用 get() 而不使用回调函数来“查询”缓存。与 peek()
不同的是,没有回调的 get()
调用将将值提升到 L1 缓存中,并且不会返回其 TTL。
set
语法: ok, err = cache:set(key, opts?, value)
在 L2 缓存中无条件设置一个值,并将一个事件广播到其他 worker,以便它们可以从其 L1 缓存中刷新该值。
第一个参数 key
是一个字符串,它是存储值的键。
第二个参数 opts
是可选的,如果提供,则与 get() 的参数相同。
第三个参数 value
是要缓存的值,类似于 L3 回调的返回值。与回调的返回值一样,它必须是 Lua 标量、表或 nil
。如果从构造函数或 opts
参数中提供 l1_serializer
,则如果 value
不为 nil
,它将使用 value
调用它。
成功时,第一个返回值将为 true
。
失败时,此方法返回 nil
和一个描述错误的字符串。
注意: set()
本质上要求 mlcache 的其他实例(来自其他工作进程)刷新其 L1 缓存。如果从单个工作进程调用 set()
,则具有相同 name
的其他工作进程的 mlcache 实例必须在下次请求期间请求缓存之前调用 update(),以确保它们刷新了其 L1 缓存。
再注意: 通常认为在热代码路径(例如在 OpenResty 提供的请求中)调用 set()
效率低下。相反,应该依赖于 get() 及其在 L3 回调中的内置互斥锁。set()
更适合在偶尔从单个工作进程调用时使用,例如在触发更新缓存值的特定事件时。一旦 set()
使用新值更新了 L2 缓存,其他工作进程将依赖于 update() 来轮询失效事件并使他们的 L1 缓存失效,这将使他们在 L2 中获取(新)值。
参见: update()
删除
语法: ok, err = cache:delete(key)
删除 L2 缓存中的值,并发布一个事件到其他工作进程,以便他们可以从他们的 L1 缓存中驱逐该值。
第一个也是唯一的参数 key
是存储值的字符串。
成功时,第一个返回值将为 true
。
失败时,此方法返回 nil
和一个描述错误的字符串。
注意: delete()
本质上要求 mlcache 的其他实例(来自其他工作进程)刷新其 L1 缓存。如果从单个工作进程调用 delete()
,则具有相同 name
的其他工作进程的 mlcache 实例必须在下次请求期间请求缓存之前调用 update(),以确保它们刷新了其 L1 缓存。
参见: update()
清除
语法: ok, err = cache:purge(flush_expired?)
清除缓存的内容,包括 L1 和 L2 级别。然后发布一个事件到其他工作进程,以便他们也可以清除他们的 L1 缓存。
此方法会回收 lua-resty-lrucache 实例,并调用 ngx.shared.DICT:flush_all,因此它可能相当昂贵。
第一个也是唯一的参数 flush_expired
是可选的,但如果给出 true
,此方法还会调用 ngx.shared.DICT:flush_expired(不带参数)。这在需要时释放 L2(shm)缓存占用的内存很有用。
成功时,第一个返回值将为 true
。
失败时,此方法返回 nil
和一个描述错误的字符串。
注意: 在 OpenResty 1.13.6.1 及以下版本中,使用自定义 LRU 缓存时无法调用 purge()
。此限制不适用于 OpenResty 1.13.6.2 及更高版本。
注意: purge()
本质上要求 mlcache 的其他实例(来自其他工作进程)刷新其 L1 缓存。如果从单个工作进程调用 purge()
,则具有相同 name
的其他工作进程的 mlcache 实例必须在下次请求期间请求缓存之前调用 update(),以确保它们刷新了其 L1 缓存。
参见: update()
更新
语法: ok, err = cache:update(timeout?)
轮询并执行其他工作进程发布的挂起的缓存失效事件。
set()
、delete()
和 purge()
方法要求 mlcache 的其他实例(来自其他工作进程)刷新其 L1 缓存。由于 OpenResty 目前没有内置的工作进程间通信机制,因此此模块捆绑了一个“现成”的 IPC 库来传播工作进程间事件。如果使用捆绑的 IPC 库,则 ipc_shm
选项中指定的 lua_shared_dict
不能被 mlcache 本身以外的其他参与者使用。
此方法允许工作进程在处理请求之前更新其 L1 缓存(通过清除由于其他工作进程调用 set()
、delete()
或 purge()
而被认为过时的值)。
此方法接受一个 timeout
参数,其单位为秒,默认为 0.3
(300 毫秒)。如果在达到此阈值时未完成更新操作,则会超时。这可以避免 update()
在处理大量事件时长时间停留在 CPU 上。在最终一致的系统中,其他事件可以等待下次调用进行处理。
一个典型的设计模式是在处理每个请求之前仅调用一次 update()
。这允许你的热代码路径在最佳情况下执行单个 shm 访问:没有收到失效事件,所有 get()
调用都将命中 L1 缓存。只有在最坏情况下(另一个工作进程驱逐了 n
个值),get()
才会 n
次访问 L2 或 L3 缓存。随后的请求将再次命中最佳情况,因为 get()
填充了 L1 缓存。
例如,如果你的工作进程在你的应用程序中的任何地方使用 set()
、delete()
或 purge()
,请在你的热代码路径的入口处调用 update()
,然后再使用 get()
http {
listen 9000;
location / {
content_by_lua_block {
local cache = ... -- retrieve mlcache instance
-- make sure L1 cache is evicted of stale values
-- before calling get()
local ok, err = cache:update()
if not ok then
ngx.log(ngx.ERR, "failed to poll eviction events: ", err)
-- /!\ we might get stale data from get()
end
-- L1/L2/L3 lookup (best case: L1)
local value, err = cache:get("key_1", nil, cb1)
-- L1/L2/L3 lookup (best case: L1)
local other_value, err = cache:get(key_2", nil, cb2)
-- value and other_value are up-to-date because:
-- either they were not stale and directly came from L1 (best case scenario)
-- either they were stale and evicted from L1, and came from L2
-- either they were not in L1 nor L2, and came from L3 (worst case scenario)
}
}
location /delete {
content_by_lua_block {
local cache = ... -- retrieve mlcache instance
-- delete some value
local ok, err = cache:delete("key_1")
if not ok then
ngx.log(ngx.ERR, "failed to delete value from cache: ", err)
return ngx.exit(500)
end
ngx.exit(204)
}
}
location /set {
content_by_lua_block {
local cache = ... -- retrieve mlcache instance
-- update some value
local ok, err = cache:set("key_1", nil, 123)
if not ok then
ngx.log(ngx.ERR, "failed to set value in cache: ", err)
return ngx.exit(500)
end
ngx.exit(200)
}
}
}
注意: 如果你的工作进程从未调用 set()
、delete()
或 purge()
,你不需要调用 update()
来刷新你的工作进程。当工作进程仅依赖于 get()
时,值会根据其 TTL 从 L1/L2 缓存中自然过期。
再注意: 此库的目的是一旦出现更好的工作进程间通信解决方案,就使用它。在这个库的未来版本中,如果 IPC 库可以避免轮询方法,那么这个库也将如此。update()
只是由于当今 Nginx/OpenResty 的“局限性”而产生的必要之恶。但是,你可以在创建 mlcache 实例时使用 opts.ipc
选项来使用自己的 IPC 库。
资源
2018 年 11 月,此库在杭州的 OpenResty 大会上发布。
幻灯片和演讲的录音(约 40 分钟长)可以在这里 [here][talk] 查看。
变更日志
请参见 CHANGELOG.md。
许可证
工作在 MIT 许可证下授权。
[lua-resty-lock]: https://github.com/openresty/lua-resty-lock [lua-resty-lrucache]: https://github.com/openresty/lua-resty-lrucache [lua_shared_dict]: https://github.com/openresty/lua-nginx-module#lua_shared_dict [talk]: https://www.slideshare.net/ThibaultCharbonnier/layered-caching-in-openresty-openresty-con-2018
作者
Thibault Charbonnier (thibaultcha)
许可证
mit
依赖关系
openresty
版本
-
用于 OpenResty 的分层缓存库 2024-02-09 20:15:13
-
用于 OpenResty 的分层缓存库 2024-01-31 00:14:46
-
用于 OpenResty 的分层缓存库 2023-02-16 20:51:50
-
用于 OpenResty 的分层缓存库 2021-02-01 20:35:21
-
用于 OpenResty 的分层缓存库 2020-01-17 22:38:47
-
用于 OpenResty 的分层缓存库 2019-03-28 23:09:32
-
用于 OpenResty 的分层缓存库 2019-01-17 20:40:23
-
用于 OpenResty 的多级缓存库 2018-07-28 21:20:32
-
用于 OpenResty 的多级缓存库 2018-06-15 03:48:26
-
用于 OpenResty 的多级缓存库 2018-04-09 18:12:51
-
用于 OpenResty 的多级缓存库 2018-03-28 00:30:16
-
用于 OpenResty 的多级缓存库 2018-03-18 18:14:19
-
用于 OpenResty 的多级缓存库 2017-08-27 03:23:28
-
用于 OpenResty 的多级缓存库 2017-08-24 01:33:37