LRU(Least recently used,最近最少使用)是缓存清理的策略,其思想是如果数据最近被访问过,未来被访问的几率就会很大。LRU算法对数据的增、删、查操作非常频繁,为了保证最少的时间复杂度,所以采用哈希+双链表的结构来实现。
首先定义一个双链表节点Node。节点里的元素包括key,值,前驱,后继。
class Node {
int key;
int val;
Node prev;
Node next;
} 接着定义算法中的变量,并初始化。变量包括缓存容量、头节点、尾节点、存储key的hash表。
private int capacity;
private Node first;
private Node last;
private Map<Integer, Node> map;
public LRUCache(int capacity) {
this.capacity=capacity;
map=new HashMap<>();
first=new Node();
last=new Node();
first.next=last;
last.prev=first;
}定义两个方法。一个将节点插入到头结点的后继。另一个是删除掉尾节点的前驱。由于是双向链表,所以插入删除的时候要同时对他们的前驱节点和后继节点进行修改。
当往头结点后插入的时候,需要判断该节点的key是否已经存在于链表中,若存在要先从原来的位置删除掉节点,然后再插入。
当删除尾节点时,要同步删除掉map中的key,所以才要在节点中定义key。,可以迅速从map中删掉它。
//将节点插入到头结点后
public void addToHead(Node node){
if(node.next!=null){
node.next.prev=node.prev;
}
if(node.prev!=null){
node.prev.next=node.next;
}
node.next=first.next;
node.prev=first;
first.next=node;
node.next.prev=node;
}
//将尾节点的前一个节点删除
public void removeFromTail(){
int key=last.prev.key;
last.prev.prev.next=last;
last.prev=last.prev.prev;
map.remove(key);
}当使用get方法获取数据时,先把节点插入到头结点后,然后返回节点的value值。
public int get(int key) {
if(map.containsKey(key)){
Node node=map.get(key);
addToHead(node);
return map.get(key).val;
}else{
return -1;
}
}当使用put方法放置数据时,先判断key是否存在,若存在则直接修改对应的value,并把节点移动到头节点。若不存在则要判断缓存容量是否已经上限,若上限则需要先删除掉尾节点,然后再插入。
public void put(int key, int value) {
Node node=new Node();
if(map.containsKey(key)){
node=map.get(key);
}else{
if(map.size()==capacity){
removeFromTail();
}
}
map.put(key,node);
node.key=key;
node.val=value;
addToHead(node);
}Redis中的近似LRU算法
上述算法虽然时间复杂度很低,但双链表结构会占用更多的存储空间。这对于数据量很大的场景下是很不合适的。所以Redis采用了一种近似LRU算法。即从全部数据中随机抽取一定数量的数据,并按访问时间进行排序,淘汰掉最不常用的。抽取的样本数量越多,Redis的近似LRU算法就会越接近真实的LRU算法,同时损耗也会变大,默认的样本数量是5。