当前位置: 首页 > 科技观察

说说缓存淘汰算法-LRU实现原理

时间:2023-03-21 22:27:17 科技观察

01。前言我们经常使用缓存来提高数据查询速度。由于缓存容量有限,当缓存容量达到上限时,需要删除部分数据,为新数据的添加腾出空间。.缓存数据不能随意删除。一般我们需要按照一定的算法来删除缓存数据。常用的淘汰算法有LRU、LFU、FIFO。在这篇文章中,我们将讨论LRU算法。02.LRU简介LRU是LeastRecentlyUsed的缩写。该算法认为最近使用的数据是流行数据,下次有很大概率会再次使用。而最近很少用到的数据,下次大概率用不到。当缓存容量满时,会优先淘汰最近很少使用的数据。假设现在缓存的内部数据如图所示:这里我们称链表的第一个节点为头节点,最后一个节点为尾节点。当调用缓存获取key=1的数据时,LRU算法需要将节点1移动到头节点,其他节点不变,如图。然后我们插入一个key=8的节点。此时缓存容量达到上限,需要删除数据再加入。由于每次查询都会将数据移动到头节点,没有被查询到的数据会下沉到尾节点,尾节点的数据可以认为是访问最少的数据,所以删除尾节点的数据。然后我们直接将数据添加到头节点。下面总结一下LRU算法的具体步骤:新数据直接插入链表头部。缓存的数据被命中,数据被移动到链表的头部。当缓存已满时,列表末尾的数据将被删除。03.LRU算法的实现从上面的例子可以看出,LRU算法需要添加头节点,删除尾节点。链表添加节点/删除节点的时间复杂度为O(1),非常适合作为存储缓存数据容器。但是,不能使用普通的单向链表。单向链表有几个缺点:每次获取任意节点数据,都需要从第一个节点开始遍历,导致获取节点的复杂度为O(N)。要将中间节点移动到头节点,我们需要知道中间节点之前节点的信息,而单向链表要再次遍历才能获得这些信息。对于以上问题,可以结合其他数据结构来解决。使用哈希表存储节点,获取节点的复杂度将降低到O(1)。对于节点移动的问题,可以在节点上加上前驱指针,记录前一个节点的信息,使链表由单向链表变为双向链表。综上所述,采用了双向链表和哈希表的结合,数据结构如图所示:双向链表中特意加入了两个“哨兵”节点,不用于存储任何数据。使用sentinel节点,在添加/删除节点时,不需要考虑边界节点的存在,简化了编程难度,降低了代码复杂度。LRU算法的实现代码如下。为了简化key和val,都认为是int类型。publicclassLRUCache{入口头,尾;内部容量;整数大小;Map缓存;publicLRUCache(intcapacity){this.capacity=capacity;//初始化链表initLinkedList();大小=0;缓存=新的HashMap<>(容量+2);}/***如果节点不存在,返回-1。如果存在,则将该节点移动到头节点,并返回该节点的数据。**@paramkey*@return*/publicintget(intkey){入口节点=cache.get(key);if(node==null){返回-1;}//有移动节点moveToHead(node);返回节点值;}/***将节点添加到头节点,如果容量已满,将删除尾节点**@paramkey*@paramvalue*/publicvoidput(intkey,intvalue){Entrynode=缓存.get(键);if(node!=null){node.value=value;moveToHead(节点);返回;}//不存在。先加入,再移除尾节点//此时容量已满,删除尾节点if(size==capacity){EntrylastNode=tail.pre;删除节点(最后一个节点);缓存.remove(lastNode.key);尺寸-;}//添加头节点EntrynewNode=newEntry();newNode.key=key;newNode.value=值;添加节点(新节点);cache.put(key,newNode);尺码++;}privatevoidmoveToHead(Entrynode){//先删除原节点的关系deleteNode(node);添加节点(节点);}privatevoidaddNode(Entrynode){head.next.pre=node;node.next=head.next;节点.pre=头;head.next=节点;}privatevoiddeleteNode(Entrynode){node.pre.next=node.next;node.next.pre=node.pre;}publicstaticclassEntry{publicEntrypre;接下来是公共条目;公钥;公共整数值;publicEntry(intkey,intvalue){this.key=键;this.value=值;}publicEntry(){}}privatevoidinitLinkedList(){head=newEntry();尾巴=新条目();head.next=尾巴;tail.pre=head;}publicstaticvoidmain(String[]args){LRUCache缓存=newLRUCache(2);缓存.put(1,1);缓存.put(2,2);System.out.println(cache.get(1));缓存.put(3,3);System.out.println(cache.get(2));}}04、LRU算法分析缓存命中率是缓存系统的一个非常重要的指标,如果缓存系统的缓存命中率过低,会导致查询流回数据库,从而导致增加数据库的压力。结合上面分析了LRU算法的优缺点。LRU算法的优点是算法实现起来不难。对于热点数据,LRU效率会很好。LRU算法的缺点是对于偶尔的批量操作,比如批量查询历史数据,有可能将缓存中的热门数据替换成这些历史数据,造成缓存污染,导致缓存命中率下降,减慢正常的数据查询。05.LRU算法改进方案下面方案的来源和MySQLInnoDBLRU改进算法把链表分成两部分,分为热数据区和冷数据区,如图。改进后算法流程会变成:如果访问的数据位于热点数据区,则和之前的LRU算法一样,将其移动到热点数据区的头节点。插入数据时,如果缓存满了,就会淘汰尾节点的数据。然后将数据插入冷数据区的头节点。每次访问冷数据区的数据,都需要做如下判断:如果数据在缓存中的时间超过了指定的时间,比如1s,则移动到热数据的头节点区域。如果数据存在的时间少于指定的时间,则位置保持不变。对于偶尔的批量查询,数据会简单地落入冷数据区,很快就会被淘汰。热门数据区的数据不会受到影响,解决了LRU算法缓存命中率下降的问题。其他改进方法还有LRU-K、2Q、LIRS算法,有兴趣的同学可以自行查看。