LRU 算法具体步骤:
新数据直接插入到列表头部
缓存数据被命中,将数据移动到列表头部
缓存已满的时候,移除列表尾部数据。
03、LRU 算法实现
上面例子中可以看到,LRU 算法需要添加头节点,删除尾结点。而链表添加节点/删除节点时间复杂度 O(1),非常适合当做存储缓存数据容器。但是不能使用普通的单向链表,单向链表有几点劣势:
每次获取任意节点数据,都需要从头结点遍历下去,这就导致获取节点复杂度为 O(N)。
移动中间节点到头结点,我们需要知道中间节点前一个节点的信息,单向链表就不得不再次遍历获取信息。
针对以上问题,可以结合其他数据结构解决。
使用散列表存储节点,获取节点的复杂度将会降低为 O(1)。节点移动问题可以在节点中再增加前驱指针,记录上一个节点信息,这样链表就从单向链表变成了双向链表。
综上使用双向链表加散列表结合体,数据结构如图所示:
LRU 算法改进方案
以下方案来源与 MySQL InnoDB LRU 改进算法
将链表拆分成两部分,分为热数据区,与冷数据区,如图所示。改进之后算法流程将会变成下面一样:
访问数据如果位于热数据区,与之前 LRU 算法一样,移动到热数据区的头结点。
插入数据时,若缓存已满,淘汰尾结点的数据。然后将数据插入冷数据区的头结点。
处于冷数据区的数据每次被访问需要做如下判断:若该数据已在缓存中超过指定时间,比如说 1 s,则移动到热数据区的头结点。若该数据存在在时间小于指定的时间,则位置保持不变。
对于偶发的批量查询,数据仅仅只会落入冷数据区,然后很快就会被淘汰出去。热门数据区的数据将不会受到影响,这样就解决了 LRU 算法缓存命中率下降的问题。
其他改进方法还有 LRU-K,2Q,LIRS 算法,感兴趣同学可以自行查阅。