最近准备面试的时候,发现很多面试官都喜欢问:如何实现 LRU/LFU 缓存机制?于是,对这部分内容简单总结、记录。
什么是 LRU 缓存机制?
LRU 算法,即 least recently used 最近最少使用,是一种用于缓存的数据淘汰策略。当内存不足以容纳新的数据时,需要淘汰最近最少使用的数据。
比如 Redis 的 8 种内存淘汰机制中的 volatile-lru、allkeys-lru 就是基于 LRU 算法的实现
具体的流程如下:
若 LRU 的最大容量为2,访问情况如下:
set(1,1) set(2,2) get(1) set(3,3) set(4,4) get(3)
图示:
可以对比下文 LFU 缓存中的数据结果【LFU的原理和实现】
实现思路
由于缓存的数据都是键值对的形式,我们可以用哈希表存储键值间的关系。但是,简单的哈希表无法保存数据之间的“新旧程度”,所以,我们需要一个数据结构维护键值对数据之间的先后关系,并且在每次 get、set 之后都要调整数据的先后关系。
所以,我们可以使用链表保存数据之前的先后次序,当发生 get、set 之后都将当前数据移动到链表的头节点位置。若内存不足时,只需要每次淘汰的链表的末尾节点就可以保证留在缓存中的数据都是热点数据。
综上分析,我们使用哈希表保存键值之间的关系,使用双向链表(更便于结点移动)保存数据之间的先后顺序。
public class LRU {
// 最大的容量
private int size;
private Map<Integer, Node> map;
// 头尾节点指针
private Node head, tail;
// 使用内部类Node保存键值信息,便于访问
private static class Node{
int key;
int value;
Node next;
Node prv;
public Node(int key,int value){
this.key = key;
this.value = value;
}
}
public LRU(int capacity) {
map = new HashMap();
this.size = capacity;
head = new Node(0,0);
tail = new Node(0,0);
head.next = tail;
tail.prv = head;
}
public int get(int key) {
if(map.containsKey(key)){
// 先从原位置删除,在将该节点移动首部
Node node = map.get(key);
removeNode(node);
insertToHead(node);
return node.value;
}
return -1;
}
public void set(int key, int value) {
if (map.containsKey(key)) {
// 更新值并移动到首部
Node node = map.get(key);
node.value = value;
removeNode(node);
insertToHead(node);
}else {
// 新插入
Node node = new Node(key, value);
insertToHead(node);
map.put(key, node);
}
// 超过容量时删除最后的节点
if (map.size() > size) {
map.remove(tail.prv.key);
removeNode(tail.prv);
}
}
// 将该节点从链表原位置中删除
private void removeNode(Node node){
node.prv.next = node.next;
node.next.prv = node.prv;
}
// 将节点插入到首位置
private void insertToHead(Node node){
node.next = head.next;
node.prv = head;
head.next.prv = node;
head.next = node;
}
}