一、背景介绍
以前面试的时候,面试官问了一些redis相关的知识,比如说常用几种数据类型、穿透、击穿、雪崩、持久化方式、过期策略、淘汰策略等等,回答的都还凑合,画风一转,面试官突然问我,redis淘汰策略中,最近最少使用的这种策略底层的实现方式可以说一下吗?当时大脑一片空白,心中万马奔腾,不会......
二、lru算法介绍
如果容器已满,有新元素到来时,删除最近最少使用的元素,给新元素腾出空间
三、实现方式
1、基于LinkedHashMap实现
/**
* 基于java LinkedHashMap 实现lru算法
* 继承LinkedHashMap
* @author huhy
*/
public class LruDemoJavaApi<K,V> extends LinkedHashMap<K,V> {
private int size;
public LruDemoJavaApi(int size) {
super(size, 0.75f,true);
this.size = size;
}
/**
* 重写移除标准
* 当目前容器实际容量大于初始设置的容量,触发淘汰最近最少使用元素策略
* @param eldest
* @return boolean
*/
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
return super.size()>size;
}
/**
* main方法测试
* @param args
*/
public static void main(String[] args) {
LruDemoJavaApi<Integer,Object> demoJavaApi = new LruDemoJavaApi<>(3);
demoJavaApi.put(1,"1");
demoJavaApi.put(2,2+"");
demoJavaApi.put(3,3+"");
System.out.println(demoJavaApi);
demoJavaApi.put(4,4+"");
System.out.println(demoJavaApi);
demoJavaApi.get(2);
System.out.println(demoJavaApi);
}
2、双向链表实现
/**
* node节点
* node数据结构包含key、value,前后node节点引用
* node是链表数据结构的关键组成
* @param <K>
* @param <V>
*/
class Node<K,V>{
/** 前节点引用 */
private Node prev;
/** 后节点引用 */
private Node next;
/** 键 */
private K key;
/** 值 */
private V value;
public Node( K key, V value) {
this.key = key;
this.value = value;
this.prev = this.next = null;
}
public Node() {
this.prev = this.next = null;
}
public Node getPrev() {
return prev;
}
public void setPrev(Node prev) {
this.prev = prev;
}
public Node getNext() {
return next;
}
public void setNext(Node next) {
this.next = next;
}
public K getKey() {
return key;
}
public void setKey(K key) {
this.key = key;
}
public V getValue() {
return value;
}
public void setValue(V value) {
this.value = value;
}
@Override
public String toString() {
return key+"="+value;
}
}
/**
* 双向链表数据结构
* lru算法特点,最近最频繁使用的放在靠前位置,
* 对于最近最频繁使用数据包括新增、修改、获取,所以新增元素,需要将新增节点放在队列最前
* 1、包含头节点、尾节点
* 2、初始状态,尾节点前节点指向头节点,头节点的后节点指向尾节点
*
*/
class LinkedNode{
private Node head;
private Node tail;
public LinkedNode() {
this.head = new Node();
this.tail = new Node();
this.head.setNext(tail);
this.tail.setPrev(head);
}
/**
* 新增
* 1、将头节点的后节点的前节点引用指向新增节点
* 2、将新增节点的后节点指向头节点的厚街店
* 3、将新增节点前节点指向头节点
* 4、将头节点的后节点引用指向新增节点
* @param node
*/
public void addNode(Node node){
if(Objects.nonNull(node)){
this.head.getNext().setPrev(node);
node.setNext(this.head.getNext());
node.setPrev(this.head);
this.head.setNext(node);
}else { throw new RuntimeException("node is null");}
}
/**
* 删除
* 1、被删除节点的后节点的前节点引用指向被删除节点的前节点
* 2、被删除节点的前节点的后节点引用指向被删除节点的后节点
* 3、将被删除节点的前后节点置空,方便垃圾回收
* @param node
*/
public void removeNode(Node node){
node.getNext().setPrev(node.getPrev());
node.getPrev().setNext(node.getNext());
node.setNext(null);
node.setPrev(null);
}
/**
* 获取尾节点,用于当队已满,淘汰最末节点时使用
* @return
*/
public Node getLastNode(){
return tail.getPrev();
}
/**
* 打印双向链表内所有元素
* @return
*/
@Override
public String toString() {
StringBuffer strBuff = new StringBuffer("{");
Node node = head.getNext();
strBuff.append(node.toString()+",");
while (node.getNext()!=tail){
node = node.getNext();
strBuff.append(node.toString()+",");
}
String substring = strBuff.toString().substring(0, strBuff.length()-1);
return substring+"}";
}
}
/**
* Lru算法模拟
* 1、内部包含map、双向链表,新增、修改、删除需同时维护map和双向链表
* 2、map中维护key<->node<k,v>数据结构
* @author huhy
*/
public class LruDemoCustom {
private Map<Integer,Node<Integer,Integer>> map = new HashMap<>();
private LinkedNode linkedNode = new LinkedNode();
private int size;
public LruDemoCustom(int size) {
this.size = size;
}
/**
* put操作
* 1、新增:
* 1.1、将元素插入到map
* 1.2 、将元素插入到双向链表
* 1.2.1 、链表大小未超过初始设置的上限,正常插入
* 1.2.2 、链表大小已超过初始设置的上线,则除插入
* 新元素外,需将队尾的元素进行删除操作
* 2、修改:
* 2.1、先从map中获取node元素,从双向链表中将该元素删除
* 2.2、封装新的node元素插入到双向链表
* 2.3、将新node元素设置到map中
* @param key
* @param value
*/
public void put(int key, int value){
Node node;
if(!map.containsKey(key)){
node = new Node(key,value);
map.put(key,node);
if(map.size()>this.size){
Node lastNode = linkedNode.getLastNode();
map.remove(lastNode.getKey());
linkedNode.removeNode(lastNode);
}
}else{
node = map.get(key);
node.setValue(value);
linkedNode.removeNode(node);
}
linkedNode.addNode(node);
}
/**
* 获取
* 1、从map中获取node元素
* 2、将获取的node元素从双向链表中删除
* 3、将获取的node元素新增到双向链表
* @param key
* @return
*/
public int get(int key){
if(!map.containsKey(key)){return -1;}
Node<Integer, Integer> node = map.get(key);
linkedNode.removeNode(node);
linkedNode.addNode(node);
return node.getValue();
}
@Override
public String toString() {
return linkedNode.toString();
}
}
/**
* 测试
* 观察是否符合lru算法标准
* 1、新增、修改、查找 元素<k,v>是否总能在队头
* 2、容量超过初始化所指定的大小,是否会淘汰掉最近最少使用的元素
* @param args
*/
public static void main(String[] args) {
LruDemoCustom lruDemoCustom = new LruDemoCustom(3);
lruDemoCustom.put(1,1);
lruDemoCustom.put(2,2);
lruDemoCustom.put(3,3);
System.out.println(lruDemoCustom);
lruDemoCustom.put(4,4);
System.out.println(lruDemoCustom);
lruDemoCustom.get(2);
System.out.println(lruDemoCustom);
lruDemoCustom.get(3);
System.out.println(lruDemoCustom);
}