一、缓存置换的要点
缓存在计算机领域运用的非常多,计算机组成、操作系统以及我们的应用,很多地方都使用了缓存或利用了其思想。那么为什么要有缓存以及缓存又有哪些地方是需要注意的?下面我将一一介绍。
为什么要有缓存?这个问题在不同的具体应用中答案不同。那么具体到实际情况中,在计算机组成原理中我们知道,在CPU与主内存之间是存在多级缓存的,这个缓存存在的目的就是提高CPU与主内存之间交换数据的速率。而在我们的应用中,我们经常遇见的需求就是查询,那么怎么样让查询的反应时间更快呢?一个策略就是在中间增加一层缓存,每次查询数据不必非要去数据库查询,可能我们要的数据在缓存中已经存在了,这里它就提高了我们应用的敏捷性。
有了缓存就会有一个新的问题等待着我们,那就是缓存中的数据怎么更新。这其实也是一个比较复杂的问题,在具体项目中采取的解决方法也不尽相同。我们在这里主要讨论的是怎么替换缓存中的数据。
二、FIFO(先进先出算法)
它的含义就是当缓存已满时,需要替换新的数据时按照先进先出的策略去替换。即把最早放进缓存中的数据给清除掉,给新来的数据腾个位置。思想很简单,下面看看具体实现。
实现思路:思路应该算比较简单,直接使用队列来实现,缓存未满时缓存数据从队首入队,缓存已满时,队尾元素删除,队首入队。那么这个队列应该怎么实现呢?由于我们存在频繁的删除元素和添加元素的操作,所以我们可以使用链表作为其存储结构。最终我选择了双向链表来实现这个算法。双向链表的实现如下:实现起来不难,都有注释,就不罗嗦了。
//双向链表
public class DoubleLinkedList<K,V> {
private class Node<K,V>{
K key;
V value;
Node next;
Node before;
public Node() {
}
public Node(K key, V value) {
this(key, value, null, null);
}
public Node(K key, V value, Node next, Node before) {
this.key = key;
this.value = value;
this.next = next;
this.before = before;
}
}
private Node head;
private Node tail;
private int size;
public DoubleLinkedList() {
head = new Node();
tail = null;
head.next = tail;
size = 0;
}
//头部添加节点
public void addFirst(K key, V value) {
if(containsKey(key)) {
throw new IllegalStateException("Key is exist");
}
Node newNode = new Node(key, value);
newNode.next = head.next;
newNode.before = head;
if(head.next != null){
head.next.before = newNode;
}
head.next = newNode;
size ++;
if(size == 1){
tail = newNode;
}
}
//尾部添加节点
public void addLast(K key, V value) {
Node newNode = new Node(key, value);
newNode.before = tail.before;
newNode.before.next = newNode;
tail = newNode;
size ++;
}
//删除尾部节点
public V removeLast() {
if(tail == null){
throw new IllegalStateException("DoubleLinkedList is empty.");
}
V value = (V) tail.value;
Node node = tail.before;
tail = node;
tail.next = null;
size--;
return value;
}
//删除指定节点
public V removeNode(K key) {
V value;
if(!containsKey(key)){
throw new IllegalArgumentException("Element doesn't exist.");
}
Node target = findNode(key);
value = (V)target.value;
if(target != null){
Node before = target.before;
before.next = target.next;
target.before = null; //切断target节点与before节点的联系
target.next.before = before;
target.next = null; //切断target节点与next节点的联系
}
return value;
}
//查询节点的值
public V find(K key) {
Node cur = head.next;
while(cur != null){
if(cur.key.equals(key)){
return (V)cur.value;
}
cur = cur.next;
}
return null;
}
//查询指定的节点,可能返回null
public Node findNode(K key) {
Node cur = head.next;
while(cur != null){
if(key.equals(cur.key)) {
return cur;
}
cur = cur.next;
}
return null;
}
//修改节点
public void setValue(K key, V value) {
if(containsKey(key)) {
throw new IllegalArgumentException("Key doesn't exist");
}
Node cur = head.next;
while(cur != null) {
if(cur.key.equals(key)){
cur.value = value;
}
cur = cur.next;
}
}
//判断key是否存在
public boolean containsKey(K key){
Node cur = head.next;
while(cur != null) {
if(cur.key.equals(key)){
return true;
}
cur = cur.next;
}
return false;
}
//获取双向链表中元素的个数
public Integer getSize(){
return size;
}
}
而FIFO算法实现如下:
public class FIFO<K,V> {
//缓存池
private DoubleLinkedList<K,V> cache;
//缓存池容量
private Integer capacity;
//模拟数据库
private HashMap<K,V> map;
//初始容量为10
public FIFO() {
this(10);
}
//自定义缓存池容量
public FIFO(Integer capacity) {
this.cache = new DoubleLinkedList();
this.capacity = capacity;
map = new HashMap<>();
}
//从缓存中查找元素
public V get(K key) {
V value = cache.find(key);
/*
* 缓存池中没有,假设数据库或内存中存在
* 更新缓存
*/
if(value == null) {
//缓存已满,删除最先进入缓存的元素
if(cache.getSize() == capacity) {
cache.removeLast();
cache.addFirst(key,map.get(key));
}else { //缓存池未满
cache.addFirst(key, map.get(key));
}
value = map.get(key);
}
return value;
}
}
三、LRU(最近最不经常使用算法)
那么什么是LRU算法呢?它的意思其实是每次替换缓存中的元素时,优先替换最近一段时间都没有使用的元素。它的实现利用双向链表也很简单了。缓存已满需要替换元素时,将链表尾部元素删除,从链表头部添加元素,这样子就代表了最近使用过的元素都在链表头部位置,而链表尾部位置的元素都是最近没有使用过的。下面是LRU的实现:
/**
* 缓存置换策略的LRU算法(Least recently used)
* 即最近最少使用算法,最近一段时间未被使用的缓存,将来被使用的概率也很小,类似于局部性原理
* 将最近(时间)使用过的元素放置在双向链表头部,代表最近使用过的,链表尾部就代表最近最少使用的
* 当有新元素加入缓存时且缓存已满时将最近最少使用的元素删除移到链表头部,
* 使用双向链表实现,每次从缓存中查询元素时,如果缓存中存在,则将元素移到链表头部
* 若缓存中不存在,则将元素存入缓存中
* 此时若缓存已满,则将链表尾部元素删除,将元素添加到链表头部
* 若没满,直接添加到链表头部
*/
public class LRU<K,V> {
private DoubleLinkedList<K,V> cache; //缓存容器
private Integer capacity; //缓存容量
//代表数据库
private HashMap<K,V> map;
//缓存默认容量为20
public LRU() {
cache = new DoubleLinkedList<>();
capacity = 20;
}
//自定义缓存容量
public LRU(Integer capacity){
cache = new DoubleLinkedList<>();
this.capacity = capacity;
map = new HashMap<>();
}
//从缓存中查询元素
public V get(K key){
if(cache.containsKey(key)){ //缓存中已存在
//将元素移动到链表头部
V value = cache.removeNode(key);
cache.addFirst(key,value);
return value;
}
//缓存中不存在
if(cache.getSize() == capacity) { //缓存已满
cache.removeLast(); //删除最近最少使用的元素
cache.addFirst(key, map.get(key)); //从内存或数据库查找存入缓存
}else { //缓存中不存在且缓存未满
cache.addFirst(key, map.get(key)); //从内存或数据库查找存入缓存
}
return map.get(key);
}
}