LRU 缓存简介 [blog/lrualgorithm/whats]
LRU 缓存简介 [blog/lrualgorithm/whats]
大家好!今天我想和大家分享如何用 MoonBit 语言实现一个 LRU(Least Recently Used,最近最少使用)缓存算法。无论你是刚接触 MoonBit,还是想了解 LRU 算法的实现细节,这篇文章都会带你深入了解这个经典而实用的算法。
什么是 LRU 缓存?
LRU 缓存是一种常见的缓存淘汰策略,它基于一个简单的假设:最近访问过的数据在不久的将来很可能再次被访问。因此,当缓存空间不足需要淘汰数据时,应该优先淘汰最长时间未被访问的数据。
一个日常生活的例子
想象你的书桌上只能放 5 本书。每次你需要一本新书,但书桌已满时,你会把哪本书放回书架?最合理的选择是把最久没看的那本书放回去。这就是 LRU 策略的直观体现。
技术应用场景
LRU 缓存在计算机系统中有广泛的应用:
- 操作系统的页面置换:当物理内存不足时,系统需要决定将哪些页面从内存换出到磁盘
- 数据库的缓存管理:数据库会在内存中缓存经常访问的数据块,提高读写性能
- Web 服务器的缓存:缓存热门的网页、图片等资源,减少服务器负载
- 浏览器的缓存机制:存储最近浏览的网页,加快重复访问的速度
- CDN 内容分发网络:根据访问频率调整内容缓存策略
LRU 缓存的实际例子
假设我们有一个容量为 3 的 LRU 缓存,初始为空。现在执行以下操作:
put(1, "一")
→ 缓存现状:[(1, “一”)]put(2, "二")
→ 缓存现状:[(2, “二”), (1, “一”)]put(3, "三")
→ 缓存现状:[(3, “三”), (2, “二”), (1, “一”)]get(1)
→ 返回 “一”,缓存现状:[(1, “一”), (3, “三”), (2, “二”)] (注意 1 被移到了最前面)put(4, "四")
→ 缓存已满,淘汰最久未使用的 (2, “二”),缓存现状:[(4, “四”), (1, “一”), (3, “三”)]get(2)
→ 返回 None (不存在)
通过这个例子,我们可以看到 LRU 缓存如何维护“最近使用“的顺序,以及如何在缓存满时进行淘汰。
LRU 缓存的核心操作
一个标准的 LRU 缓存需要支持以下两个核心操作:
get(key): 获取缓存中键对应的值
- 如果键存在,返回对应的值,并将该键值对移动到“最近使用“的位置
- 如果键不存在,返回特殊值(如 None)
put(key, value): 向缓存中插入或更新键值对
- 如果键已存在,更新值,并将该键值对移动到“最近使用“的位置
- 如果键不存在,创建新的键值对,放到“最近使用“的位置
- 如果缓存已满,先删除“最久未使用“的键值对,再添加新的键值对
这两个操作的时间复杂度都应该是 O(1),这就需要我们精心设计数据结构。
为什么需要特殊的数据结构?
实现一个高效的 LRU 缓存面临两个主要挑战:
- 快速查找:我们需要 O(1) 时间确定一个键是否存在于缓存中
- 顺序维护:我们需要跟踪所有缓存项的使用顺序,以便知道哪个是“最久未使用“的
如果只用数组,查找需要 O(n) 时间;如果只用哈希表,无法跟踪使用顺序。因此,我们需要结合两种数据结构。
实现思路:哈希表 + 双向链表
LRU 缓存的经典实现方式是结合使用:
- 哈希表:提供 O(1) 的查找能力,键映射到链表中的节点
- 双向链表:维护数据的访问顺序,最近使用的在链表头,最久未使用的在链表尾
这种组合让我们能够同时满足快速查找和顺序维护的需求。
接下来,我们将用 MoonBit 语言一步步实现这个数据结构,从基础的节点定义,到完整的 LRU 缓存功能。