Bootstrap

lua-lru缓存算法解析

lua-lru 是 Lua 语言中的一个 LRU(Least Recently Used,最近最少使用)缓存实现,是一个基于哈希表和双向链表的数据结构,用于管理缓存中的数据项,并根据访问频率自动淘汰最不常用的数据项

-- lua-lru, LRU cache in Lua
-- Copyright (c) 2015 Boris Nagaev
-- See the LICENSE file for terms of use.

local lru = {}

function lru.new(max_size, max_bytes)
    assert(max_size >= 1, "max_size must be >= 1")
    assert(not max_bytes or max_bytes >= 1,
        "max_bytes must be >= 1")

    -- current size
    local size = 0
    local bytes_used = 0

    -- map is a hash map from keys to tuples
    -- tuple: value, prev, next, key
    -- prev and next are pointers to tuples
    local map = {}

    -- indices of tuple
    local VALUE = 1
    local PREV = 2
    local NEXT = 3
    local KEY = 4
    local BYTES = 5

    -- newest and oldest are ends of double-linked list
    local newest = nil  -- first
    local oldest = nil  -- last

    local removed_tuple -- created in del(), removed in set()

    -- remove a tuple from linked list
    local function cut(tuple)
        local tuple_prev = tuple[PREV]
        local tuple_next = tuple[NEXT]
        tuple[PREV] = nil
        tuple[NEXT] = nil
        if tuple_prev and tuple_next then
            tuple_prev[NEXT] = tuple_next
            tuple_next[PREV] = tuple_prev
        elseif tuple_prev then
            -- tuple is the oldest element
            tuple_prev[NEXT] = nil
            oldest = tuple_prev
        elseif tuple_next then
            -- tuple is the newest element
            tuple_next[PREV] = nil
            newest = tuple_next
        else
            -- tuple is the only element
            newest = nil
            oldest = nil
        end
    end

    -- insert a tuple to the newest end
    local function setNewest(tuple)
        if not newest then
            newest = tuple
            oldest = tuple
        else
            tuple[NEXT] = newest
            newest[PREV] = tuple
            newest = tuple
        end
    end

    local function del(key, tuple)
        map[key] = nil
        cut(tuple)
        size = size - 1
        bytes_used = bytes_used - (tuple[BYTES] or 0)
        removed_tuple = tuple
    end

    -- removes elemenets to provide enough memory
    -- returns last removed element or nil
    local function makeFreeSpace(bytes)
        while size + 1 > max_size or
            (max_bytes and bytes_used + bytes > max_bytes)
        do
            assert(oldest, "not enough storage for cache")
            del(oldest[KEY], oldest)
        end
    end

    local function get(_, key)
        local tuple = map[key]
        if not tuple then
            return nil
        end
        cut(tuple)
        setNewest(tuple)
        return tuple[VALUE]
    end

    local function set(_, key, value, bytes)
        local tuple = map[key]
        if tuple then
            del(key, tuple)
        end
        if value ~= nil then
            -- the value is not removed
            bytes = max_bytes and (bytes or #value) or 0
            makeFreeSpace(bytes)
            local tuple1 = removed_tuple or {}
            map[key] = tuple1
            tuple1[VALUE] = value
            tuple1[KEY] = key
            tuple1[BYTES] = max_bytes and bytes
            size = size + 1
            bytes_used = bytes_used + bytes
            setNewest(tuple1)
        else
            assert(key ~= nil, "Key may not be nil")
        end
        removed_tuple = nil
    end

    local function delete(_, key)
        return set(_, key, nil)
    end

    local function mynext(_, prev_key)
        local tuple
        if prev_key then
            tuple = map[prev_key][NEXT]
        else
            tuple = newest
        end
        if tuple then
            return tuple[KEY], tuple[VALUE]
        else
            return nil
        end
    end

    -- returns iterator for keys and values
    local function lru_pairs()
        return mynext, nil, nil
    end

    local mt = {
        __index = {
            get = get,
            set = set,
            delete = delete,
            pairs = lru_pairs,
        },
        __pairs = lru_pairs,
    }

    return setmetatable({}, mt)
end

return lru

主要功能和作用

1. 缓存管理:

  • lua-lru 可创建一个最多可以存储 max_size 个数据项的缓存,并且可以选择限制缓存的总字节数 max_bytes
  • 当缓存达到最大容量时,会自动淘汰最久未使用的数据项,以腾出空间给新数据

2. 数据存储与访问:

  • set(key, value, bytes):将数据项存储到缓存中,如果缓存中已经存在相同的 key,则会替换旧数据,如果缓存达到最大容量或字节限制,会自动淘汰最久未使用的数据项
  • get(key):从缓存中获取数据项,如果数据项存在,将其移动到链表的最新位置(即标记为最近使用)
  • delete(key):从缓存中删除指定的数据项

3. 迭代器:

  • lru_pairs():返回一个迭代器,用于遍历缓存中的所有数据项,迭代器按最近使用到最久未使用的顺序遍历

4. 容量管理:

  • makeFreeSpace(bytes):当缓存即将超出容量限制时,自动删除最久未使用的数据项,直到有足够的空间或字节数

具体实现细节

  • 双向链表:用于维护数据项的访问顺序,最近访问的数据项放在链表的前端,最久未访问的数据项放在链表的后端
  • 哈希表:用于快速查找数据项,哈希表的键是缓存的键,值是一个包含数据、链表前后指针等信息的数据结构
  • 自动淘汰:当缓存达到最大容量或字节数限制时,通过删除链表尾部的数据项来释放空间

使用场景

  • 数据缓存:适用于需要缓存数据的场景,特别是在数据访问频率高但存储空间有限的情况下
  • 限流:可以用于实现限流机制,淘汰掉长时间未使用的数据项,确保缓存中保留的是最近活跃的数据
  • 性能优化:在一些需要频繁访问数据但数据量较大的场景中,通过 LRU 缓存可以显著提高访问性能

使用示例

local lru = require("lru")

local cache = lru.new(3, 100) -- 创建一个最多存储3个数据项,最大100字节的缓存

cache:set("key1", "value1", 10) -- 存储数据项,占用10字节
cache:set("key2", "value2", 20) -- 存储数据项,占用20字节
cache:set("key3", "value3", 30) -- 存储数据项,占用30字节

print(cache:get("key1")) -- 输出 "value1"

cache:set("key4", "value4", 40) -- 存储新数据项,占用40字节,淘汰 "key1" 因为它是最近最少使用的

print(cache:get("key1")) -- 输出 nil,因为 "key1" 已经被淘汰

for key, value in cache:pairs() do
    print(key, value) -- 按最近使用到最久未使用的顺序遍历缓存
end
;