缓存淘汰算法

在高并发、高性能的质量要求不断提高时,我们首先会想到的就是利用缓存予以应对。

第一次请求时把计算好的结果存放在缓存中,下次遇到同样的请求时,把之前保存在缓存中的数据直接拿来使用。

但是,缓存的空间一般都是有限,不可能把所有的结果全部保存下来。那么,当缓存空间全部被占满再有新的数据需要被保存,就要决定删除原来的哪些数据。如何做这样决定需要使用缓存淘汰算法。

常用的缓存淘汰算法有:FIFO、LRU、LFU,下面我们就逐一介绍一下。

FIFO

FIFO,First In First Out,先进先出算法。判断被存储的时间,离目前最远的数据优先被淘汰。简单地说,先存入缓存的数据,先被淘汰。

最早存入缓存的数据,其不再被使用的可能性比刚存入缓存的可能性大。建立一个FIFO队列,记录所有在缓存中的数据。当一条数据被存入缓存时,就把它插在队尾上。需要被淘汰的数据一直在队列头。这种算法只是在按线性顺序访问数据时才是理想的,否则效率不高。因为那些常被访问的数据,往往在缓存中也停留得最久,结果它们却因变“老”而不得不被淘汰出去。

FIFO算法用队列实现就可以了,这里就不做代码实现了。

LRU

LRU,Least Recently Used,最近最少使用算法。判断最近被使用的时间,目前最远的数据优先被淘汰。简单地说,LRU 的淘汰规则是基于访问时间。

如果一个数据在最近一段时间没有被使用到,那么可以认为在将来它被使用的可能性也很小。因此,当缓存空间满时,最久没有使用的数据最先被淘汰。

在Java中,其实LinkedHashMap已经实现了LRU缓存淘汰算法,需要在构造函数第三个参数传入true,表示按照时间顺序访问。可以直接继承LinkedHashMap来实现。

package one.more;

import java.util.LinkedHashMap;
import java.util.Map; public class LruCache<K, V> extends LinkedHashMap<K, V> { /**
* 容量限制
*/
private int capacity; LruCache(int capacity) {
// 初始大小,0.75是装载因子,true是表示按照访问时间排序
super(capacity, 0.75f, true);
//缓存最大容量
this.capacity = capacity;
} /**
* 重写removeEldestEntry方法,如果缓存满了,则把链表头部第一个节点和对应的数据删除。
*/
@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > capacity;
}
}

我写一个简单的程序测试一下:

package one.more;

public class TestApp {

    public static void main(String[] args) {
LruCache<String, String> cache = new LruCache(3);
cache.put("keyA", "valueA");
System.out.println("put keyA");
System.out.println(cache);
System.out.println("========================="); cache.put("keyB", "valueB");
System.out.println("put keyB");
System.out.println(cache);
System.out.println("========================="); cache.put("keyC", "valueC");
System.out.println("put keyC");
System.out.println(cache);
System.out.println("========================="); cache.get("keyA");
System.out.println("get keyA");
System.out.println(cache);
System.out.println("========================="); cache.put("keyD", "valueD");
System.out.println("put keyD");
System.out.println(cache);
}
}

运行结果如下:

put keyA
{keyA=valueA}
=========================
put keyB
{keyA=valueA, keyB=valueB}
=========================
put keyC
{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
{keyC=valueC, keyA=valueA, keyD=valueD}

当然,这个不是面试官想要的,也不是我们想要的。我们可以使用双向链表和哈希表进行实现,哈希表用于存储对应的数据,双向链表用于数据被使用的时间先后顺序。

在访问数据时,如果数据已存在缓存中,则把该数据的对应节点移到链表尾部。如此操作,在链表头部的节点则是最近最少使用的数据。

当需要添加新的数据到缓存时,如果该数据已存在缓存中,则把该数据对应的节点移到链表尾部;如果不存在,则新建一个对应的节点,放到链表尾部;如果缓存满了,则把链表头部第一个节点和对应的数据删除。

package one.more;

import java.util.HashMap;
import java.util.Map; public class LruCache<K, V> { /**
* 头结点
*/
private Node head;
/**
* 尾结点
*/
private Node tail;
/**
* 容量限制
*/
private int capacity;
/**
* key和数据的映射
*/
private Map<K, Node> map; LruCache(int capacity) {
this.capacity = capacity;
this.map = new HashMap<>();
} public V put(K key, V value) {
Node node = map.get(key);
// 数据存在,将节点移动到队尾
if (node != null) {
V oldValue = node.value;
//更新数据
node.value = value;
moveToTail(node);
return oldValue;
} else {
Node newNode = new Node(key, value);
// 数据不存在,判断链表是否满
if (map.size() == capacity) {
// 如果满,则删除队首节点,更新哈希表
map.remove(removeHead().key);
}
// 放入队尾节点
addToTail(newNode);
map.put(key, newNode);
return null;
}
} public V get(K key) {
Node node = map.get(key);
if (node != null) {
moveToTail(node);
return node.value;
}
return null;
} @Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LruCache{");
Node curr = this.head;
while (curr != null) {
if(curr != this.head){
sb.append(',').append(' ');
}
sb.append(curr.key);
sb.append('=');
sb.append(curr.value);
curr = curr.next;
}
return sb.append('}').toString();
} private void addToTail(Node newNode) {
if (newNode == null) {
return;
}
if (head == null) {
head = newNode;
tail = newNode;
} else {
//连接新节点
tail.next = newNode;
newNode.pre = tail;
//更新尾节点指针为新节点
tail = newNode;
}
} private void moveToTail(Node node) {
if (tail == node) {
return;
}
if (head == node) {
head = node.next;
head.pre = null;
} else {
//调整双向链表指针
node.pre.next = node.next;
node.next.pre = node.pre;
}
node.pre = tail;
node.next = null;
tail.next = node;
tail = node;
} private Node removeHead() {
if (head == null) {
return null;
}
Node res = head;
if (head == tail) {
head = null;
tail = null;
} else {
head = res.next;
head.pre = null;
res.next = null;
}
return res;
} class Node {
K key;
V value;
Node pre;
Node next; Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}

再次运行测试程序,结果如下:

put keyA
LruCache{keyA=valueA}
=========================
put keyB
LruCache{keyA=valueA, keyB=valueB}
=========================
put keyC
LruCache{keyA=valueA, keyB=valueB, keyC=valueC}
=========================
get keyA
LruCache{keyB=valueB, keyC=valueC, keyA=valueA}
=========================
put keyD
LruCache{keyC=valueC, keyA=valueA, keyD=valueD}

LFU

LFU,Least Frequently Used,最不经常使用算法,在一段时间内,数据被使用次数最少的,优先被淘汰。简单地说,LFU 的淘汰规则是基于访问次数。

如果一个数据在最近一段时间很少被使用到,那么可以认为在将来它被使用的可能性也很小。因此,当空间满时,最小频率使用的数据最先被淘汰。

我们可以使用双哈希表进行实现,一个哈希表用于存储对应的数据,另一个哈希表用于存储数据被使用次数和对应的数据。

package one.more;

import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors; public class LfuCache<K, V> { /**
* 容量限制
*/
private int capacity; /**
* 当前最小使用次数
*/
private int minUsedCount; /**
* key和数据的映射
*/
private Map<K, Node> map;
/**
* 数据频率和对应数据组成的链表
*/
private Map<Integer, List<Node>> usedCountMap; public LfuCache(int capacity) {
this.capacity = capacity;
this.minUsedCount = 1;
this.map = new HashMap<>();
this.usedCountMap = new HashMap<>();
} public V get(K key) { Node node = map.get(key);
if (node == null) {
return null;
}
// 增加数据的访问频率
addUsedCount(node);
return node.value;
} public V put(K key, V value) {
Node node = map.get(key);
if (node != null) {
// 如果存在则增加该数据的访问频次
V oldValue = node.value;
node.value = value;
addUsedCount(node);
return oldValue;
} else {
// 数据不存在,判断链表是否满
if (map.size() == capacity) {
// 如果满,则删除队首节点,更新哈希表
List<Node> list = usedCountMap.get(minUsedCount);
Node delNode = list.get(0);
list.remove(delNode);
map.remove(delNode.key);
}
// 新增数据并放到数据频率为1的数据链表中
Node newNode = new Node(key, value);
map.put(key, newNode);
List<Node> list = usedCountMap.get(1);
if (list == null) {
list = new LinkedList<>();
usedCountMap.put(1, list);
} list.add(newNode);
minUsedCount = 1;
return null;
}
} @Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("LfuCache{");
List<Integer> usedCountList = this.usedCountMap.keySet().stream().collect(Collectors.toList());
usedCountList.sort(Comparator.comparingInt(i -> i));
int count = 0;
for (int usedCount : usedCountList) {
List<Node> list = this.usedCountMap.get(usedCount);
if (list == null) {
continue;
}
for (Node node : list) {
if (count > 0) {
sb.append(',').append(' ');
}
sb.append(node.key);
sb.append('=');
sb.append(node.value);
sb.append("(UsedCount:");
sb.append(node.usedCount);
sb.append(')');
count++;
}
}
return sb.append('}').toString();
} private void addUsedCount(Node node) {
List<Node> oldList = usedCountMap.get(node.usedCount);
oldList.remove(node); // 更新最小数据频率
if (minUsedCount == node.usedCount && oldList.isEmpty()) {
minUsedCount++;
} node.usedCount++;
List<Node> set = usedCountMap.get(node.usedCount);
if (set == null) {
set = new LinkedList<>();
usedCountMap.put(node.usedCount, set);
}
set.add(node);
} class Node { K key;
V value;
int usedCount = 1; Node(K key, V value) {
this.key = key;
this.value = value;
}
}
}

再次运行测试程序,结果如下:

put keyA
LfuCache{keyA=valueA(UsedCount:1)}
=========================
put keyB
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1)}
=========================
put keyC
LfuCache{keyA=valueA(UsedCount:1), keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1)}
=========================
get keyA
LfuCache{keyB=valueB(UsedCount:1), keyC=valueC(UsedCount:1), keyA=valueA(UsedCount:2)}
=========================
put keyD
LfuCache{keyC=valueC(UsedCount:1), keyD=valueD(UsedCount:1), keyA=valueA(UsedCount:2)}

总结

看到这里,你已经超越了大多数人!

  • FIFO,First In First Out,先进先出算法。判断被存储的时间,离目前最远的数据优先被淘汰,可以使用队列实现。
  • LRU,Least Recently Used,最近最少使用算法。判断最近被使用的时间,目前最远的数据优先被淘汰,可以使用双向链表和哈希表实现。
  • LFU,Least Frequently Used,最不经常使用算法,在一段时间内,数据被使用次数最少的,优先被淘汰,可以使用双哈希表实现。

竟然已经看到这里了,你我定是有缘人,留下你的点赞关注,他日必成大器。

微信公众号:万猫学社

微信扫描二维码

关注后回复「电子书」

获取12本Java必读技术书籍

昨天面试被问到的 缓存淘汰算法FIFO、LRU、LFU及Java实现的更多相关文章

  1. 图解缓存淘汰算法二之LFU

    1.概念分析 LFU(Least Frequently Used)即最近最不常用.从名字上来分析,这是一个基于访问频率的算法.与LRU不同,LRU是基于时间的,会将时间上最不常访问的数据淘汰;LFU为 ...

  2. 缓存淘汰算法之LRU

    1. LRU1.1. 原理 LRU(Least recently used,最近最少使用)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”. ...

  3. 缓存淘汰算法之LRU实现

    Java中最简单的LRU算法实现,就是利用 LinkedHashMap,覆写其中的removeEldestEntry(Map.Entry)方法即可 如果你去看LinkedHashMap的源码可知,LR ...

  4. 缓存淘汰算法之FIFO

    前段时间去网易面试,被这个问题卡住,先做总结如下: 常用缓存淘汰算法 FIFO类:First In First Out,先进先出.判断被存储的时间,离目前最远的数据优先被淘汰. LRU类:Least ...

  5. 聊聊缓存淘汰算法-LRU 实现原理

    前言 我们常用缓存提升数据查询速度,由于缓存容量有限,当缓存容量到达上限,就需要删除部分数据挪出空间,这样新数据才可以添加进来.缓存数据不能随机删除,一般情况下我们需要根据某种算法删除缓存数据.常用淘 ...

  6. 04 | 链表(上):如何实现LRU缓存淘汰算法?

    今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是+LRU+缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...

  7. 数据结构与算法之美 06 | 链表(上)-如何实现LRU缓存淘汰算法

    常见的缓存淘汰策略: 先进先出 FIFO 最少使用LFU(Least Frequently Used) 最近最少使用 LRU(Least Recently Used) 链表定义: 链表也是线性表的一种 ...

  8. 链表:如何实现LRU缓存淘汰算法?

    缓存淘汰策略: FIFO:先入先出策略 LFU:最少使用策略 LRU:最近最少使用策略   链表的数据结构: 可以看到,数组需要连续的内存空间,当内存空间充足但不连续时,也会申请失败触发GC,链表则可 ...

  9. 《数据结构与算法之美》 <04>链表(上):如何实现LRU缓存淘汰算法?

    今天我们来聊聊“链表(Linked list)”这个数据结构.学习链表有什么用呢?为了回答这个问题,我们先来讨论一个经典的链表应用场景,那就是 LRU 缓存淘汰算法. 缓存是一种提高数据读取性能的技术 ...

随机推荐

  1. Pod:Kubernetes最小执行单元

    Pod基本概念理解 Pod是什么 Pod 是 Kubernetes 应用程序的基本执行单元,它是 Kubernetes 对象模型中创建或部署的最小和最简单的单元. 一个Pod可以包括一个或者多个容器. ...

  2. json 转换C# class(用于对接api

    //说明//使用场景:对接api,返回json结果,直接转换C# class//如何使用:复制下面js代码在浏览器控制台执行 ` "order_item_id": "28 ...

  3. [转载]Win10蓝牙设备删除后无法连接解决办法

    转自 https://blog.csdn.net/Tokeyman/article/details/86268005 现象 一般情况下,当操作系统无法与蓝牙设备,比如鼠标键盘等出现无法连接的情况,通过 ...

  4. 使用redis+lua实现SQL中的select intersect的效果

    公众号文章地址 1.需求 业务中需要实现在两个集合中搜索数据,并返回交集. 用SQL的伪代码可以描述如下: select key from set1 where sorted_key between ...

  5. golang中使用zap日志库

    1. 快速使用 package main import ( "go.uber.org/zap" "time" ) func main() { // 1. sug ...

  6. URL Rewrite(四种重定向策略)

    目录 一:Rewrite基本概述 1.Rewrite简介 2.Rewrite基本概述 3.Rewrite作用 4.什么是URL? 二:rewrite语法 三:Rewrite标记Flag 1.last和 ...

  7. k8s对接jenkins通用pipeline

    pipeline { agent any tools { //工具必须预先在jenkins中预配置 maven 'mvn' jdk 'jdk' } stages { stage('Env') { st ...

  8. python函数关键字实参传参

    #!/usr/bin/python #coding=utf-8 #好好学习,天天向上 def describe_pet(type,name): print(f"i have a {type} ...

  9. 设置redis能远程访问

    远程服务器,redis 安装在/opt下redis-4.0.10 cd redis-4.0.10 修改配置文件redis.conf配置文件:(注释掉bind:127.0.0.1)和修改保护模式为no ...

  10. CF Round #669 Div2

    A 可以发现不论往怎样一个串往后加上两个 \(0\) 或两个 \(1\) 其奇数位和偶数位上的差值都是相同的.因此我们两位两位考虑这个 \(01\) 串,对于相邻两位相同那么直接留下,否则留下 \(0 ...