LRU 缓存原理与 C++ 实践示例

文章目录

LRU 缓存原理与 C++ 实践示例

什么是 LRU

LRU(Least Recently Used),最近最少使用,是一种常见的缓存淘汰算法。

核心思想:如果一个数据最近被访问过,那么它在未来被访问的概率也更高;反之,很久没有被访问的数据,未来访问概率低,缓存满的时候优先把它淘汰掉。

缓存容量是有限的,当缓存达到上限,插入新元素时,需要剔除掉「最久没有使用」的那条数据,腾出空间存放新数据。

LRU 需要满足的操作

  1. get(key) :读取 key。
    • key存在:返回 value,并且把该节点标记为最近使用;
    • key不存在:返回空。
  2. put(key, value) :写入键值对。
    • key已存在:更新 value,标记为最近使用;
    • key不存在:插入新节点。如果缓存已满,删除最久未使用的节点,再插入新节点。

数据结构选型

要做到 get、put 平均 O(1) 时间复杂度:

  • 哈希表(unordered_map):实现 key 到节点的快速映射,O(1) 查找。
  • 双向链表 :维护访问时序。链表头部代表最近使用 ,链表尾部代表最久未使用 。
    • 访问到某个节点:把节点移动到链表头部;
    • 缓存淘汰:直接删除链表尾部节点。

单向链表无法 O(1) 删除中间节点,所以必须双向链表。


C++ 常见使用场景

  1. 服务端内存缓存:限制内存占用,缓存数据库查询结果,避免频繁查库。比如用户信息、配置信息缓存。
  2. 本地资源缓存:文件解析结果、图片解码结果缓存,防止内存暴涨。
  3. 中间件/网关:限流、会话缓存、请求结果缓存。
  4. 嵌入式程序:有限内存下缓存高频访问数据。

C++ 标准库没有直接提供 LRU 容器,一般两种方案:

  1. 自己实现 unordered_map + list;
  2. 使用开源库(Abseil absl::LRUCache,folly LRUCache)。

下面给出手写简易 LRUCache 实现,基于 C++17。

完整可运行示例

复制代码
#include <iostream>
#include <unordered_map>
#include <list>
#include <utility>

/**
 * LRUCache 简易实现
 * key: int, value: int
 */
class LRUCache {
public:
    // list 存储 pair<key, value>,头部是最近使用,尾部最久未使用
    using ListNode = std::pair<int, int>;
    std::list<ListNode> m_list;
    // key -> list 的迭代器
    std::unordered_map<int, std::list<ListNode>::iterator> m_map;
    int m_capacity;

    explicit LRUCache(int capacity) : m_capacity(capacity) {}

    int get(int key) {
        auto it = m_map.find(key);
        if (it == m_map.end()) {
            return -1; // 不存在
        }
        // 取出节点,移动到链表头部
        auto node_it = it->second;
        int val = node_it->second;
        // splice:转移节点,不会拷贝,O(1)
        m_list.splice(m_list.begin(), m_list, node_it);
        return val;
    }

    void put(int key, int value) {
        auto it = m_map.find(key);
        if (it != m_map.end()) {
            // key已存在,更新value,挪到头部
            auto node_it = it->second;
            node_it->second = value;
            m_list.splice(m_list.begin(), m_list, node_it);
            return;
        }

        // 容量满,淘汰尾部
        if ((int)m_list.size() >= m_capacity) {
            auto last = m_list.back();
            int del_key = last.first;
            m_map.erase(del_key);
            m_list.pop_back();
        }

        // 插入新节点到头部
        m_list.emplace_front(key, value);
        m_map[key] = m_list.begin();
    }

    // 打印缓存内容,用于调试
    void print() const {
        for (const auto& p : m_list) {
            std::cout << "[" << p.first << ":" << p.second << "] ";
        }
        std::cout << "\n";
    }
};

// 模拟业务场景:缓存数据库查询结果
int main() {
    // 缓存最多3条记录
    LRUCache cache(3);

    std::cout << "put(1,100)\n";
    cache.put(1, 100);
    cache.print();

    std::cout << "put(2,200)\n";
    cache.put(2, 200);
    cache.print();

    std::cout << "put(3,300)\n";
    cache.put(3, 300);
    cache.print();

    std::cout << "get(1) -> " << cache.get(1) << "\n";
    cache.print();

    std::cout << "put(4,400) 缓存满,淘汰最久未使用的key=2\n";
    cache.put(4, 400);
    cache.print();

    std::cout << "get(2) -> " << cache.get(2) << "\n";

    return 0;
}

输出结果:

复制代码
put(1,100)
[1:100]
put(2,200)
[2:200] [1:100]
put(3,300)
[3:300] [2:200] [1:100]
get(1) -> 100
[1:100] [3:300] [2:200]
put(4,400) 缓存满,淘汰最久未使用的key=2
[4:400] [1:100] [3:300]
get(2) -> -1

逻辑解释:访问 key=1 之后,1被挪到链表头部;插入4时容量已满,删除尾部key=2。

实际项目中的注意点

  1. 线程安全 :上面示例非线程安全。多线程环境需要加 std::mutex 保护 get/put。
  2. 性能坑点 :std::list::splice 本身O(1),但是 unordered_map 存在哈希冲突。高并发场景考虑使用成熟开源库,不要裸写。
  3. key/value 类型扩展:示例用int,实际可以封装模板类支持任意key、value。
  4. LRU 的局限 :存在缓存污染问题:一次性批量读取大量冷数据,会把热点数据挤出去。衍生算法:LRU‑K、LFU。

开源工业级实现参考

  1. Abseil absl::LRUCache:Google开源,模板实现,支持自定义容量,工业级质量。
  2. Facebook Folly LRUCache:支持分片、统计、过期,适合服务端C++。
  3. 不建议生产环境直接复制上面手写版本,手写适合学习原理。

小结

  • LRU 淘汰最近最少使用的数据,是内存缓存最经典算法。
  • C++ 标准库没有内置LRU,底层依靠 unordered_map + std::list 组合实现O(1)读写。
  • 使用场景:数据库结果缓存、资源缓存、网关缓存;生产优先使用成熟开源组件。
相关推荐
toooooop81 小时前
FastAdmin Hook 机制与插件错误透传:`$result` 之外的信封
开发语言·fastadmin
小马过河R1 小时前
EdgeOne深度解析:从原理、适用场景到COS接入的技术逻辑
java·开发语言·前端·spring·cos
多弗朗皮卡丘1 小时前
C++ 红黑树
开发语言·c++·红黑树
ZoeLandia2 小时前
TypeScript 04:类型断言与类型守卫
开发语言·前端·typescript
geovindu2 小时前
rust: pool Pattern
开发语言·后端·设计模式·rust·对象池简写模式
曼巴UE52 小时前
UE 属性同步
网络·c++·ue5
无忧.芙桃2 小时前
图的精髓:邻接矩阵与遍历全解析
c++·算法·面试·深度优先·广度优先
jimy12 小时前
标准的动态类型定义与“多态”无关,非多态对象同样会出现动态类型不同于静态类型的情形
开发语言·c++
冬、不眠的夜2 小时前
Nuitka 将 python 程序编译成 exe 指南
开发语言·python