手写LRU热点缓存数据结构
引言
LRU是开发过程中设计缓存的常用算法,在此基础上,如何设计一个高效的缓存呢?本文就带大家分析并手撸一个LRUCache。
LRU算法
LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。也就是说,当限定的空间已存满数据时,应当把最久没有被访问到的数据淘汰。
如何储存
做为缓存,它应该有查询速度快,同时尽可能的修改也快,怎么储存才能查询速度能够保证的情况下,尽可能地提高缓存的修改速度?
使用数组
如果使用数组,可以肯定的是,它的查询速度肯定快,因为它的查询是通过索引下标来进行的,天然速度很快。但是数组大小一旦固定下来,它是不可变的,即使是我们的ArrayList,它要在扩容的时候,效率依然较低。同时如果对数组进行了删除操作,所以的位于被删除结点后面的结点都应该往前移动,它的花销不容小觑。
使用链表
对于链表而言,它的查询速度很慢,因为链表中的查询是通过for遍历来查找的,在最坏的情况下,时间复杂度为O(n),其中n是指当前链表的长度。虽然java中的LinkedList是通过双向队列来实现的,它的效果也依然较慢
高效LRUCache储存方案
通过对以上问题的思考,要想提高LRUCache的查询修改效率,就必须合理设计其中的数据结构。

本文中,通过HashMap 和 链表组合使用的方式,来提高LRUCache的查询、修改效率。
HashMap中记录着每一个K值,和与之对应的Node结点,如果只是查询,通过map.get(key)操作,能够快速的将结果查询出来,根据LRU理论,如果数据被访问过,那么它将来被再次访问的几率也更高,所以需要将被访问的数据移至尾部(存放最热的数据)
当LRUCache的容量被使用完后,对于冷数据(相对于热点数据)而言,再次插入的时候,应该将冷数据移出,并把刚刚插入的数据加在尾部(热点数据存在于末尾)。也就是所谓的FIFO(先进先出)。
代码实现
Node结点
采用双向队列(前驱结点、后驱结点、当前Node的ket,当前Node的value)
class Node {
//前驱结点
private Node before;
//当前结点的key值
private String key;
//当前结点的value值
private T value;
//后驱结点
private Node after;
}
LRUCache类
//头结点
private String firstKey;
//尾结点
private String lastKey;
//最大容量
private int capacity;
/**
* map
*/
private Map<String, Node> map;
/**
* 构造一个指定容量的LRUCache
*
* @param capacity
*/
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(this.capacity);
}
/**
* 构造一个无容量限制的LRUCache
*/
public LRUCache() {
this.capacity = Integer.MAX_VALUE;
map = new HashMap<>();
}
添加缓存
新添加的缓存作为热点数据放在链尾,当缓存容量不够时,移除头部的非热点数据
/**
* 添加或更新缓存
*
* @param key key值
* @param value 缓存对象
*/
public void put(String key, T value) {
Node node = map.get(key);
if (node == null) {
if (map.size() >= capacity) {
this.removeMode();
}
node = addNewNode(key);
node.key = key;
node.value = value;
map.put(key, node);
} else {
changeNodeToLast(node);
node.key = key;
node.value = value;
}
}
/**
* 添加新结点到末尾
*
* @param key key值
* @return 添加好的结点
*/
private Node addNewNode(String key) {
Node newNode = new Node();
if (firstKey == null) {
//第一次添加,直接添加到开始位置
firstKey = key;
} else if (lastKey == null) {
//第二次添加,添加到末尾,和head结点互相连接
lastKey = key;
Node firstNode = map.get(firstKey);
newNode.before = firstNode;
firstNode.after = newNode;
} else {
//当头和尾都有Node的时候,添加到末尾
Node lastNode = map.get(lastKey);
newNode.before = lastNode;
lastNode.after = newNode;
lastKey = key;
}
return newNode;
}
/**
* 移除头结点
*/
private void removeMode() {
Node firstNode = map.get(firstKey);
map.remove(firstKey);
if (firstNode.after != null) {
firstKey = firstNode.after.key;
firstNode.after.before = null;
} else {
//只有一个结点
firstKey = null;
lastKey = null;
}
}
查询缓存
对于存在的缓存,经过了一次查询后,应该将其作为热点数据放到链尾
/**
* 查询缓存
*
* @param key
* @return
*/
public T get(String key) {
Node node = map.get(key);
if (node != null) {
changeNodeToLast(node);
return node.value;
}
return null;
}
/**
* 将热点缓存移到尾部
*
* @param node
*/
private void changeNodeToLast(Node node) {
//如果还没有结点,则node就是firstNode
if (firstKey == null) {
firstKey = node.key;
}
//判断是否已经是尾结点
if (lastKey.equals(node.key)) {
return;
}
//判断是否是头结点
if (firstKey.equals(node.key)) {
//如果是头结点,而且没有下个结点,则只有一个结点,直接返回
if (node.after == null) {
return;
}
//如果是是头结点,且存在多个结点
firstKey = node.after.key;
}
Node a = node.after;
Node b = node.before;
if (b != null) {
b.after = a;
}
if (a != null) {
a.before = b;
}
Node lastNode = map.get(lastKey);
lastKey = node.key;
node.before = lastNode;
lastNode.after = node;
lastNode = node;
lastNode.after = null;
}
删除缓存
删除的时候,如果删除的正是头结点或尾结点,则需要更改firstKey或lastKey
/**
* 删除缓存
*
* @param key 要删除的缓存key值
* @return
*/
public boolean delete(String key) {
Node removeNode = map.remove(key);
if (removeNode == null) {
return false;
} else {
Node a = removeNode.after;
Node b = removeNode.before;
//如果是头结点需要移动firstKey指针
if (key.equals(firstKey)) {
firstKey = a.key;
} else if (key.equals(lastKey)) {
//如果是尾结点需要移动lastkey指针
lastKey = b.key;
}
if (a != null) {
a.before = b;
}
if (b != null) {
b.after = a;
}
return true;
}
}
完整代码
public class LRUCache<T> {
class Node {
//前驱结点
private Node before;
//当前结点的key值
private String key;
//当前结点的value值
private T value;
//后驱结点
private Node after;
}
//头结点
private String firstKey;
//尾结点
private String lastKey;
//最大容量
private int capacity;
/**
* map
*/
private Map<String, Node> map;
/**
* 构造一个指定容量的LRUCache
*
* @param capacity
*/
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>(this.capacity);
}
/**
* 构造一个无容量限制的LRUCache
*/
public LRUCache() {
this.capacity = Integer.MAX_VALUE;
map = new HashMap<>();
}
/**
* 添加新结点到末尾
*
* @param key key值
* @return 添加好的结点
*/
private Node addNewNode(String key) {
Node newNode = new Node();
if (firstKey == null) {
//第一次添加,直接添加到开始位置
firstKey = key;
} else if (lastKey == null) {
//第二次添加,添加到末尾,和head结点互相连接
lastKey = key;
Node firstNode = map.get(firstKey);
newNode.before = firstNode;
firstNode.after = newNode;
} else {
//当头和尾都有Node的时候,添加到末尾
Node lastNode = map.get(lastKey);
newNode.before = lastNode;
lastNode.after = newNode;
lastKey = key;
}
return newNode;
}
/**
* 移除头结点
*/
private void removeMode() {
Node firstNode = map.get(firstKey);
map.remove(firstKey);
if (firstNode.after != null) {
firstKey = firstNode.after.key;
firstNode.after.before = null;
} else {
//只有一个结点
firstKey = null;
lastKey = null;
}
}
/**
* 将热点缓存移到尾部
*
* @param node
*/
private void changeNodeToLast(Node node) {
//如果还没有结点,则node就是firstNode
if (firstKey == null) {
firstKey = node.key;
}
//判断是否已经是尾结点
if (lastKey.equals(node.key)) {
return;
}
//判断是否是头结点
if (firstKey.equals(node.key)) {
//如果是头结点,而且没有下个结点,则只有一个结点,直接返回
if (node.after == null) {
return;
}
//如果是是头结点,且存在多个结点
firstKey = node.after.key;
}
Node a = node.after;
Node b = node.before;
if (b != null) {
b.after = a;
}
if (a != null) {
a.before = b;
}
Node lastNode = map.get(lastKey);
lastKey = node.key;
node.before = lastNode;
lastNode.after = node;
lastNode = node;
lastNode.after = null;
}
/**
* 查询缓存
*
* @param key
* @return
*/
public T get(String key) {
Node node = map.get(key);
if (node != null) {
changeNodeToLast(node);
return node.value;
}
return null;
}
/**
* 添加缓存
*
* @param key key值
* @param value 缓存对象
*/
public void put(String key, T value) {
Node node = map.get(key);
if (node == null) {
if (map.size() >= capacity) {
this.removeMode();
}
node = addNewNode(key);
node.key = key;
node.value = value;
map.put(key, node);
} else {
changeNodeToLast(node);
node.key = key;
node.value = value;
}
}
/**
* 删除缓存
*
* @param key 要删除的缓存key值
* @return
*/
public boolean delete(String key) {
Node removeNode = map.remove(key);
if (removeNode == null) {
return false;
} else {
Node a = removeNode.after;
Node b = removeNode.before;
//如果是头结点需要移动firstKey指针
if (key.equals(firstKey)) {
firstKey = a.key;
} else if (key.equals(lastKey)) {
//如果是尾结点需要移动lastkey指针
lastKey = b.key;
}
if (a != null) {
a.before = b;
}
if (b != null) {
b.after = a;
}
return true;
}
}
/**
* 输出
*/
public void print() {
Node node = map.get(firstKey);
while (node != null) {
System.out.println(node.value);
node = node.after;
}
}
}
以上代码并未考虑线程安全问题
手写LRU热点缓存数据结构的更多相关文章
- java 手写 jvm高性能缓存
java 手写 jvm高性能缓存,键值对存储,队列存储,存储超时设置 缓存接口 package com.ws.commons.cache; import java.util.function.Func ...
- HashMap+双向链表手写LRU缓存算法/页面置换算法
import java.util.Hashtable; class DLinkedList { String key; //键 int value; //值 DLinkedList pre; //双向 ...
- python实现LRU热点缓存
基于列表+Hash的LRU算法实现. 访问某个热点时,先将其从原来的位置删除,再将其插入列表的表头 为使读取及删除操作的时间复杂度为O(1),使用hash存储热点的信息的键值 class LRUCac ...
- Javascript 手写 LRU 算法
LRU 是 Least Recently Used 的缩写,即最近最少使用.作为一种经典的缓存策略,它的基本思想是长期不被使用的数据,在未来被用到的几率也不大,所以当新的数据进来时我们可以优先把这些数 ...
- 第三节:工厂+反射+配置文件(手写IOC)对缓存进行管理。
一. 章前小节 在前面的两个章节,我们运用依赖倒置原则,分别对 System.Web.Caching.Cache和 System.Runtime.Cacheing两类缓存进行了封装,并形成了ICach ...
- 手写LRU算法
import java.util.LinkedHashMap; import java.util.Map; public class LRUCache<K, V> extends Link ...
- 手写LRU实现
完整基于 Java 的代码参考如下 class DLinkedNode { String key; int value; DLinkedNode pre; DLinkedNode post; } LR ...
- 面试题目:手写一个LRU算法实现
一.常见的内存淘汰算法 FIFO 先进先出 在这种淘汰算法中,先进⼊缓存的会先被淘汰 命中率很低 LRU Least recently used,最近最少使⽤get 根据数据的历史访问记录来进⾏淘汰 ...
- java手写多级缓存
多级缓存实现类,时间有限,该类未抽取接口,目前只支持两级缓存:JVM缓存(实现 请查看上一篇:java 手写JVM高性能缓存).redis缓存(在spring 的 redisTemplate 基础实现 ...
- 《吊打面试官》系列-Redis哨兵、持久化、主从、手撕LRU
你知道的越多,你不知道的越多 点赞再看,养成习惯 前言 Redis在互联网技术存储方面使用如此广泛,几乎所有的后端技术面试官都要在Redis的使用和原理方面对小伙伴们进行360°的刁难.作为一个在互联 ...
随机推荐
- 让微服务开源更普惠,阿里云微服务引擎MSE全球开服
简介:MSE 于2020年10月在国内开启商业化服务,目前已吸引近万客户使用,用于在云上更低成本构建.更稳定运行微服务架构.此次,MSE 向阿里云国际站开放服务,旨在帮助更多客户享受到更加普惠的微服 ...
- KubeVela v1.3 多集群初体验,轻松管理应用分发和差异化配置
简介:KubeVela v1.3 在之前的多集群功能上进行了迭代,本文将为你揭示,如何使用 KubeVela 进行多集群应用的部署与管理,实现以上的业务需求. 作者:段威(段少) 在当今的多集群业务 ...
- 饿了么EMonitor演进史
简介: 可观测性作为技术体系的核心环节之一,跟随饿了么技术的飞速发展,不断自我革新. 序言 时间回到2008年,还在上海交通大学上学的张旭豪.康嘉等人在上海创办了饿了么,从校园外卖场景出发,饿了么一步 ...
- Apache Hudi 在 B 站构建实时数据湖的实践
简介: B 站选择 Flink + Hudi 的数据湖技术方案,以及针对其做出的优化. 本文作者喻兆靖,介绍了为什么 B 站选择 Flink + Hudi 的数据湖技术方案,以及针对其做出的优化.主 ...
- python语言中的装饰器详解
装饰器是一个用于封装函数或类的代码的工具.它显式地将封装器应用到函数或类上,从而使它们选择加入到装饰器的功能中.对于在函数运行前处理常见前置条件(例如确认授权),或在函数运行后确保清理(例如输 ...
- Region-区域
定义Region的方式有两种: 一种是在XAML定义 RegionManager.RegionName(XAML) 一.View代码 1 <Viewbox Grid.Column="1 ...
- VisualStudio 在 DebuggerDisplay 的属性更改业务逻辑将会让调试和非调试下逻辑不同
本文记录我写的逗比代码,我在 DebuggerDisplay 对应的属性的 get 方法上,在这个方法里面修改了业务逻辑,如修改界面元素,此时我在 VisualStudio 断点调试下和非断点调试下的 ...
- Solution - AGC060B
Link 简要题意:在 \(n \times m\) 的方格表中填入一些不超过 \(2^k-1\) 的数.考虑所有从左上角到右下角的最短路径,要求其中满足路径上数异或和为 \(0\) 的路径只有给定的 ...
- 【爬虫实战】用python爬今日头条热榜TOP50榜单!
目录 一.爬取目标 二.爬取结果 三.代码讲解 四.技术总结 五.演示视频 六.附完整源码 一.爬取目标 您好!我是@马哥python说,一名10年程序猿. 今天分享一期爬虫案例,爬取的目标是:今日头 ...
- 【爬虫+情感判定+Top10高频词+词云图】"乌克兰"油管热评python舆情分析
目录 一.分析背景 二.整体思路 三.代码讲解 3.1 爬虫采集 3.2 情感判定 3.3 Top10高频词 3.4 词云图 四.得出结论 五.同步视频演示 六.附完整源码 一.分析背景 乌克兰局势这 ...