跳转至

mlcache: NGINX模块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-mlcache

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

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

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

本文档描述了lua-resty-mlcache v2.7.0,于2024年2月14日发布。


CI

为OpenResty提供快速和自动化的分层缓存。

此库可以作为键/值存储来操作,缓存标量Lua类型和表,结合了[lua_shared_dict] API和[lua-resty-lrucache]的强大功能,从而实现了极高性能和灵活性的缓存解决方案。

特点:

  • 带有TTL的缓存和负缓存。
  • 通过[lua-resty-lock]内置互斥锁,防止在缓存未命中时对数据库/后端造成“狗堆”效应。
  • 内置的工作进程间通信以传播缓存失效,并允许工作进程在更改时更新其L1(lua-resty-lrucache)缓存(set()delete())。
  • 支持拆分命中和未命中缓存队列。
  • 可以创建多个独立实例以保存各种类型的数据,同时依赖于相同的 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
                            ▼

                   数据库、API、DNS、磁盘、任何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 {
    # 当您使用LuaRocks或opm时,无需配置以下行。
    # 'on'已经是该指令的默认值。如果为'off',则L1缓存将无效,因为每个请求都会重新创建Lua VM。
    # 这在开发期间是可以的,但确保生产环境为'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,    -- L1(Lua VM)缓存的大小
            ttl      = 3600,   -- 命中的1小时TTL
            neg_ttl  = 30,     -- 未命中的30秒TTL
        })
        if err then

        end

        -- 为了简洁,我们将实例放入全局表中,但建议使用上值来替代您的模块
        -- 正如ngx_lua所推荐的那样
        _G.cache = cache
    }

    server {
        listen 8080;

        location / {
            content_by_lua_block {
                local function callback(username)
                    -- 这只会运行*一次*,直到键过期,因此
                    -- 在这里执行昂贵的操作,例如连接到远程
                    -- 后端。即:在此回调中调用MySQL服务器
                    return db:get_user(username) -- { name = "John Doe", email = "[email protected]" }
                end

                -- 此调用将在运行回调(L3)之前尝试L1和L2
                -- 返回的值将存储在L2和L1中
                -- 以供下一个请求使用。
                local user, err = cache:get("my_key", nil, callback, "jdoe")

                ngx.say(user.name) -- "John Doe"
            }
        }
    }
}

方法

new

语法: cache, err = mlcache.new(name, shm, opts?)

创建一个新的mlcache实例。如果失败,返回nil和描述错误的字符串。

第一个参数name是您为此缓存选择的任意名称,必须是字符串。每个mlcache实例根据其名称对其持有的值进行命名空间,因此多个具有相同名称的实例将共享相同的数据。

第二个参数shmlua_shared_dict共享内存区域的名称。多个mlcache实例可以使用相同的shm(值将被命名空间化)。

第三个参数opts是可选的。如果提供,它必须是一个表,包含此实例所需的选项。可能的选项包括:

  • lru_size:定义底层L1缓存的大小(lua-resty-lrucache实例)的数字。此大小是L1缓存可以容纳的最大项目数。 默认值: 100
  • ttl:指定缓存值的过期时间段的数字。单位为秒,但接受小数部分,例如0.3ttl0表示缓存值将永不过期。 默认值: 30
  • neg_ttl:指定缓存未命中(当L3回调返回nil)的过期时间段的数字。单位为秒,但接受小数部分,例如0.3neg_ttl0表示缓存未命中将永不过期。 默认值: 5
  • resurrect_ttl:_可选_数字。当指定时,mlcache实例将在L3回调返回nil, err(软错误)时尝试恢复过期值。有关此选项的更多详细信息,请参见get()部分。单位为秒,但接受小数部分,例如0.3
  • lru可选。您选择的lua-resty-lrucache实例。如果指定,mlcache将不实例化LRU。可以使用此值来使用lua-resty-lrucache的resty.lrucache.pureffi实现。
  • shm_set_tries:lua_shared_dict set()操作的尝试次数。当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, -- 在L1缓存(Lua VM)中最多保存1000个项目
    ttl      = 3600, -- 缓存标量类型和表1小时
    neg_ttl  = 60    -- 缓存nil值60秒
})
if not cache then
    error("无法创建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.3ttl0表示缓存值将永不过期。 默认值: 从实例继承。
  • neg_ttl:指定缓存未命中(当L3回调返回nil)的过期时间段的数字。单位为秒,但接受小数部分,例如0.3neg_ttl0表示缓存未命中将永不过期。 默认值: 从实例继承。
  • resurrect_ttl:_可选_数字。当指定时,get()将在遇到错误时尝试恢复过期值。L3回调返回的错误(nil, err)被视为获取/刷新值的失败。当get()看到来自回调的此类返回值,并且如果过期值仍在内存中,则get()将在resurrect_ttl秒内恢复过期值。get()返回的错误将在WARN级别记录,但不会返回给调用者。最后,hit_level返回值将为4,以表示提供的项目是过期的。当达到resurrect_ttl时,get()将再次尝试运行回调。如果到那时回调再次返回错误,则值将再次恢复,依此类推。如果回调成功,则值将被刷新,不再标记为过期。由于当前LRU缓存模块的限制,当过期值被提升到L1缓存并从那里检索时,hit_level将为1。回调抛出的Lua错误_不会_触发恢复,并将按常规返回给get()nil, err)。当多个工作进程在等待运行回调的工作进程时超时(例如,因为数据存储超时),则使用此选项的用户将看到与传统行为的get()相比的轻微差异。get()将返回过期值(如果可用),没有错误,并且hit_level将为4。但是,值不会被恢复(因为另一个工作进程仍在运行回调)。此选项的单位为秒,但接受小数部分,例如0.3。此选项必须大于0,以防止过期值被无限期缓存。 默认值: 从实例继承。
  • shm_set_tries:lua_shared_dict set()操作的尝试次数。当lua_shared_dict已满时,它尝试从其队列中释放最多30个项目。当要设置的值远大于释放的空间时,此选项允许mlcache重试该操作(并释放更多插槽),直到达到最大尝试次数或释放足够的内存以容纳该值。 默认值: 从实例继承。
  • l1_serializer:_可选_函数。其签名和接受的值在get()方法下进行了文档说明,并附有示例。如果指定,每次从L2缓存提升值到L1(工作进程Lua VM)时,将调用此函数。此函数可以对缓存项进行任意序列化,以在存储到L1缓存之前将其转换为任何Lua对象。这样可以避免您的应用程序在每个请求中重复这些转换,例如创建表、cdata对象、加载新Lua代码等... 默认值: 从实例继承。
  • resty_lock_opts:_可选_表。如果指定,将覆盖当前get()查找的实例resty_lock_opts默认值: 从实例继承。

第三个参数callback是可选的。如果提供,它必须是一个函数,其签名和返回值在以下示例中进行了文档说明:

-- arg1、arg2和arg3是从`get()`可变参数转发到回调的参数,如下所示:
-- cache:get(key, opts, callback, arg1, arg2, arg3)

local function callback(arg1, arg2, arg3)
    -- I/O查找逻辑
    -- ...

    -- value: 要缓存的值(Lua标量或表)
    -- err: 如果不是`nil`,将中止get(),返回`value`和`err`
    -- ttl: 为此值覆盖ttl
    --      如果返回为`ttl >= 0`,将覆盖实例(或选项)`ttl`或`neg_ttl`。
    --      如果返回为`ttl < 0`,则`value`将由get()返回,
    --      但不会被缓存。如果不是数字,则此返回值将被忽略。
    return value, err, ttl
end

提供的callback函数可以在受保护模式下抛出Lua错误。来自回调的此类错误将作为字符串返回在第二个返回值err中。

如果未提供callbackget()仍将查找L1和L2缓存中的请求键,并在找到时返回它。在缓存未提供回调的情况下,如果未找到值,get()将返回nil, nil, -1,其中-1表示缓存未命中(没有值)。这与返回值如nil, nil, 1不同,后者表示在L1中找到的负缓存项(缓存的nil)。

不提供callback函数允许实现缓存查找模式,这些模式保证在CPU上进行,以获得更恒定、更平滑的延迟尾部(例如,通过set()在后台定时器中刷新值)。

local value, err, hit_lvl = cache:get("key")
if value == nil then
    if err ~= nil then
        -- 错误
    elseif hit_lvl == -1 then
        -- 未命中(没有值)
    else
        -- 负命中(缓存的`nil`值)
    end
end

提供回调时,get()遵循以下逻辑:

  1. 查询L1缓存(lua-resty-lrucache实例)。此缓存位于Lua VM中,因此查询效率最高。
    1. 如果L1缓存有值,则返回它。
    2. 如果L1缓存没有值(L1未命中),则继续。
  2. 查询L2缓存(lua_shared_dict内存区域)。此缓存由所有工作进程共享,几乎与L1缓存一样高效。然而,它需要序列化存储的Lua表。
    1. 如果L2缓存有值,则返回它。
      1. 如果设置了l1_serializer,则运行它,并将结果值提升到L1缓存中。
      2. 如果没有,则直接将值原样提升到L1缓存中。
    2. 如果L2缓存没有值(L2未命中),则继续。
  3. 创建[lua-resty-lock],确保单个工作进程将运行回调(其他尝试访问同一值的工作进程将等待)。
  4. 单个工作进程运行L3回调(例如,执行数据库查询)
  5. 回调成功并返回值:该值设置在L2缓存中,然后在L1缓存中(默认原样,或如指定的那样由l1_serializer返回)。
  6. 回调失败并返回nil, err: a. 如果指定了resurrect_ttl,并且过期值仍然可用,则在L2缓存中恢复它并提升到L1。 b. 否则,get()返回nil, err
  7. 其他尝试访问同一值但正在等待的工作进程被解锁并从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
        -- 在这种情况下,get()将返回`nil` + `err`
        return nil, err
    end

    return user -- 表或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, "无法检索用户: ", err)
    return
end

-- `user`可能是一个表,但也可能是`nil`(不存在)
-- 无论如何,它将被缓存,后续对get()的调用将
-- 返回缓存的值,最多持续`ttl`或`neg_ttl`。
if user then
    ngx.say("用户存在: ", user.name)
else
    ngx.say("用户不存在")
end

第二个示例与上述示例类似,但在这里我们在通过l1_serializer回调缓存之前对检索到的user记录应用了一些转换:

-- 我们的l1_serializer,在值从L2提升到L1时调用
--
-- 它的签名接收一个参数:作为L2命中的返回项。因此,此参数永远不会是`nil`。结果将保留在L1缓存中,但不能是`nil`。
--
-- 此函数可以返回`nil`和描述错误的字符串,这将向调用者的`get()`冒泡。它也在受保护模式下运行,并将报告任何Lua错误。
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
            -- 在这种情况下,缓存中将不会存储任何内容(就像L3回调失败一样)
            return nil, "编译自定义代码失败: " .. 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, "无法检索用户: ", err)
     return
end

-- 现在我们可以调用在进入L1缓存(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加上描述错误的字符串。

此方法执行的所有查找操作将完全融入其他方法和Nginx工作进程并发执行的其他操作(例如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({
  -- 批量布局:
  -- 键     选项          L3回调                    回调参数

    "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 -- 指定操作的数量
}, { concurrency = 3 })
if err then
     ngx.log(ngx.ERR, "无法执行批量查找: ", err)
     return
end

-- res布局:
-- 数据, "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, ", hit_lvl: ", hit_lvl)
    end
end

上述示例将产生以下输出:

数据: hello, hit_lvl: 3
数据: world, hit_lvl: 3
数据: 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("查找 ", 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值将为负。查询的key的剩余TTL返回值仅在查询的key具有无限TTL(ttl=0)时为0。否则,此返回值可能为正(key仍有效)或负(key已过期)。

第三个返回值将是存储在L2缓存中的缓存值(如果仍然可用)。

此方法在您想要确定某个值是否被缓存时非常有用。存储在L2缓存中的值被视为缓存,无论它是否也在工作进程的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, "无法查看缓存: ", err)
    return
end

ngx.say(ttl)   -- nil,因为`key`尚未有值
ngx.say(value) -- nil

-- 缓存值

cache:get("key", { ttl = 5 }, function() return "some value" end)

-- 等待2秒

ngx.sleep(2)

local ttl, err, value = cache:peek("key")
if err then
    ngx.log(ngx.ERR, "无法查看缓存: ", 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缓存中设置一个值,并向其他工作进程广播事件,以便它们可以从其L1缓存中刷新该值。

第一个参数key是一个字符串,是存储值的键。

第二个参数opts是可选的,如果提供,和get()的选项相同。

第三个参数value是要缓存的值,类似于L3回调的返回值。与回调的返回值一样,它必须是Lua标量、表或nil。如果提供了l1_serializer,无论是来自构造函数还是在opts参数中,它将在value不为nil时被调用。

成功时,第一个返回值将为true

失败时,此方法返回nil和描述错误的字符串。

注意: 根据其性质,set()要求其他mlcache实例(来自其他工作进程)刷新其L1缓存。如果set()是由单个工作进程调用的,则其他工作进程的mlcache实例具有相同的name,必须在下次请求期间调用update(),以确保它们刷新了其L1缓存。

注意二: 通常认为在热代码路径(例如在OpenResty提供的请求中)调用set()效率低下。相反,应该依赖get()及其内置互斥锁在L3回调中。set()更适合在单个工作进程中偶尔调用,例如在触发缓存值更新的特定事件时。一旦set()用新值更新L2缓存,其他工作进程将依赖update()来轮询失效事件并使其L1缓存失效,这将使它们从L2中获取(新鲜)值。

参见: update()

delete

语法: ok, err = cache:delete(key)

在L2缓存中删除一个值,并向其他工作进程发布事件,以便它们可以从其L1缓存中驱逐该值。

第一个也是唯一的参数key是存储值的字符串。

成功时,第一个返回值将为true

失败时,此方法返回nil和描述错误的字符串。

注意: 根据其性质,delete()要求其他mlcache实例(来自其他工作进程)刷新其L1缓存。如果delete()是由单个工作进程调用的,则其他工作进程的mlcache实例具有相同的name,必须在下次请求期间调用update(),以确保它们刷新了其L1缓存。

参见: update()

purge

语法: 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和描述错误的字符串。

注意: 在使用自定义LRU缓存的OpenResty 1.13.6.1及以下版本时,无法调用purge()。此限制不适用于OpenResty 1.13.6.2及以上版本。

注意: 根据其性质,purge()要求其他mlcache实例(来自其他工作进程)刷新其L1缓存。如果purge()是由单个工作进程调用的,则其他工作进程的mlcache实例具有相同的name,必须在下次请求期间调用update(),以确保它们刷新了其L1缓存。

参见: update()

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()才会访问L2或L3缓存n次。后续请求将再次命中最佳情况,因为get()填充了L1缓存。

例如,如果您的工作进程在应用程序的任何地方使用set()delete()purge(),请在热代码路径的入口处调用update(),在使用get()之前:

http {
    listen 9000;

    location / {
        content_by_lua_block {
            local cache = ... -- 获取mlcache实例

            -- 确保在调用get()之前L1缓存被驱逐过期值
            local ok, err = cache:update()
            if not ok then
                ngx.log(ngx.ERR, "轮询失效事件失败: ", err)
                -- /!\ 我们可能会从get()获取到过期数据
            end

            -- L1/L2/L3查找(最佳情况:L1)
            local value, err = cache:get("key_1", nil, cb1)

            -- L1/L2/L3查找(最佳情况:L1)
            local other_value, err = cache:get("key_2", nil, cb2)

            -- value和other_value是最新的,因为:
            -- 它们要么没有过期并直接来自L1(最佳情况)
            -- 要么它们过期并从L1驱逐,来自L2
            -- 要么它们既不在L1中也不在L2中,来自L3(最坏情况)
        }
    }

    location /delete {
        content_by_lua_block {
            local cache = ... -- 获取mlcache实例

            -- 删除某个值
            local ok, err = cache:delete("key_1")
            if not ok then
                ngx.log(ngx.ERR, "从缓存中删除值失败: ", err)
                return ngx.exit(500)
            end

            ngx.exit(204)
        }
    }

    location /set {
        content_by_lua_block {
            local cache = ... -- 获取mlcache实例

            -- 更新某个值
            local ok, err = cache:set("key_1", nil, 123)
            if not ok then
                ngx.log(ngx.ERR, "在缓存中设置值失败: ", 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 Con上进行了展示。

演示文稿和演讲的录音(约40分钟)可以在[这里][talk]观看。

更新日志

请参见CHANGELOG.md

GitHub

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