LRU页面缓存淘汰算法主要是由于内存较为可贵,所以在系统设计中要秉持着将热数据长久存在内存缓存中而冷数据则被淘汰掉的思想,它常见应用场景主要是数据库的缓存页。实质上是对数据进行读取时,查询是否能够命中缓存,如果存在则将其位置调整到缓存头部,如果不存在则存入缓存并将其放入缓存头部。当放入缓存时超过指定的缓存大小则淘汰掉缓存数据最尾部的数据。
1.实现方式和各自的优劣
- 使用数组实现,数组检索速度比较快,但是增删速度比较慢而LRU算法可能会频繁的进行在缓存中增删的操作,所以在此并不推荐。
- 使用双向链表实现,但是链表检索效率极其低下,而LRU本质上还是要查询选定内容是否存在缓存中,所以在此也不推荐。
- 通过Hash表加双向链表的方式进行数据存储,使用Hash表的key存放需要缓存的数据唯一标记,用val存放双向链表的节点信息,而双向链表的节点中保存数据以及它的前置、后置节点信息。
2.相关代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
| package com.peachl.demo.lru;
import cn.hutool.core.util.ObjectUtil; import lombok.Data;
import java.util.HashMap; import java.util.Map;
/** * @author: peach l * @date: 2022-09-20 17:21 * @description: */ // TODO 可以针对synchronized进行优化,可以提升部分效率 @Data public class CustomCache<T> {
// 游标 private static Integer index; // 缓存最大长度 private static Integer maxSize;
private Node<T> first = new Node<>(); private Node<T> last = new Node<>();
private Map<Integer, Node> cache;
private static class Node<T> { private Node<T> pre; private Node<T> next; private Integer key; private T data;
public Node() { }
public Node(Integer k, T obj) { key = k; data = obj; }
}
// 初始化CustomCache public CustomCache() { first.next = last; last.pre = first; maxSize = 3; index = 1; cache = new HashMap<>(); }
// 存 public synchronized Map put(Integer id, T obj) { Node oldNode = cache.get(id); if(ObjectUtil.isEmpty(oldNode)) { // 判断是否超长,超长则删除最后的元素 if(index > maxSize) { removeLast(); }
Node<T> newNode = new Node<>(id, obj); // 如果是首次插入需要初始化first和last if(index.equals(1)) { first = newNode; last = newNode; } else { moveToHead(newNode); } index++; cache.put(id, newNode); } else { ifExisted(oldNode); }
return cache; }
// 拿 public synchronized Object get(Integer id) { // 获取缓存 Node oldNode = cache.get(id); if(ObjectUtil.isNotEmpty(oldNode)) {
// 当不是第一个时才会进行解除绑定和移到最前面的效果 ifExisted(oldNode);
return oldNode.data; } return null; }
// 如果命中缓存则进行解除链表绑定和移动两步操作 private void ifExisted(Node oldNode) { // 当不是第一个时才会进行解除绑定和移到最前面的效果 if (!oldNode.equals(first)) { // 解除原有绑定 unbind(oldNode); // 移动到最前方 moveToHead(oldNode); } }
// 解除当前节点绑定的所有其他节点 private void unbind(Node node) { // 解除原有绑定 if(!node.equals(first)) { node.pre.next = node.next; } if(ObjectUtil.isEmpty(node.next)) { last = node.pre; } else { node.next.pre = node.pre; } }
// 移动指定节点到缓存头部 private void moveToHead(Node node) { first.pre = node; node.next = first; first = node; }
// 移除缓存尾部最后一位数据 private void removeLast() { cache.remove(last.key); last.pre.next = null; last = last.pre; }
// 迭代 public void iterator() { if(ObjectUtil.isNotEmpty(first)) { iterator(first); } else { System.out.println("collection is empty"); } }
// 迭代递归方法体 private Node iterator(Node node) { if(ObjectUtil.isNotEmpty(node.next)) { return iterator(node.next); } return null; }
}
|
3.LRU的缺点
当偶尔有冷数据被读取时,它便会被加入头节点,只有当所有尾部节点都被读取或超长移除时才会淘汰掉它,会造成空间的浪费。在此可以加入次数的限制,只有超过某个调用次数时才会将数据加入到某个范围,例如超过一次则被加入三分之二缓存处,超过三次则被加入头节点,当然会带来并发相关的问题。