文章目录

  • LRU思路及实现代码
    • 思想
    • 方案一、数组
      • 思路
      • 问题
    • 方案二、链表
      • 思路
      • 问题
    • 方案三、双向链表+哈希表
      • 思路
      • 为什么这里要用双链表呢,单链表为什么不行?
      • 哈希表里面已经保存了 key ,那么链表中为什么还要存储 key 和 value 呢,只存入 value 不就行了
      • 借助LinkedHashMap来实现
      • 手写实现(不准借助LinkedHashMap)
        • 使用虚拟head、tail节点, 不需要判空(比较好写)
        • 下面这个没有使用虚拟节点, 各种判空
  • LRU在MySQL中的应用
  • LRU在Redis中的应用
    • 数据库中有 3000w 的数据,而 Redis 中只有 100w 数据,如何保证 Redis 中存放的都是热点数据?

LRU思路及实现代码

原文:

https://mp.weixin.qq.com/s/l3Rb0bD9CxPcaG0tBlMKIg
https://blog.csdn.net/czxlylc/article/details/102763156
https://www.jianshu.com/p/3ec150f1a78b

Least Recently Used 最近最少使用

思想

如果一个数据在最近一段时间没有被访问到, 那么在讲台它被访问到的概率也会比较小, 所以当内存空间有限时, 应该把这部分数据淘汰掉。

方案一、数组

思路

假设一个定长数组, 数组中的元素都有一个标记, 这个标记可以是时间戳, 也可以是一个自增的数字。每放入一个元素, 就将这个标志自增1, 每访问一个元素, 就将这个标志置为0; 当数组空间不够大的时候, 就删除标志最大的元素。

问题

需要不停的维护整个数组, 时间复杂度是O(n)

方案二、链表

思路

最近最少使用, 应该需要一个有序的结构, 没加入一个元素, 都放在元素的末尾, 每访问一个元素, 就删除原位置的元素, 并在末尾添加, 这样后面的都是最近被使用过的, 头部是最近没有使用的, 当内存空间不足时, 淘汰头部的元素。

问题

同样, 从时间复杂度的角度看,因为链表插入、查询的时候都要遍历链表,查看数据是否存在,所以它还是O(n)。

方案三、双向链表+哈希表

思路

想要查询和插入的时间复杂度都是O(1), 需要满足以下条件:

  1. 首先必须是有序的, 以区分最近使用的和很久没有使用的数据, 当容量满了之后删除很久没有使用的数据;
  2. 要能够快速找到某个key是否存在, 并返回value;
  3. 每次访问某个key, 需要将这个元素变为最近使用的, 也就是说, 要支持在任意位置快速插入和删除元素;

查找快, 可以想到哈希表, 但是哈希表是乱序的;

有序, 可以想到链表, 链表插入、和删除都很快, 但是查询是O(n);

集合一下就是哈希链表: LinkedHashMap, 结构如下:

借助上图结构, 分析上面的三个条件:

  1. 每次在链表尾部添加元素, 那么越靠近尾部的元素就越是最近使用的, 链表头部的元素就是越久未使用的;
  2. 对于某一个key, 可以通过哈希表快速定位到链表中的节点, 取到value, 不用从头遍历;
  3. 链表支持在任意位置插入和删除, 修改指针即可, 可以借助哈希表, 快速映射到任意一个链表节点, 进行插入和删除。

为什么这里要用双链表呢,单链表为什么不行?

涉及到链表的删除操作, 因为通过哈希表定位到某一个链表中的节点, 想要删除时, 需要知道前驱节点的指针。

哈希表里面已经保存了 key ,那么链表中为什么还要存储 key 和 value 呢,只存入 value 不就行了

//TODO 感觉有点问题, 应该是淘汰头节点时这样吧, 因为如果是访问某个节点删除时, 需要哈希表来定位

如果链表中的结点,只有 value 没有 key,那么我们就无法删除哈希表的 key。

借助LinkedHashMap来实现

LinkedHashMap已经实现了LRU的功能, 仅需要重写一个方法即可, 代码如下:

// 继承LinkedHashMap
public class LruCache<K,V> extends LinkedHashMap<K,V> {// 缓存容量private int size;public LruCache(int initialCapacity, float loadFactor) {// 这里true是关键// true: 按照访问的顺序存储元素--> put、get都会将最近使用的元素存储在链表尾部// false: 按照put的顺序存储元素super(initialCapacity, loadFactor, true);this.size = initialCapacity;}@Overrideprotected boolean removeEldestEntry(Map.Entry<K, V> eldest) {// 容量超过缓存设置的容量后, 移除最老的元素return size() > size;}
}
// 原来是 1,1 2,2, 3,3,    put一个2,4,  会变成1,1 3,3 2,4

手写实现(不准借助LinkedHashMap)

时间复杂度:get、set方法都达到O(1)
空间复杂度:O(capacity)

使用虚拟head、tail节点, 不需要判空(比较好写)

class LRUCache{class DLinkedNode {int key;int value;DLinkedNode prev;DLinkedNode next;public DLinkedNode() {}public DLinkedNode(int _key, int _value) {key = _key; value = _value;}}private Map<Integer, DLinkedNode> cache = new HashMap<Integer, DLinkedNode>();private int size;private int capacity;private DLinkedNode head, tail;public LRUCache(int capacity) {this.size = 0;this.capacity = capacity;// 使用伪头部和伪尾部节点// 这个不错, 不用各种判断空了head = new DLinkedNode();tail = new DLinkedNode();head.next = tail;tail.prev = head;}public int get(int key) {DLinkedNode node = cache.get(key);if (node == null) {return -1;}// 如果 key 存在,先通过哈希表定位,再移到头部moveToHead(node);return node.value;}public void put(int key, int value) {DLinkedNode node = cache.get(key);if (node == null) {// 如果 key 不存在,创建一个新的节点DLinkedNode newNode = new DLinkedNode(key, value);// 添加进哈希表cache.put(key, newNode);// 添加至双向链表的头部addToHead(newNode);++size;if (size > capacity) {// 如果超出容量,删除双向链表的尾部节点DLinkedNode tail = removeTail();// 删除哈希表中对应的项cache.remove(tail.key);--size;}}else {// 如果 key 存在,先通过哈希表定位,再修改 value,并移到头部node.value = value;moveToHead(node);}}private void addToHead(DLinkedNode node) {node.prev = head;node.next = head.next;head.next.prev = node;head.next = node;}private void removeNode(DLinkedNode node) {node.prev.next = node.next;node.next.prev = node.prev;}private void moveToHead(DLinkedNode node) {removeNode(node);addToHead(node);}private DLinkedNode removeTail() {DLinkedNode res = tail.prev;removeNode(res);return res;}
}

下面这个没有使用虚拟节点, 各种判空

public class LruCache {private int capacity;private Node head;private Node tail;private HashMap<Integer, Node> map;public LruCache() {}public LruCache(int capacity) {this.capacity = capacity;map = new HashMap<>();}private class Node{private int key;private int value;private Node pre;private Node next;public Node() {}public Node(int key, int value, Node pre, Node next) {this.key = key;this.value = value;this.pre = pre;this.next = next;}}public Object get(int key){Node node = map.get(key);if(node != null){moveToTail(node);return node.value;}else{return -1;}}public void put(int key, int value){Node node = map.get(key);Node newNode = new Node(key, value, null, null);if(node == null){// 队列还没满, 插入尾部if(map.size() < capacity) {// 队列为空if(map.size() == 0){head = newNode;tail = newNode;map.put(key, newNode);}else{// 队列有数据tail.next = newNode;newNode.pre = tail;tail = newNode;map.put(key, newNode);}}else{// 队列已满 删除头部元素Node oldNode = removeHead();map.remove(oldNode.key);addLast(newNode);map.put(key, newNode);}}else{node.value = value;moveToTail(node);}}/**** @param newNode*/private void addLast(Node newNode) {if(tail != null){tail.next = newNode;newNode.pre = tail;}tail = newNode;}/*** 移除头部节点* @return*/private Node removeHead() {Node deleteNode = head;if(deleteNode != null){Node next = deleteNode.next;if(next != null){next.pre = null;}else{tail = null;}head = next;}return deleteNode;}/*** 将节点移动到尾部* @param node*/private void moveToTail(Node node) {if(tail != node){Node pre = node.pre;Node next = node.next;if(pre != null){pre.next = next;}if(next != null){next.pre = pre;// 删除头节点时if(next.pre == null){head = next;}}if(tail != null){tail.next = node;node.next = null;node.pre = tail;}tail = node;}}
}

LRU在MySQL中的应用

lru在MySQL中的应用就是Buffer Pool 缓冲池, 目的是减少磁盘IO。

你就知道它是一块连续的内存,默认大小 128M,可以进行修改。

这一块连续的内存,被划分为若干默认大小为 16KB 的页, 内存满了就淘汰掉很久没有被访问过的页。

因为 MySQL 里面有一个预读功能。预读的出发点是好的,但是有可能预读到并不需要被使用的页。

这些页也被放到了链表的头部,容量不够,导致尾部元素被淘汰。哦豁,降低命中率了,凉凉。

还有一个场景是全表扫描的 sql,有可能直接把整个缓冲池里面的缓冲页都换了一遍,影响其他查询语句在缓冲池的命中率。

那么怎么处理这种场景呢?

把 LRU 链表分为两截,一截里面放的是热数据,一截里面放的是冷数据。

LRU在Redis中的应用

redis的6中内存淘汰策略, 设置参数: maxmemory-policy noeviction, 内存设置参数: maxmemory

  1. noenviction: 默认策略, 写请求失败, 读请求正常, del请求正常;
  2. volatile-lru: 在设置了过期键的键空间中移除最近最少使用的key;
  3. volatile-random: 在设置过过期键的键空间中随机移除部分key;
  4. volatile-ttl: 在设置过期键的键空间中挑选将要国期的数据淘汰;
  5. allkeys-lru: 在所有键空间中移除最近最少使用的可以;
  6. allkeys-random: 在所有键空间中随机移除部分随机移除部分key;

关于redis中的lru算法不是严格的lru算法, 见文档:

https://github.com/redis/redis-doc/blob/master/topics/lru-cache.md

从 Redis 3.0 开始,改善了算法的性能,使得更接近于真实的 LRU 算法。做法就是维护了一个回收候选键池。

Redis 的 LRU 算法有一个非常重要的点就是你可以通过修改下面这个参数的配置,自己调整算法的精度。

原因: The reason why Redis does not use a true LRU implementation is because it costs more memory.

maxmemory-samples 5

数据库中有 3000w 的数据,而 Redis 中只有 100w 数据,如何保证 Redis 中存放的都是热点数据?

先指定淘汰策略为 allkeys-lru 或者 volatile-lru,然后再计算一下 100w 数据大概占用多少内存,根据算出来的内存,限定 Redis 占用的内存。

接下来的,就交给 Redis 的淘汰策略了。

LRU算法思想及手写LRU实现相关推荐

  1. 算法------手写LRU算法

    算法------手写LRU算法 LRU是Redis中常用的内存淘汰算法. 意思是:当缓存容量满的时候,淘汰最近很少使用的数据. 具体实现逻辑: 把缓存放到双向链表当中,最近使用过.或新放入的缓存,放到 ...

  2. 透彻理解Spring事务设计思想之手写实现

    2019独角兽企业重金招聘Python工程师标准>>> 前言 事务,是描述一组操作的抽象,比如对数据库的一组操作,要么全部成功,要么全部失败.事务具有4个特性:Atomicity(原 ...

  3. 【手写系列】透彻理解MyBatis设计思想之手写实现

    前言 MyBatis,曾经给我的感觉是一个很神奇的东西,我们只需要按照规范写好XXXMapper.xml以及XXXMapper.java接口.要知道我们并没有提供XXXMapper.java的实现类, ...

  4. 【手写系列】透彻理解Spring事务设计思想之手写实现

    事务,是描述一组操作的抽象,比如对数据库的一组操作,要么全部成功,要么全部失败.事务具有4个特性:Atomicity(原子性),Consistency(一致性),Isolation(隔离性),Dura ...

  5. TF之LSTM:利用多层LSTM算法对MNIST手写数字识别数据集进行多分类

    TF之LSTM:利用多层LSTM算法对MNIST手写数字识别数据集进行多分类 目录 设计思路 实现代码 设计思路 更新-- 实现代码 # -*- coding:utf-8 -*- import ten ...

  6. DL之DNN:利用DNN【784→50→100→10】算法对MNIST手写数字图片识别数据集进行预测、模型优化

    DL之DNN:利用DNN[784→50→100→10]算法对MNIST手写数字图片识别数据集进行预测.模型优化 导读 目的是建立三层神经网络,进一步理解DNN内部的运作机制 目录 输出结果 设计思路 ...

  7. DL之LiRDNNCNN:利用LiR、DNN、CNN算法对MNIST手写数字图片(csv)识别数据集实现(10)分类预测

    DL之LiR&DNN&CNN:利用LiR.DNN.CNN算法对MNIST手写数字图片(csv)识别数据集实现(10)分类预测 目录 输出结果 设计思路 核心代码 输出结果 数据集:Da ...

  8. TF:利用是Softmax回归+GD算法实现MNIST手写数字图片识别(10000张图片测试得到的准确率为92%)

    TF:利用是Softmax回归+GD算法实现MNIST手写数字图片识别(10000张图片测试得到的准确率为92%) 目录 设计思路 全部代码 设计思路 全部代码 #TF:利用是Softmax回归+GD ...

  9. DL之DNN:利用DNN算法对mnist手写数字图片识别数据集(sklearn自带,1797*64)训练、预测(95%)

    DL之DNN:利用DNN算法对mnist手写数字图片识别数据集(sklearn自带,1797*64)训练.预测(95%) 目录 数据集展示 输出结果 设计代码 数据集展示 先查看sklearn自带di ...

最新文章

  1. 求两个有序数组的中位数-算法导论
  2. 编程之美-2.6-精确表达浮点数
  3. MySQL查询表内重复记录
  4. 获取服务器配置信息的方法
  5. zepto不支持animate({scrollTop:100px})的解决办法
  6. 儿童手表怎么删除联系人_儿童节来了,送孩子400多元的超值礼物,儿童手表9X评测分享...
  7. 【Python图像特征的音乐序列生成】思路的转变
  8. csi python 摄像头 树莓派_树莓派之摄像头和人脸识别
  9. [ SAP ]MM Valuation System
  10. 用C语言编程计算下列表达式:s=1! 2...,2012年全国计算机等级二级C语言模拟试题及答案(3)...
  11. 沙尘暴为何再次肆虐?
  12. sublime如何运行HTML?
  13. 自己用C#写的控制三菱FX5U PLC
  14. ipad微信号无法连接服务器,ipad微信内置浏览器无法微信登录
  15. imac mini 双系统_iMac,Mini和Pro:Apple的台式Mac比较
  16. 谷歌浏览器 android 69,如何将谷歌浏览器69及以上版本切换回旧版UI界面
  17. python之无限浏览网页
  18. Vue源码实现之watcher拾遗
  19. 第13期 《万物并作,吾以观复》
  20. TensorRT(8):动态batch进行推理

热门文章

  1. ifndef/define/endif的使用
  2. 高德地图自定义创建地图
  3. 倦怠和枯燥_避免倦怠的13种方法
  4. 普通心理学-学习笔记
  5. 实时准确高效动态检测确保铁路运行安全——TFDS、TVDS、TEDS检测系统
  6. 鄙视糯米团购网, 声讨糯米团
  7. 【转载】jsDelivr的一些替代方案
  8. jsp允许跨域访问_js跨域访问解决方法(jsp代理)
  9. 硅磷晶罐的使用注意事项有哪些?
  10. 使用Django框架遇到RuntimeError: populate() isn't reentrant错误