什么是 LRU ?
LRU是Least Recently Used的缩写,即最近最少使用,是一种常用的页面置换算法,选择最近最久未使用的页面予以淘汰。该算法赋予每个页面一个访问字段,用来记录一个页面自上次被访问以来所经历的时间 t,当须淘汰一个页面时,选择现有页面中其 t 值最大的,即最近最少使用的页面予以淘汰。
基于分值实现的 LRU 分析
class Item:
key = None
value = None
counter = 0
class LRUCache(dict):
capacity = 100
threshold = 0.5
_counter = 0
capacity 绝对有效内存(一定不会被回收), capacity + threshold * capacity 超出则触发缓存内存回收,回收 [capacity:] 部分, 设计 threshold * capacity 是为了避免频繁内存回收。
_counter 内容访问记录逻辑次数(非实际访问次)基值, 每次访问基值都会自增 1,保证最近访问的内容的 _counter 值是最大。(如下)
def _counter_inc(self):
self._counter += 1
return self._counter
获取缓存值时,更新其 _counter 值,如下
def get(self, key):
item = dict.__getitem__(self, key)
if item:
item.counter = self._counter(self)
return item.value
else:
return None
插入缓存值,校验内存空间,若超出内存空间,则触发内存空间回收,代码如下:
def set(self, key, value):
item = dict.__getitem__(self, key)
if item:
item.value = value
item.counter = self._counter
else:
item = Item(key=key, value=value, counter=self._counter())
dict.__setitem__(self, key, item)
if len(self) > capacity + capacity * threshold:
items = sorted(dict.values(self), key=lamaba x: x.counter, reverse=True)
for item in items[capacity:]:
del self[item.key]
基于双向链表实现的 LRU 分析(草稿)
class Node:
before = None
after = None
key = None
value = None
class LRUCache(dict):
capacity = 100
threshold = 0.5
head = None
tail = None
访问缓存值时,将最新被访问的数据移到链表头部,代码如下:
def get(self, key):
item = dict.__getitem__(self, key)
if item:
return None
afterNodeAccess(item)
return item
将最新访问数据移至链表头部,代码如下:
def afterNodeAccess(item):
if item == self.head:
return
before = item.before
after = item.after
head = self.head
tail = self.tail
self.head = item
item.after = head
head.before = item
before.after = after
if after:
after.before = before
else:
self.tail = before
插入缓存值,校验内存空间,若超出内存空间,则触发内存空间回收,代码如下:
def set(self, key, value):
item = dict.__getitem__(self, key)
if item:
item.value = value
else:
item = Item(key=key, value=value, before=None, after=None)
head = self.head
self.head = item
item.after = head
head.before = item
dict.__setitem__(self, key, item)
if len(self) > capacity + capacity * threshold:
for i in range(len(self) - capacity):
tail = self.tail
before = self.tail
before.after = None
self.tail = before
del self[tail.key]
Java 的 LinkedHashMap 实现了 LRU 算法,原理即是基于双向链表记录插入、访问的规则,在插入、访问元素时,变换链表。LinkedHashMap 默认插入作为排序,可以设定 accessOrder 为 True,来使得排序依据访问情况,具体内部实现逻辑与 HashMap 类似,可以根据插入、访问排序主要是重写了 newNode、afterNodeAccess 访问,该方法实现了对双向链表的操作,插入时将元素更新至链表尾部,访问时将数据更新至链表头部。
其它
LRU 算法弊端是存在偶发性、周期性的批量操会降低缓存的命中率,对缓存造成污染,下面几个就是改进算法。
LRU-K 会记录每条数据的访问历史,当达到 k 时,才将数据存放到缓存,在缓存内存回收时,缓存中越接近 k 的数据被优先删除。
Two queues(2Q)相当于 LRU-2,区别是访问历史(首次访问)数据缓存于 FIFO 队列,二次及以上的数据存放 LRU 缓存,FIFO 队列数据遵循该缓存的内存回收机制,LRU 缓存数据遵循该缓存的内存回收机制。
Multi Queue(MQ)该算法根据访问频率,分割出多个优先级别的缓存,访问频率越高的数据存放越高优先级的缓存中,各优先级根据访问频率增减数据在之间迁移,最低优先级的数据会被清除。