Redis内存淘汰策略中会使用到 LRU 算法,以下是简单说明及Java代码实现。
一、前言
LRU(The Least Recently Used)即最近最少使用算法。其原理是如果一个数据最近没有被使用,那么将来它被使用的可能也更低,因此在内存达到一定阈值时,将最近最少使用的数据淘汰的一种策略算法。
二、图解
TODO 图解说明后期有空再补充
三、LRU 算法 Java 实现
Java实现LRU算法可通过哈希表+双向链表的方式实现,在哈希表中维护所有节点,实现查找的时间复杂度为O(1)(实际元素出现哈希碰撞后,形成的链表或者红黑树时间复杂度为O(logN)此处不考虑,只当哈希表为O(1)))。
3.1 定义节点类
节点Node类中应包含 key(键)、value(值)和prev(前置节点)、next(后置节点)指针;
import lombok.Getter;
import lombok.Setter;
/**
* 节点Node
*/
@Getter
@Setter
public class Node {
/**
* 节点键key
*/
private int key;
/**
* 节点值value
*/
private int value;
/**
* 前置节点
*/
private Node prev;
/**
* 后置节点
*/
private Node next;
public Node() {
}
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
注意:代码中 @Getter、@Setter 使用的是 lombok 插件,如果没有安装直接删除这两个注解和对应 import 导入逻辑,再手工给所有属性添加 Getter/Setter 方法即可,后续代码也是不做另外说明;
3.2 双向链表实现
双向链表中定义 dummyHead(虚拟头节点)、dummyTail(虚拟尾节点)两个虚拟头尾节点,避免后续处理中针对头尾节点的特殊处理;再用 length 属性维护链表长度
即
真实头节点:dummyHead.next
真实尾节点:dummyTail.prev
双向链表中定义 add、delete、deleteHead 方法,实现对链表的新增、删除及删除头节点功能(因为我们新增插入是在链表尾部,所以如需淘汰从链表头开始);
import lombok.Getter;
import lombok.Setter;
/**
* 双向链表
* LRU实现使用双向链表存储,头尾节点(head、tail)使用两个默认的虚拟节点,这样避免使用时针对头尾节点的特殊处理
*/
@Getter
@Setter
public class DoubleLinkedList {
/**
* 虚拟头节点,实际头节点应为dummyHead.next
*/
private Node dummyHead;
/**
* 虚拟尾节点,实际尾节点应为dummyTail.prev
*/
private Node dummyTail;
/**
* 链表长度
*/
private int length;
public DoubleLinkedList() {
//默认虚拟头、尾节点初始化
this.dummyHead = new Node();
this.dummyTail = new Node();
this.dummyHead.setNext(this.dummyTail);
this.dummyTail.setPrev(this.dummyHead);
this.length = 0;
}
/**
* 添加节点
* 每次添加节点在链表末尾
* @param node
*/
public void add(Node node) {
node.setNext(this.dummyTail);
node.setPrev(this.dummyTail.getPrev());
this.dummyTail.getPrev().setNext(node);
this.dummyTail.setPrev(node);
this.length++;
}
/**
* 删除节点
* @param node
*/
public void delete(Node node) {
node.getPrev().setNext(node.getNext());
node.getNext().setPrev(node.getPrev());
node.setPrev(null);
node.setNext(null);
this.length--;
}
/**
* 删除头节点
*/
public void deleteHead() {
Node head = this.dummyHead.getNext();
if (head == null) {
return;
}
this.delete(this.dummyHead.getNext());
}
}
3.3 LRUCache 类实现
即使用我们上方定义的双向链表(DoubleLinkedList )+ 哈希表(HashMap)实现LRU。LRUCache 中定义哈希表 nodeMap(节点Map)、双向链表 linkedList、capacity(容量大小)属性,且应包含 get、put 方法,具体实现见下面代码
import java.util.HashMap;
import java.util.Map;
/**
* LRU Java实现,指定容量,达到时淘汰最久未使用数据
* 通过双向链表+哈希表的方式实现查找、插入、删除的复杂度为O(1)
*/
public class LRUCache {
/**
* 节点Map
*/
private Map<Integer, Node> nodeMap;
/**
* 双向链表
*/
private DoubleLinkedList linkedList;
/**
* 容量大小,超过时应淘汰最久未使用数据
*/
private int capacity;
/**
* 元素不存在默认为-1
*/
private static final int NOT_FOUND = -1;
public LRUCache(int capacity) {
this.capacity = capacity;
nodeMap = new HashMap<>();
linkedList = new DoubleLinkedList();
}
/**
* get获取单个节点值,未找到则返回-1
* @param key
* @return
*/
public int get(int key) {
if (this.capacity == 0) {
//容量capacity设置为0直接返回-1
return NOT_FOUND;
}
if (!nodeMap.containsKey(key)) {
//根据key在哈希表中未找到即不存在返回-1
return NOT_FOUND;
}
//节点存在
Node node = nodeMap.get(key);
//get获取某个节点,即最新使用应将节点移动到双向链表尾部,可认为是删除、插入链表的一个组合操作
this.linkedList.delete(node);
this.linkedList.add(node);
return node.getValue();
}
/**
* put新增或更新节点
* 注意:新增节点时如达到设定容量上限,则应淘汰最久未使用数据
* @param key
* @param value
*/
public void put(int key, int value) {
if (this.capacity == 0) {
//容量capacity设置为0即认为无法进行put操作,直接返回即可
return ;
}
if (nodeMap.containsKey(key)) {
//键值对存在,即应更新维护键值对新值,且将该节点移动到链表尾部
Node node = nodeMap.get(key);
this.linkedList.delete(node);
} else if (this.capacity == this.linkedList.getLength()) {
//容量达到上限,删除链表中头节点,且从哈希表中也要移除映射
Node head = this.linkedList.getDummyHead().getNext();
nodeMap.remove(head.getKey());
this.linkedList.deleteHead();
}
//重新维护哈希表映射,新增节点(默认移动到尾部)
Node node = new Node(key, value);
nodeMap.put(key, node);
this.linkedList.add(node);
}
@Override
public String toString() {
if (this.linkedList.getLength() == 0) {
return "LRUCache{}";
}
//从头节点开始遍历按顺序打印
Node node = this.linkedList.getDummyHead().getNext();
StringBuffer sb = new StringBuffer("LRUCache{");
while (node != this.linkedList.getDummyTail()) {
if (node.getNext() == this.linkedList.getDummyTail()) {
//最后一个元素
sb.append(node.getKey() + ": " + node.getValue());
} else {
sb.append(node.getKey() + ": " + node.getValue() + ", ");
}
node = node.getNext();
}
sb.append("}");
return sb.toString();
}
}
3.4 测试验证
测试代码如下:
public static void main(String[] args) {
LRUCache LRUCache = new LRUCache(2);
LRUCache.put(1, 11);
LRUCache.put(2, 22);
System.out.println(LRUCache);
System.out.println("获取节点1,值为" + LRUCache.get(1));
System.out.println(LRUCache);
System.out.println("更新节点2,新值为250");
LRUCache.put(2, 250);
System.out.println(LRUCache);
System.out.println("达到最大容量,插入新值");
LRUCache.put(6, 66);
System.out.println(LRUCache);
}
打印输出如下:
LRUCache{1: 11, 2: 22}
获取节点1,值为11
LRUCache{2: 22, 1: 11}
更新节点2,新值为250
LRUCache{1: 11, 2: 250}
达到最大容量,插入新值
LRUCache{2: 250, 6: 66}
四、使用LinkedHashMap实现LRU
LinkedHashMap 内部本身就很好的支持了LRU算法,可通过继承 LinkedHashMap 实现 LRU,只要定义 capacity(容量大小),且覆盖父类 removeEldestEntry 方法,定义删除最早元素触发逻辑(this.size() > this.capacity)即可;
import lombok.Getter;
import java.util.LinkedHashMap;
import java.util.Map;
@Getter
public class LRUCache<K, V> extends LinkedHashMap {
/**
* 容量大小
*/
private int capacity;
public LRUCache(int capacity) {
super();
this.capacity = capacity;
}
@Override
protected boolean removeEldestEntry(Map.Entry eldest) {
//链表长度达到上限,触发删除链表最早未使用元素
return this.size() > this.capacity;
}
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。