• 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缓存

  • 相关阅读:
    linux系统性能监控常用命令
    如何在windows的DOS窗口中正常显示中文(UTF-8字符)
    在Windows的CMD中如何设置支持UTF8编码?
    设置cmd的codepage的方法
    Oracle字符集转换
    移动端跨平台开发的深度解析
    类型擦除是抽象泛型的实例化的过程
    FP又称为Monadic Programming
    深入剖析Swift性能优化
    真实世界中的 Swift 性能优化
  • 原文地址:https://www.cnblogs.com/welan/p/15908967.html
Copyright © 2020-2023  润新知