在互联网应用开发中,缓存技术是提升系统性能的关键手段之一。想象一下你正在使用一个电商APP,每次浏览商品详情时,如果系统都要从数据库重新查询商品信息,那响应速度会有多慢?这就是缓存存在的意义。
LRU(Least Recently Used)算法作为最常用的缓存淘汰策略之一,在大厂面试中出现频率极高。根据我的面试官经验,超过70%的候选人被要求手写LRU实现,但只有不到30%能完整解释其设计原理。下面我将结合多年开发经验,带你深入理解这个经典算法。
LRU的核心思想可以用图书馆借书的场景来类比:
这种策略基于"局部性原理":最近被访问的数据,短期内再次被访问的概率更高。实际测试数据显示,合理配置的LRU缓存命中率可达60-80%。
一个合格的LRU实现必须满足:
传统数组或链表结构无法同时满足这些要求,这就是为什么需要组合数据结构。
python复制class DLinkedNode:
def __init__(self, key=0, value=0):
self.key = key
self.value = value
self.prev = None
self.next = None
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.size = 0
self.cache = {}
# 使用伪头部和伪尾部节点
self.head = DLinkedNode()
self.tail = DLinkedNode()
self.head.next = self.tail
self.tail.prev = self.head
这个实现包含三个关键组件:
cache:提供O(1)的key查询添加节点到头部:
python复制def _add_to_head(self, node):
node.prev = self.head
node.next = self.head.next
self.head.next.prev = node
self.head.next = node
移除指定节点:
python复制def _remove_node(self, node):
node.prev.next = node.next
node.next.prev = node.prev
移动节点到头部:
python复制def _move_to_head(self, node):
self._remove_node(node)
self._add_to_head(node)
移除尾部节点:
python复制def _remove_tail(self):
node = self.tail.prev
self._remove_node(node)
return node
Python的collections.OrderedDict内部也是基于哈希表+双向链表实现,因此可以直接利用它简化代码:
python复制from collections import OrderedDict
class LRUCache:
def __init__(self, capacity: int):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key: int) -> int:
if key not in self.cache:
return -1
self.cache.move_to_end(key)
return self.cache[key]
def put(self, key: int, value: int) -> None:
if key in self.cache:
self.cache.move_to_end(key)
elif len(self.cache) >= self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value
注意:在Python 3.7+中,普通dict也保持了插入顺序,但缺少
move_to_end等关键方法,因此仍需使用OrderedDict。
原生实现不是线程安全的,可以通过加锁改造:
python复制import threading
class ThreadSafeLRUCache(LRUCache):
def __init__(self, capacity):
super().__init__(capacity)
self.lock = threading.Lock()
def get(self, key):
with self.lock:
return super().get(key)
def put(self, key, value):
with self.lock:
super().put(key, value)
添加缓存命中率统计:
python复制class MonitoredLRUCache(LRUCache):
def __init__(self, capacity):
super().__init__(capacity)
self.hits = 0
self.misses = 0
def get(self, key):
result = super().get(key)
if result == -1:
self.misses += 1
else:
self.hits += 1
return result
@property
def hit_rate(self):
total = self.hits + self.misses
return self.hits / total if total else 0
当缓存大量小对象时,双向链表节点的额外内存开销可能达到30-40%。解决方案:
__slots__减少Python对象内存占用python复制class DLinkedNode:
__slots__ = ['key', 'value', 'prev', 'next']
# 其余代码不变
恶意攻击者可能通过高频访问不存在的key导致缓存被刷。防护措施:
python复制class ProtectedLRUCache(LRUCache):
def __init__(self, capacity):
super().__init__(capacity)
self.nonexistent_access = OrderedDict()
self.max_nonexistent = 1000
def get(self, key):
if key not in self.cache:
self._record_nonexistent_access(key)
return -1
# 其余代码不变
def _record_nonexistent_access(self, key):
self.nonexistent_access[key] = time.time()
if len(self.nonexistent_access) > self.max_nonexistent:
self.nonexistent_access.popitem(last=False)
标准LRU对突发流量敏感,LRU-K通过记录最近K次访问时间来解决:
python复制class LRUKNode:
def __init__(self, key, value):
self.key = key
self.value = value
self.access_times = deque(maxlen=K) # 记录最近K次访问时间
class LRUKCache:
# 实现类似,但淘汰策略基于第K次最近访问时间
给缓存项添加过期时间:
python复制class TTLNode:
def __init__(self, key, value, ttl):
self.key = key
self.value = value
self.expire_at = time.time() + ttl
class TTL_LRUCache(LRUCache):
def get(self, key):
if key not in self.cache:
return -1
node = self.cache[key]
if time.time() > node.expire_at:
self._remove_node(node)
del self.cache[key]
return -1
self._move_to_head(node)
return node.value
在实际项目中,我建议先使用标准LRU实现,待性能监控数据表明需要优化时,再考虑这些高级变体。过早优化往往会导致系统复杂度不必要的增加。