java先进先出淘汰算法_聊聊缓存淘汰算法-LRU 实现原理

本文介绍了LRU(Least Recently Used)缓存淘汰算法的原理,它认为最近使用的数据更可能再次被使用。当缓存满时,LRU会删除最近最少使用的数据。文章通过一个双向链表加散列表的实现展示了LRU算法的详细步骤,并讨论了其在处理批量操作时的不足,提出了改进方案,包括将链表拆分为热数据和冷数据区,以提高缓存命中率。

前言

我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来。缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据。常用淘汰算法有 lru,lfu,fifo,这篇文章我们聊聊 lru 算法。

lru 简介

lru 是 least recently used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大概率将会再次被使用。而最近很少被使用的数据,很大概率下一次不再用到。当缓存容量的满时候,优先淘汰最近很少使用的数据。

假设现在缓存内部数据如图所示:

b8f108eeb4b3888e47478834396eccc4.png

这里我们将列表第一个节点称为头结点,最后一个节点为尾结点。

当调用缓存获取 key=1 的数据,lru 算法需要将 1 这个节点移动到头结点,其余节点不变,如图所示。

37c7b7bca9cde8e77e1d23d2488b76f3.png

然后我们插入一个 key=8 节点,此时缓存容量到达上限,所以加入之前需要先删除数据。由于每次查询都会将数据移动到头结点,未被查询的数据就将会下沉到尾部节点,尾部的数据就可以认为是最少被访问的数据,所以删除尾结点的数据。

99326e28681cddebe5e16452979ca9eb.png

然后我们直接将数据添加到头结点。

e41c54c6f1ccc13c248ef05466d63dcb.png

这里总结一下 lru 算法具体步骤:

新数据直接插入到列表头部

缓存数据被命中,将数据移动到列表头部

缓存已满的时候,移除列表尾部数据。

lru 算法实现

上面例子中可以看到,lru 算法需要添加头节点,删除尾结点。而链表添加节点/删除节点时间复杂度 o(1),非常适合当做存储缓存数据容器。但是不能使用普通的单向链表,单向链表有几点劣势:

每次获取任意节点数据,都需要从头结点遍历下去,这就导致获取节点复杂度为 o(n)。

移动中间节点到头结点,我们需要知道中间节点前一个节点的信息,单向链表就不得不再次遍历获取信息。

针对以上问题,可以结合其他数据结构解决。

使用散列表存储节点,获取节点的复杂度将会降低为 o(1)。节点移动问题可以在节点中再增加前驱指针,记录上一个节点信息,这样链表就从单向链表变成了双向链表。

综上使用双向链表加散列表结合体,数据结构如图所示:

4e5671c384768c3460a5b54a1e75de7a.png

在双向链表中特意增加两个『哨兵』节点,不用来存储任何数据。使用哨兵节点,增加/删除节点的时候就可以不用考虑边界节点不存在情况,简化编程难度,降低代码复杂度。

lru 算法实现代码如下,为了简化 key ,val 都认为 int 类型。

public class lrucache {

entry head, tail;

int capacity;

int size;

map cache;

public lrucache(int capacity) {

this.capacity = capacity;

// 初始化链表

initlinkedlist();

size = 0;

cache = new hashmap<>(capacity + 2);

}

/**

* 如果节点不存在,返回 -1.如果存在,将节点移动到头结点,并返回节点的数据。

*

* @param key

* @return

*/

public int get(int key) {

entry node = cache.get(key);

if (node == null) {

return -1;

}

// 存在移动节点

movetohead(node);

return node.value;

}

/**

* 将节点加入到头结点,如果容量已满,将会删除尾结点

*

* @param key

* @param value

*/

public void put(int key, int value) {

entry node = cache.get(key);

if (node != null) {

node.value = value;

movetohead(node);

return;

}

// 不存在。先加进去,再移除尾结点

// 此时容量已满 删除尾结点

if (size == capacity) {

entry lastnode = tail.pre;

deletenode(lastnode);

cache.remove(lastnode.key);

size--;

}

// 加入头结点

entry newnode = new entry();

newnode.key = key;

newnode.value = value;

addnode(newnode);

cache.put(key, newnode);

size++;

}

private void movetohead(entry node) {

// 首先删除原来节点的关系

deletenode(node);

addnode(node);

}

private void addnode(entry node) {

head.next.pre = node;

node.next = head.next;

node.pre = head;

head.next = node;

}

private void deletenode(entry node) {

node.pre.next = node.next;

node.next.pre = node.pre;

}

public static class entry {

public entry pre;

public entry next;

public int key;

public int value;

public entry(int key, int value) {

this.key = key;

this.value = value;

}

public entry() {

}

}

private void initlinkedlist() {

head = new entry();

tail = new entry();

head.next = tail;

tail.pre = head;

}

public static void main(string[] args) {

lrucache cache = new lrucache(2);

cache.put(1, 1);

cache.put(2, 2);

system.out.println(cache.get(1));

cache.put(3, 3);

system.out.println(cache.get(2));

}

}

lru 算法分析

缓存命中率是缓存系统的非常重要指标,如果缓存系统的缓存命中率过低,将会导致查询回流到数据库,导致数据库的压力升高。

结合以上分析 lru 算法优缺点。

lru 算法优势在于算法实现难度不大,对于对于热点数据, lru 效率会很好。

lru 算法劣势在于对于偶发的批量操作,比如说批量查询历史数据,就有可能使缓存中热门数据被这些历史数据替换,造成缓存污染,导致缓存命中率下降,减慢了正常数据查询。

lru 算法改进方案

以下方案来源与 mysql innodb lru 改进算法

将链表拆分成两部分,分为热数据区,与冷数据区,如图所示。

3705aa490ede1f7328d6e11936949d50.png

改进之后算法流程将会变成下面一样:

访问数据如果位于热数据区,与之前 lru 算法一样,移动到热数据区的头结点。

插入数据时,若缓存已满,淘汰尾结点的数据。然后将数据插入冷数据区的头结点。

处于冷数据区的数据每次被访问需要做如下判断:

若该数据已在缓存中超过指定时间,比如说 1 s,则移动到热数据区的头结点。

若该数据存在在时间小于指定的时间,则位置保持不变。

对于偶发的批量查询,数据仅仅只会落入冷数据区,然后很快就会被淘汰出去。热门数据区的数据将不会受到影响,这样就解决了 lru 算法缓存命中率下降的问题。

其他改进方法还有 lru-k,2q,lirs 算法,感兴趣同学可以自行查阅。

欢迎关注我的公众号:程序通事,获得日常干货推送。如果您对我的专题内容感兴趣,也可以关注我的博客:

希望与广大网友互动??

点此进行留言吧!

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符  | 博主筛选后可见
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值