LRU 缓存将被访问条目移到最近端,并从最旧端选择淘汰对象。

LRU 缓存淘汰最久未用的条目

LRU 按最近使用顺序选择淘汰对象,而不是按历史热度、存放年龄或业务重要程度选择。有界缓存需要腾出空间时,精确的最近最少使用策略会移除最后一次有效访问发生得最早的条目。它利用的是时间局部性,既不保证命中有价值的数据,也不保证留下来的数据仍然新鲜。 沿访问顺序找出淘汰对象 假设缓存只能存三个条目,并按从旧到新的方向列出。依次加载 A、B、C 后,顺序为 A、B、C;再次读取 A,顺序变成 B、C、A;此时加载 D,就淘汰 B,留下 C、A、D。普通字典即使保留插入顺序,也不会因为某个条目被读取就自动调整它的位置。 实现必须明确哪些操作算作一次访问。常见精确方案把查找表与双向链表结合起来,以较低成本完成查找、移到最近端和从最旧端移除。维护顺序需要额外元数据,也让缓存命中变成状态更新;并发实现还必须协调这些更新,不能把读取缓存理解成永远没有写入开销。 精确顺序与采样近似不是同一契约 Redis 使用近似 LRU:抽取候选并维护较好的淘汰候选池,而不是为每个键保存完整的精确访问顺序。采样数量可以配置,更多采样用额外计算换取更接近精确 LRU 的结果。它不承诺下面小模型的逐个淘汰顺序,也不能由此推导所有淘汰操作都有固定耗时。 候选资格同样重要。Redis 的 allkeys-lru 可以淘汰任意键,volatile-lru 则只考虑带过期信息的键。设置了有效期,并不保证键在截止时间之前不会被淘汰;如果没有符合条件的带过期时间的键,volatile 策略在内存压力处理上会表现得像 noeviction。 容量约束不是新鲜度约束 限制三个条目,不等于限制三千字节。不同键、值和维护结构占用不同空间,按数量限制的缓存仍可能持有很大的对象。Python 的 lru_cache 会持有参数与返回值的引用,直到条目被淘汰或缓存被清空。选择容量单位时,应对应真正需要控制的资源,而不是只看条目数是否稳定。 最近访问也不能说明源数据有没有变化。频繁读取一个过时答案,反而可能让它一直留在缓存中。有效期、显式失效和版本化键分别处理新鲜度问题;淘汰规则既不是一致性协议,也不是备份策略,不能因为缓存命中就跳过业务对数据有效性的判断。 坏掉的样子 一次扫描大量只用一遍的条目,可能挤掉本来有用的工作集。容量为三时循环访问四个不同键,即使这些键都在被反复请求,也可能每次都未命中。LRU 不会因为过去访问次数很多就永久保留一个键。LFU、插入前的准入策略或调整容量可能适合某些负载,但不能仅凭“读多”就自动选出正确方案。 应在有代表性的时间段一起观察命中、未命中、淘汰、保留字节数以及未命中的代价。很高的命中率也可能掩盖少量特别昂贵的回源;单看淘汰数量,不知道被移除的数据是否有价值。修改策略前,先还原实际访问轨迹,再解释缓存为什么做出了这些选择。 缓存命中还必须允许安全复用。带副作用的函数,或每次调用都必须创建独立可变对象的函数,不适合直接做记忆化。Python 的 lru_cache 能保持多线程下内部结构一致,但同时发生的未命中仍可能让被包装函数执行多次。线程安全不等于抑制同一项工作被重复执行。 快速验证 将代码保存为 lru_check.py,用 Python 3.10 以上版本执行 python3 lru_check.py。实验已在 Python 3.14.4 上运行,只使用标准库:OrderedDict 展示精确访问顺序,真正的 functools 装饰器另行验证记忆化行为。不启动 Redis,不发送网络请求,也不写文件。 ...

十一月 12, 2027 · 2 分钟 · XB Field Notes