已發布·持續改進
Algorithm
LRU 快取在滿了時淘汰最久未使用的項目,以雜湊表加雙向鏈結串列讓 get 與 put 都達到 O(1)。
LRU(Least Recently Used,最近最少使用)快取是一種容量固定的快取,滿了之後優先淘汰最長時間沒有被使用的項目。它依賴時間區域性,也就是最近用過的資料很可能很快再被用到;與 FIFO 不同,每次讀取都會更新項目的位置。標準實作把依鍵查找節點的雜湊表與依使用順序排列的雙向鏈結串列結合起來,讓 get 與 put 的平均時間都是 O(1)。
LRU 以原樣或近似的形式出現在系統的幾乎每一層:CPU 快取、作業系統頁面快取、資料庫緩衝池、Redis 的記憶體淘汰策略、CDN 與瀏覽器快取。標準函式庫也有現成的實作,例如 Python 的 functools.lru_cache 與 OrderedDict,Java 的 LinkedHashMap。由於它在一個設計中同時用到雜湊表與鏈結串列,也是程式設計面試中的經典題目。
建議先弄清 FIFO、LRU、LFU、TTL 等淘汰策略的差異以及命中率的概念,再手動追蹤一段簡短的請求序列。接著用字典與帶哨兵節點的雙向鏈結串列親自實作,並與 OrderedDict、LinkedHashMap 等函式庫版本比較。最後透過練習處理執行緒安全、快取失效以及與過期時間結合等實務問題。
每次成功的 get 或 put 都把項目移到最近使用的一端;空間不足時淘汰另一端的項目。
雜湊表依鍵直接找到節點,鏈結串列以 O(1) 取下節點並重新插入到前端,藉此維持順序。
get 與 put 只修改固定數量的指標,平均為常數時間;空間與容量成正比。
實際系統常用 CLOCK、偽 LRU、取樣 LRU、2Q 或 W-TinyLFU 來降低成本或抵禦大規模掃描。
dict 把鍵對應到節點,哨兵 head 與 tail 之間的雙向鏈結串列保存使用順序。get 把節點移到最前面;put 在快取已滿時淘汰 tail 前面的節點。執行 python lru_cache.py 會淘汰 "b",輸出 None 1 3。
lru_cache.py
class Node:
__slots__ = ("key", "value", "prev", "next")
def __init__(self, key=None, value=None):
self.key, self.value = key, value
self.prev = self.next = None
class LRUCache:
def __init__(self, capacity):
self.capacity = capacity
self.map = {} # key -> Node
self.head, self.tail = Node(), Node() # sentinels
self.head.next, self.tail.prev = self.tail, self.head
def _unlink(self, node):
node.prev.next, node.next.prev = node.next, node.prev
def _push_front(self, node):
node.prev, node.next = self.head, self.head.next
self.head.next.prev = node
self.head.next = node
def get(self, key, default=None):
node = self.map.get(key)
if node is None:
return default
self._unlink(node) # touch: move to the front
self._push_front(node)
return node.value
def put(self, key, value):
if self.capacity <= 0:
return
node = self.map.get(key)
if node is not None:
node.value = value
self._unlink(node)
self._push_front(node)
return
if len(self.map) >= self.capacity: # evict the least recently used
lru = self.tail.prev
self._unlink(lru)
del self.map[lru.key]
node = Node(key, value)
self.map[key] = node
self._push_front(node)
cache = LRUCache(2)
cache.put("a", 1)
cache.put("b", 2)
cache.get("a") # "a" is now the most recent
cache.put("c", 3) # full: evicts "b"
print(cache.get("b"), cache.get("a"), cache.get("c")) # None 1 3
python lru_cache.py共六章,帶你從安裝一步步認識 LRU 快取 的核心概念。
在這裡提問、分享經驗,交流關於 LRU 快取 的看法。
還沒有討論。來發起第一則吧。
0 則留言
登入 · 登入後即可留言。
來留下第一則留言吧。