缓存数据的淘汰之路(下)
- 2021 年 12 月 09 日
本文字数:4314 字
阅读完需:约 14 分钟
背景
数据在经过上次 LRU 大师兄的专业解读, 很清晰的明白了 LRU(最近最少淘汰算法)的底层,需要有顺序和查询插入数据去支持做出操作,而且要比较高效的进行数据的筛选,虽然大师兄 LRU 的武功很强, 数据心里也是强忍着一口气,非得让老头把看家的底层本领教教他,这不,又开始试探 LRU 底层的逻辑了;
虽然 可以用 java 集合中的 LinkedHashMap 来整理个容器,做出合格的 LRU 的实现,但是底层还是封装了很多东西。所以数据感觉要自己来搞定
开始干活。。
LRU 底层实现:
其中 HASHMAP 是保证键值对,查找,双向链表保证,键值对的队列中的位置;
1.对于自己手动写出一个 LRU 的算法,需要确定两点:
需要一个节点 NODE,确定前后指针,保证顺序
Map,作为节点数据的存储集合,做到查询和插入
2.为什么需要一个节点 Node 作为一个数据载体呢?
我们来用 hashmap 这个集合容器的底层实现做类比:它的底层存储是 put 方法;依次是
确定好可以作为数据载体,我们需要创建 node 节点数据;
创建出一个双向的队列,保证顺序;
进过删除,添加表头的操作进行调整顺序,使得每次进入的数据都可以动态的调整;真正做到顺序的有效淘汰;
我们最后做的结构就是在双端的队列中,创建出一个集合 map
3.创建一个 Node 节点存储数据
//创建存储节点类,类似于节点node的数据载体,存储键值对
class Node<k, v> {
k keys;
v values;
Node<k, v> prev; //前指针
Node<k, v> next; //后指针
public Node() {
this.prev = null;
this.next = null;
}
//初始化指针
public Node(k keys, v values) {
this.keys = keys;
this.values = values;
this.prev = this.next = null;
}
}
4.创建一个双向的队列
class DoubleLinkedList<k, v> {
Node<k, v> head;
Node<k, v> tail;
//双端队列的构造方法
public DoubleLinkedList() {
this.head = new Node<k, v>();
this.tail = new Node<k, v>();
//头节点的下一个是尾节点,尾节点的前一个是头节点
head.next = tail;
tail.prev = head;
}
添加表头
//添加到表头
public void addHeader(Node<k, v> node) {
/**
* 将node放入表头
* 当前节点node的前节点-是当前节点(一个节点进入之后,新节点的下一个节点,head的下一个的位置)
* 当前节点的前一个节点是头节点
*/
node.next = head.next;
node.prev = head;
/**
* 头节点的下一个是尾节点,尾节点的前一个是node
* 头节点的下一个是当前节点
*/
head.next.prev = node;
head.next = node;
}
删除操作
//3删除node节点
public void removeNode(Node<k, v> node) {
/**
* 删除当前节点
* 节点node的下一个节点是tail,之前节点,(删除前此节点的前一个节点)
* 节点node的前一个节点head,之后的节点tail(删除前此节点的下一个节点)
*/
node.next.prev = node.prev; //head
node.prev.next = node.next;
node.prev = null;
node.next = null;
}
//获取尾节点数据
public Node getLast() {
return tail.prev;
}
5.代码实现:
package com.lru;
import java.util.HashMap;
import java.util.Map;
/**
* @author lucas
* @create 2021-08-11 20:14
* @description 手写lru算法的实现
*/
public class LRUCache {
//创建存储节点类,类似于节点node的数据载体,存储键值对
class Node<k, v> {
k keys;
v values;
Node<k, v> prev; //前指针
Node<k, v> next; //后指针
public Node() {
this.prev = null;
this.next = null;
}
//初始化指针
public Node(k keys, v values) {
this.keys = keys;
this.values = values;
this.prev = this.next = null;
}
}
//2.创建一个双向的队列,存储一个个node节点,组成一个数据结构
class DoubleLinkedList<k, v> {
Node<k, v> head;
Node<k, v> tail;
//双端队列的构造方法
public DoubleLinkedList() {
this.head = new Node<k, v>();
this.tail = new Node<k, v>();
//头节点的下一个是尾节点,尾节点的前一个是头节点
head.next = tail;
tail.prev = head;
}
//添加到表头
public void addHeader(Node<k, v> node) {
/**
* 将node放入表头
* 当前节点node的前节点-是当前节点(一个节点进入之后,新节点的下一个节点,head的下一个的位置)
* 当前节点的前一个节点是头节点
*/
node.next = head.next;
node.prev = head;
/**
* 头节点的下一个是尾节点,尾节点的前一个是node
* 头节点的下一个是当前节点
*/
head.next.prev = node;
head.next = node;
}
//3删除node节点
public void removeNode(Node<k, v> node) {
/**
* 删除当前节点
* 节点node的下一个节点是tail,之前节点,(删除前此节点的前一个节点)
* 节点node的前一个节点head,之后的节点tail(删除前此节点的下一个节点)
*/
node.next.prev = node.prev; //head
node.prev.next = node.next;
node.prev = null;
node.next = null;
}
//获取尾节点数据
public Node getLast() {
return tail.prev;
}
}
private int cacheSize;
Map<Integer, Node<Integer, Integer>> dateMap;
DoubleLinkedList<Integer, Integer> doubleLinkedList;
//构造方法
public LRUCache(int cacheSize) {
this.cacheSize = cacheSize;
//查找
dateMap = new HashMap<>();
//排序,交换位置
doubleLinkedList = new DoubleLinkedList<>();
}
//缓存获取value值
public int get(int key) {
/**
* 1.从map中取出数据,交换位置到表头,然后返回
* 2.取不到返回-1/
*/
if (!dateMap.containsKey(key)) {
return -1;
}
Node<Integer, Integer> dataNode = dateMap.get(key);
/**
* 将队列中顺序,删除之前的位置node,添加到表头
*/
doubleLinkedList.removeNode(dataNode);
//将node添加到表头
doubleLinkedList.addHeader(dataNode);
return dataNode.values;
}
//写操作或者是更新操作key
public void put(int key, int value) {
/**
* 三种情况:
* 1.如果key存在,就更新操作
* 2.key不存在, 新增节点信息
*
*/
if (dateMap.containsKey(key)) {
//更新节点的信息数据
Node<Integer, Integer> node = dateMap.get(key);
node.values = value;
dateMap.put(key, node);
//加入队列表头
doubleLinkedList.removeNode(node);
doubleLinkedList.addHeader(node);
} else {
//是否达到最大容量了
if (dateMap.size() == cacheSize) {
Node lastNode = doubleLinkedList.getLast();
//获取到最末尾的节点数据,然后剔除map集合,剔除队列
dateMap.remove(lastNode.keys);
doubleLinkedList.removeNode(lastNode);
}
/**
* 新增操作
*/
//重新加入node
Node dateNode = new Node(key, value);
dateMap.put(key, dateNode);
//加入队列表头
doubleLinkedList.addHeader(dateNode);
}
}
}
6.代码测试:
public static void main(String[] args) {
LRUCache lruCacheDemo = new LRUCache(3);
lruCacheDemo.put(1, 1);
lruCacheDemo.put(2, 1);
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.dateMap.keySet());
lruCacheDemo.put(4, 1);
System.out.println("增加第四个后,列表是"+lruCacheDemo.dateMap.keySet());
System.out.println("--------------------------------------------");
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.dateMap.keySet());
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.dateMap.keySet());
lruCacheDemo.put(3, 1);
System.out.println(lruCacheDemo.dateMap.keySet());
System.out.println("--------------------------------------------");
lruCacheDemo.put(5, 1);
System.out.println(lruCacheDemo.dateMap.keySet());
/**
* [1, 2, 3]
* 增加第四个后,列表是[2, 3, 4]
* --------------------------------------------
* [2, 3, 4]
* [2, 3, 4]
* [2, 3, 4]
* --------------------------------------------
* [3, 4, 5]
*/
}
7.卢卡总结:
经过代码测试。可以实现淘汰 LRU 机制的效果,其中对于插入顺序,和节点 node 的删除和添加,细节中,要注意, 根据步骤完成具体实现;
LRU 数据缓存历险记就告一段落了,希望这次数据的历险记可以帮助到你获取不一样的缓存视角,记得点赞哦
版权声明: 本文为 InfoQ 作者【卢卡多多】的原创文章。
原文链接:【http://xie.infoq.cn/article/9b9030b1b460a3b4ad3968f57】。文章转载请联系作者。
卢卡多多
努力寻找生活答案的旅途者 2020.04.12 加入
公众号:卢卡多多,欢迎一起交流学习
评论