Python 内存缓存 LFU 淘汰算法实践:基于双向链表与频次哈希桶的 O(1) 淘汰器实现

举报
yd_236210837 发表于 2026/11/06 10:39:46 2026/11/06
【摘要】 一、问题背景:高频热点与时间衰减冲突下的缓存淘汰困境在高性能云原生微服务与本地内存缓存架构中,缓存空间总是有限的,必须借助置换算法剔除低价值数据。最常用的 LRU(Least Recently Used,最近最少使用)算法存在明显的缺陷——容易被偶发的批量全表扫描(冷数据一次性突发访问)直接冲垮,将原本极具业务价值的高频热点数据无辜淘汰(缓存污染)。相比之下,LFU(Least Frequ...

一、问题背景:高频热点与时间衰减冲突下的缓存淘汰困境

在高性能云原生微服务与本地内存缓存架构中,缓存空间总是有限的,必须借助置换算法剔除低价值数据。最常用的 LRU(Least Recently Used,最近最少使用)算法存在明显的缺陷——容易被偶发的批量全表扫描(冷数据一次性突发访问)直接冲垮,将原本极具业务价值的高频热点数据无辜淘汰(缓存污染)。

相比之下,LFU(Least Frequently Used,最不经常使用) 算法更符合数据长期访问热度特征:优先保留被调用次数最多的热点数据,在面对突发冷数据时具有极强的抗干扰韧性。

然而,朴素的 LFU 实现如果每次淘汰时遍历搜索最低频次节点,或者利用二叉堆(Heap)排序,时间复杂度将退化至 O(N)O(N) 或 O(log⁡N)O(\log N)。为了在纳秒级高频读写场景下保持无锁/极低锁开销,必须将 get() 和 put() 的复杂度彻底压至严苛的 O(1)O(1)。

二、双哈希表 + 双向链表桶的 O(1)O(1) 算法核心设计

实现严格 O(1)O(1) 复杂度的 LFU 核心在于维护两层索引与一个全局最小频次指针:

  1. 键值索引表 key_map: Dict[Key, Node]:实现基于 Key 的 O(1)O(1) 快速查找。
  2. 频次哈希桶 freq_map: Dict[Freq, DoublyLinkedList]:以访问频次作为 Key,Value 是一个双向链表。每个双向链表内部管理具有相同访问频次的所有数据节点,并按访问时间先后保持严格时序(新访问的插入链表头部,最久未访问的沉在尾部)。
  3. 全局最小频次追踪 min_freq:
    • 当插入全新数据时,其频次为 1,直接重置 min_freq = 1。
    • 当已有节点因被读取或重写导致频次由 ff 升为 f+1f+1 时,若原本 ff 对应的链表变空且刚好等于 min_freq,则原子自增 min_freq += 1。
    • 当缓存容量满溢触发淘汰时,直接定位到 freq_map[min_freq] 双向链表的尾部节点 remove_last(),并在常数时间 O(1)O(1) 内摘除并同步从 key_map 中清理,完美实现“淘汰最低频次,同频次下淘汰最久未访问(Tie-breaker: LRU)”。

三、完整可运行代码实现

以下脚本在 Python 3.11 标准库环境下直接运行,无任何第三方依赖,包含 ThreadSafeLFUCache 完整实现与高并发逻辑测试:

import threading
import time
from typing import Dict, Optional


class Node:
    """双向链表节点:存储键值与访问频次"""

    def __init__(self, key: str, value: any):
        self.key = key
        self.value = value
        self.freq = 1
        self.prev: Optional["Node"] = None
        self.next: Optional["Node"] = None


class DoublyLinkedList:
    """双向链表:用于管理相同访问频次的节点时序(头部最新,尾部最旧)"""

    def __init__(self):
        self.dummy_head = Node("", None)
        self.dummy_tail = Node("", None)
        self.dummy_head.next = self.dummy_tail
        self.dummy_tail.prev = self.dummy_head
        self.size = 0

    def add_first(self, node: Node):
        node.next = self.dummy_head.next
        node.prev = self.dummy_head
        self.dummy_head.next.prev = node
        self.dummy_head.next = node
        self.size += 1

    def remove(self, node: Node):
        node.prev.next = node.next
        node.next.prev = node.prev
        self.size -= 1

    def remove_last(self) -> Optional[Node]:
        if self.size == 0:
            return None
        last_node = self.dummy_tail.prev
        self.remove(last_node)
        return last_node


class ThreadSafeLFUCache:
    """基于双向链表与频次哈希桶的 O(1) 线程安全 LFU 缓存淘汰器"""

    def __init__(self, capacity: int):
        if capacity <= 0:
            raise ValueError("capacity 必须大于 0")
        self.capacity = capacity
        self.min_freq = 0
        self.key_map: Dict[str, Node] = {}
        self.freq_map: Dict[int, DoublyLinkedList] = {}
        self._lock = threading.Lock()

    def _update_freq(self, node: Node):
        freq = node.freq
        self.freq_map[freq].remove(node)
        if self.freq_map[freq].size == 0 and self.min_freq == freq:
            self.min_freq += 1
            del self.freq_map[freq]

        node.freq += 1
        new_freq = node.freq
        if new_freq not in self.freq_map:
            self.freq_map[new_freq] = DoublyLinkedList()
        self.freq_map[new_freq].add_first(node)

    def get(self, key: str) -> Optional[any]:
        with self._lock:
            if key not in self.key_map:
                return None
            node = self.key_map[key]
            self._update_freq(node)
            return node.value

    def put(self, key: str, value: any):
        with self._lock:
            if key in self.key_map:
                node = self.key_map[key]
                node.value = value
                self._update_freq(node)
                return

            if len(self.key_map) >= self.capacity:
                min_list = self.freq_map[self.min_freq]
                evicted_node = min_list.remove_last()
                if evicted_node:
                    del self.key_map[evicted_node.key]

            new_node = Node(key, value)
            self.key_map[key] = new_node
            self.min_freq = 1
            if 1 not in self.freq_map:
                self.freq_map[1] = DoublyLinkedList()
            self.freq_map[1].add_first(new_node)


def verify_lfu_cache():
    cache = ThreadSafeLFUCache(capacity=3)
    cache.put("k1", "v1")
    cache.put("k2", "v2")
    cache.put("k3", "v3")

    cache.get("k1")  # freq=2
    cache.get("k1")  # freq=3
    cache.get("k2")  # freq=2

    # 插入第 4 个元素 k4,淘汰最低频次(freq=1)的 k3
    cache.put("k4", "v4")

    k3_val = cache.get("k3")  # None
    k1_val = cache.get("k1")  # "v1"
    k2_val = cache.get("k2")  # "v2"
    k4_val = cache.get("k4")  # "v4"

    def worker(wid: int):
        for i in range(100):
            cache.put(f"key-{wid}-{i%5}", f"val-{wid}-{i}")
            cache.get(f"key-{wid}-{i%5}")

    threads = [threading.Thread(target=worker, args=(i,)) for i in range(4)]
    t0 = time.monotonic()
    for t in threads:
        t.start()
    for t in threads:
        t.join()
    elapsed_ms = round((time.monotonic() - t0) * 1000)

    print(
        f"[Result] LFU 缓存 O(1) 淘汰逻辑验证通过: k3淘汰={k3_val is None}, "
        f"k1保留={k1_val == 'v1'}, k2保留={k2_val == 'v2'}, k4保留={k4_val == 'v4'}, "
        f"4线程高并发测试耗时={elapsed_ms}ms, 最终容量={len(cache.key_map)}/3"
    )


if __name__ == "__main__":
    verify_lfu_cache()

四、实际运行验证与生产工程落地避坑指南

在 Python 3.11 环境下执行上述脚本,实测输出如下:

  • [Result] LFU 缓存 O(1) 淘汰逻辑验证通过: k3淘汰=True, k1保留=True, k2保留=True, k4保留=True, 4线程高并发测试耗时=1ms, 最终容量=3/3

生产工程落地三大核心考量:

  1. 防止历史热点频次永久虚高(频次衰减机制):LFU 经典缺点是某些早期高频访问的数据在业务下线后仍长期霸占内存。生产实践中应引入定时衰减(Aging)或对频次整体减半/采用 W-TinyLFU(Count-Min Sketch 衰减),定期重置冷态数据。
  2. 结合分段锁(Lock Striping)释放锁竞争:在超高并发只读场景下,可将上述 ThreadSafeLFUCache 按 Hash 分段划分为多个独立的 LFU 分段实例,彻底消除全局互斥锁的线程排队瓶颈。
【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。