apisix 中的 lrucache
基于 3.10.0 版本
apisix 的 lrucache 封装了 resty-lrucache 及 resty-lock
机制
代码不多,逐行分析
先看下新建一个 lrucache时支持的选项 opts
type 类型, 如果是插件是 plugin
count 容量, 如果没有设置, 类型为 plugin 的默认 count=8,其他类型默认 count=1024
ttl 过期时间,如果没有设置,类型为 plugin 默认 ttl=5min, 其他类型默认 ttl=60min
release 缓存命中,但是val.version不是预期的,会调用hook函数, 做一些额外的行为
invalid_stale 是否允许获取已过期的值, key 对应value过期,但是val.version是一致的,是的话,会重新将这个已过期值设置到 lrucache中 (即,会命中缓存,不会调用 create_obj_fun
serial_creating 确保缓存对象是串行创建的,即创建流程中会上锁避免竟态场景下创建同一个对象
local function new_lru_fun(opts)
-- 选项
local item_count, item_ttl
if opts and opts.type == 'plugin' then
item_count = opts.count or PLUGIN_ITEMS_COUNT
item_ttl = opts.ttl or PLUGIN_TTL
else
item_count = opts and opts.count or GLOBAL_ITEMS_COUNT
item_ttl = opts and opts.ttl or GLOBAL_TTL
end
local item_release = opts and opts.release
local invalid_stale = opts and opts.invalid_stale
local serial_creating = opts and opts.serial_creating
-- 创建一个 lrucache 对象, 大小为 item_count
local lru_obj = lru_new(item_count)
-- 返回一个fun, 其参数为 key, version, create_obj_fun, 以及 create_obj_fun 的参数
return function (key, version, create_obj_fun, ...)
-- 非 serial_creating ,或者 phase 不是 ssl_session_fetch、ssl_session_store、rewrite、access、content、timer 之一
-- nginx.get_phase: https://github.com/openresty/lua-nginx-module#ngxget_phase
if not serial_creating or not can_yield_phases[get_phase()] then
-- 获取缓存
local cache_obj = fetch_valid_cache(lru_obj, invalid_stale,
item_ttl, item_release, key, version)
-- 命中直接返回
if cache_obj then
return cache_obj.val
end
-- 主意,这里没有上锁; 重复调用 create_obj_fun 以及 lru_obj:set 是允许的
-- 没上锁的好处:1. 不必要的复杂度 2. 性能
-- 未命中,获取数据,设置缓存,返回
local obj, err = create_obj_fun(...)
if obj ~= nil then
-- 注意这里 set 的时候,值 = 对象+version
lru_obj:set(key, {val = obj, ver = version}, item_ttl)
end
return obj, err
end
-- 获取缓存
local cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
item_release, key, version)
-- 命中,直接返回
if cache_obj then
return cache_obj.val
end
-- 否则,进入 获取-设置流程
-- 1. 获取 lock
local lock, err = resty_lock:new(lock_shdict_name)
if not lock then
return nil, "failed to create lock: " .. err
end
-- 2. 上锁
local key_s = tostring(key)
log.info("try to lock with key ", key_s)
local elapsed, err = lock:lock(key_s)
if not elapsed then
return nil, "failed to acquire the lock: " .. err
end
-- 3. 再次获取缓存: 避免从 获取缓存失败 到 上锁成功 这段时间内 其他地方已经获取缓存并成功
cache_obj = fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
nil, key, version)
--- 3.1 命中则解锁,返回
if cache_obj then
lock:unlock()
log.info("unlock with key ", key_s)
return cache_obj.val
end
-- 4. 调用 create_obj_fun, 获取数据
local obj, err = create_obj_fun(...)
--- 4.1 获取成功,设置
if obj ~= nil then
lru_obj:set(key, {val = obj, ver = version}, item_ttl)
end
-- 如果获取成功, obj正确, err=nil; 如果获取失败,obj nil, err!=nil
--- 5. 解锁
lock:unlock()
log.info("unlock with key ", key_s)
-- 6. 返回
return obj, err
end
end
说明:
- 如果 opts.serial_creating = true, 在lrucache流程中会上锁,如果 opts.serial_creating = false或者没有设置,则不会上锁
- 如果当前的 phase nginx.get_phase() 不是
ssl_session_fetch、ssl_session_store、rewrite、access、content、timer
之一(can_yield_phases, 即属于不能 yield的 phase), 例如 rewrite 阶段、log 阶段等,那么 lrucache流程也不会上锁;- 即,如果配置了
serial_creating = true
, 当前 phase 也必须是can_yield_phases: ssl_session_fetch、ssl_session_store、rewrite、access、content、timer
,才会是附带上锁的逻辑 - 如果phase被yield,那么必须上锁,不上锁的话,执行一半被yeild, 当恢复执行时,获取的缓存对象可能不是最新,使用过程中会发现缓存被设置回旧的值。
- 即,如果配置了
大部分的使用场景,都只设置了 count/ttl/type
, 所以使用时几乎都没有上锁
local function fetch_valid_cache(lru_obj, invalid_stale, item_ttl,
item_release, key, version)
-- 从 lru 中获取 key 对应值
--- 如果存在且未过期, obj非空是, 否则obj = nil
--- 如果存在但过期了, obj=nil, stale_obj != nil
local obj, stale_obj = lru_obj:get(key)
-- 1. 命中,并且 version 一致,直接返回
if obj and obj.ver == version then
return obj
end
-- 2. 没有命中(如果有已过期的值, stale_obj非空),或者 version 不一致
--- 2.1 如果配置允许使用陈旧的值, 并且缓存中存在过期的值,且 version 一致
if not invalid_stale and stale_obj and stale_obj.ver == version then
-- 重新设置为陈旧的值,并且返回
lru_obj:set(key, stale_obj, item_ttl)
return stale_obj
end
-- 没有命中
--- a. key 对应 value 不存在, 此时 obj = nil
--- b. key 对应 value 存在,但是已过期,没有配置 invalid_stale = true, 此时 obj = nil
--- c. key 对应的 value 存在,但是 version不一致, 此时 obj != nil
--- 2.2 如果配置了 item_release, 并且 obj 非空(value存在但是version不一致),调用 item_release(是一个 hoook?)
---- 目的是,发现 version不一致,主动调用缓存主动刷新接口?
---- 目前项目中没有调用点
if item_release and obj then
item_release(obj.val)
end
--- 2.3 返回 nil
return nil
end
接口
lrucache中提供了一个快捷创建插件 lrucache的函数
---
-- Cache some objects for plugins to avoid duplicate resources creation.
--
-- @function core.lrucache.plugin_ctx
-- @tparam table lrucache LRUCache object instance.
-- @tparam table api_ctx The request context.
-- @tparam string extra_key Additional parameters for generating the lrucache identification key.
-- @tparam function create_obj_func Functions for creating cache objects.
-- If the object does not exist in the lrucache, this function is
-- called to create it and cache it in the lrucache.
-- @treturn table The object cached in lrucache.
-- @usage
-- local function create_obj() {
-- -- create the object
-- -- return the object
-- }
-- local obj, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_obj)
-- -- obj is the object cached in lrucache
local function plugin_ctx(lrucache, api_ctx, extra_key, create_obj_func, ...)
local key, ver = plugin_ctx_key_and_ver(api_ctx, extra_key)
return lrucache(key, ver, create_obj_func, ...)
end
-- 生成 key 和 version
-- key = api_ctx.conf_type .. "#" .. api_ctx.conf_id = route#123 每个路由唯一
-- version = api_ctx.conf_version = = route.modifiedIndex, 只要 route 配置不变,这个version 不变
local function plugin_ctx_key_and_ver(api_ctx, extra_key)
local key = api_ctx.conf_type .. "#" .. api_ctx.conf_id
if extra_key then
key = key .. "#" .. extra_key
end
return key, api_ctx.conf_version
end
使用示例
local lrucache = core.lrucache.new({
type = "plugin",
})
-- 这里创建一个携带 limit object 的 lrucache
-- 其中
-- 1. lrucache
-- 2. 没有设置 extra_key
-- 3. ctx, conf 为 apisix plugin 机制中的 ctx, conf
-- 4. create_limit_obj 为创建函数
local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_limit_obj, conf)
注意,这里会存在一个非常容易导致 bug 的点:假设我在公共模块配置了一个lrucache,然后在多个插件中调用,那么你会发现不同插件 lrucache 获取到的对象串
了, api_ctx.conf_type/api_ctx.conf_id
对于同一个资源是一致的, 所以同一个资源如果没有指定 extra_key
,那么多个插件调用生成的key是一样地!
解决:公共模块中如果使用 plugin_ctx, 需要额外指定 extra_key = plugin_name
-- ratelimit/init.lua
local lrucache = core.lrucache.new(
{
type = "plugin",
serial_creating = true,
}
)
# bug here
function _M.rate_limit(conf, ctx, key, count, time_window)
......
local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, nil, create_limit_obj, plugin_name)
end
# ok here
function _M.rate_limit(conf, ctx, plugin_name, key, count, time_window)
......
local lim, err = core.lrucache.plugin_ctx(lrucache, ctx, plugin_name, create_limit_obj, plugin_name)
end