小黑有个朋友最近去面试,过程中有问他一些缓存相关的问题。
让他回答一下,设计一个LRU缓存,应该怎么实现
我这个朋友呢,应该是没好好准备这块儿内容,反正是没咋答上来,于是。。。就让他回家等通知了。
今天小黑就带大家来聊一聊LRU算法,并动手写一个LRU缓存。
缓存淘汰策略是啥
在我们平时开发中,经常会使用到缓存,比如一些热点商品,配置数据等,为了提高访问速度都会放到缓存中,但是,往往缓存的容量是有限的,我们不能将所有数据都放在缓存中,需要给缓存设定一个容量,当容量放满之后,要有新的数据放入缓存时,需要按照一定的策略将原来缓存中的数据淘汰掉,那么这个策略就叫做缓存淘汰策略。
缓存淘汰策略有很多种选择,常见的有FIFO(first in last out),LRU(Least Recently Used),LFU(Least Frequently Used)等。
LRU是啥
LRU是Least Recently Used的缩写,意思是最近最少使用,也就是说将最近使用最少的数据淘汰掉。
例如,我们有如下一个缓存结构:
最开始缓存时空的,我们分别往缓存中放入了5,6,9三个元素,接着在放元素3时,缓存空间已经使用完了,这是我们需要淘汰掉一个元素,释放出空间放心的元素,按照LRU算法的逻辑,此时缓存中最近最少使用的元素是5,所以将5淘汰掉,放入元素3。
接下来我们来想想如何实现这样一个LRU缓存结构,在开始写代码之前,我们要先明确我们这个缓存需要满足的一个条件。
- 该缓存的容量要有限制
- 在缓存容量使用完时,再添加新元素时必须使用LRU算法淘汰元素
- 添加元素,查询元素操作的时间复杂度都应该是O(1)
- 对缓存的操作要支持并发
因为有上面的一些要求,我们先来思考下面这个问题。
如何保证所有操作的时间复杂度都是O(1)呢?
要找到这个问题的答案,我们还得再深入思考一下LRU缓存的特点。
首先,按照开头我们图片看,LRU缓存应该是一个队列结构,如果一个元素被重新访问,那么这个元素要重新放到队列的头部;
然后呢,我们的队列有容量限制,每当有新元素要添加进缓存时,都把它添加到队列的头部;有元素要淘汰时,都从队列尾部删除;
这样可以保证添加和淘汰元素的时间复杂度都是O(1),那么我们如果想从缓存中查询元素时,该怎么办呢?
你是不是想到了将队列遍历一遍,找到符合的元素?很显然这样是能找到元素,但是时间复杂度是O(n)的,并且当我们缓存中数据量如果很多时,查询元素的时间是不固定的,可能会很快,可能会特别慢。
如果单纯使用队列的话,是做不到查询操作的时间复杂度为O(1)的。
怎样可以让查询的时间复杂度变成O(1)呢?
队列不可以,但是HashMap可以呀。
但是问题又来了,如果仅使用HashMap,虽然可以让查询时间复杂度变为O(1),但是淘汰元素时,就没办法用O(1)的时间复杂度删除了。
我们可以使用HashMap+链表的组合方式,来完成LRU缓存的结构。
以上结构,我们可以把要缓存数据的key做为HashMap的key,这样保证查询元素时能快速查到数据;
通过双向链表,可以保证在添加新元素和淘汰元素时从头节点和尾节点操作,可能在O(1)时间内完成。
现在是不是思路变得非常清晰了!
好的,我们现在来写一下实现思路:
首先,如果HashMap中包含key,则缓存命中,获取元素;如果不包含,则表示缓存未命中。
如果缓存命中:
- 从链表中移除该元素,将元素添加到链表头部;
- 将头部节点作为value保存到HashMap中;
如果未命中:
- 添加该元素到链表头部;
- 将链表头部节点保存在HashMap中。
- 嗯,到这里我们就可以写代码了。
代码实现
首先我们定义一个Cache接口,在该结构中定义Cache有哪些方法:
/**
* @author 小黑说Java
* @ClassName Cache
* @Description
* @date 2022/1/13
**/
public interface Cache<K, V> {
boolean put(K key, V value);
Optional<V> get(K key);
int size();
boolean isEmpty();
void clear();
}
接下来,我们来实现我们的LRUCache类,该类实现Cache接口:
public class LRUCache<K, V> implements Cache<K, V> {
private int size;
private Map<K, LinkedListNode<CacheElement<K, V>>> linkedListNodeMap;
private DoublyLinkedList<CacheElement<K, V>> doublyLinkedList;
public LRUCache(int size) {
this.size = size;
this.linkedListNodeMap = new ConcurrentHashMap<>(size);
this.doublyLinkedList = new DoublyLinkedList<>();
}
// ...其他方法
}
首先我们在LRUCache中定义了一个Map和我们自定义的双向链表DoublyLinkedList,在构造方法中进行初始化。
接下来实现具体操作的方法。