LRU缓存及实现

一、淘汰策略

缓存:缓存作为一种平衡高速设备与低速设备读写速度之间差异而引入的中间层,利用的是局部性原理。比如一条数据在刚被访问过只有就很可能再次被访问到,因此将其暂存到内存中的缓存中,下次访问不用读取磁盘直接从内存中的缓存读取。而内存是有限的,无法无限制的添加数据。当缓存超过设置的容量的时候,在添加缓存就需要选择性的移除无效数据。需要具体的策略判定数据是否无效。

1、FIFO

FIFO:First In First Out,先进先出,淘汰缓存中最早添加的数据。认为缓存中最早添加的数据被在此使用的可能性就越小。实现可以使用一个队列,队列中的数据严格遵循先进先出,每次内存不够用,则直接淘汰队首元素。但是很多场景下,最早添加的元素也会被经常访问,因此这类数据会频繁的进出缓存,导致性能不佳。

2、LFU

LFU:Least Frequently Used,最少使用,淘汰缓存中使用频率最低的数据。认为数据过去访问的次数越多,将来更可能被访问,因此应该尽量不被淘汰。实现上,需要维护一个记录数据访问次数的数组,每次访问数据,访问次数+1,数组就要重新排序,在淘汰时,只需淘汰访问次数最少的数据。LFU的命中率很高,缓存更有效,但是每次访问数据,都需要重排访问次数数据,排序消耗很大。另外,数据访问模式的经常变化,会导致缓存的性能下降。比如微博热点事件,在某个时间点上访问量突然加大,导致访问次数很大,过段时间可能很少访问,但是已经记录了很高的访问次数,导致该数据在缓存中很难被淘汰。

3、LRU

LRU:Least Recently Used,最近最少被使用,FIFO和LFU的这种方案。认为最近使用过的数据,在将来更可能被访问,尽量不被淘汰。相对于LFU中需要记录数据的访问次数,LRU只需要维护一个队列,队列头部保存刚被访问过的数据,队尾是最近最少未被访问的数据,缓存容量不够时候可以直接淘汰。

二、LRU实现

1、数据结构

  • 缓存字典:LRU对象需要包含一个字典,用于缓存数据。这样根据键查找值和插入新值的复杂度都是O(1)。
  • 双向链表:双向链表维护数据的最近最少使用状态。使用双向链表可以保证队尾删除节点和队头添加节点的复杂度都是O(1)

字典的键是查找值,键对应的值是双向链表对应的节点引用,这样根据字典就可以找到双向链表中的节点,进而调整双向链表中节点的顺序,更新数据的状态。

2、实现

class DLinkList:
    """定义双向链表""""
    def __init__(self, key=0, value=0):
        self.key = key
        self.value = value
        self.pre = None
        self.next = None


class LRUCache:
    """LRU缓存"""
    def __init__(self, capacity: int):
        # 初始化容量和占用大小
        self.capacity = capacity
        self.size = 0
        self.cache = dict()
        # 初始化头结点和尾节点
        self.tail = DLinkList()
        self.head = DLinkList()
        self.tail.pre = self.head
        self.head.next = self.tail

    def get(self, key: int) -> int:
        # 未命中缓存
        if key not in self.cache:
            return -1
        # 命中缓存修改将节点前移首部
        node = self.cache[key]
        self.moveToHead(node)
        return node.value

    def put(self, key: int, value: int) -> None:
        # 新增缓存
        if key not in self.cache:
            node = DLinkList(key, value)
            self.cache[key] = node
            self.addToHead(node)
            self.size += 1
            if self.size > self.capacity:
                removed_node = self.removeTail()
                del self.cache[removed_node.key]
                self.size -= 1
        else:
            # 更新缓存
            node = self.cache[key]
            node.value = value
            self.moveToHead(node)

    def removeTail(self):
        # 移除尾部节点
        node = self.tail.pre
        self.removeNode(node)
        # 这里仍旧需要将删除的节点返回,为了方便cache字典删除键值对
        return node

    def removeNode(self, node):
        # 移除某个节点
        node.next.pre = node.pre
        node.pre.next = node.next

    def moveToHead(self, node):
        # 节点前移首部
        self.removeNode(node)
        self.addToHead(node)

    def addToHead(self, node):
        # 添加到首部
        node.next = self.head.next
        node.pre = self.head
        self.head.next.pre = node
        self.head.next = node

注:

  • 字典的定义的键是查找值,键对应的值是双向链表对应节点的引用。
  • 双线链表的节点保存的键值对,好处在于,淘汰尾部节点的时候可以直接从节点取出键,进而删除字典中的键值对。
  • 查找数据的时候,如果缓存未命中,可以采用回调函数去查找数据库真实数据。如果命中,则返回数据的同时,仍需要将该数据对应的节点调整到链表首部,更新最近最少使用状态。
  • 添加数据的时候,如果缓存容量满了,则需要淘汰链表尾部节点,也就是最近最少访问的节点。

相关链接:leetcode:lru缓存

你可能感兴趣的:(LRU缓存及实现)