咱们经常使用缓存提高数据查询速度,因为缓存容量有限,当缓存容量到达上限,就须要删除部分数据挪出空间,这样新数据才能够添加进来。缓存数据不能随机删除,通常状况下咱们须要根据某种算法删除缓存数据。经常使用淘汰算法有 LRU,LFU,FIFO,这篇文章咱们聊聊 LRU 算法。java
LRU 是 Least Recently Used 的缩写,这种算法认为最近使用的数据是热门数据,下一次很大几率将会再次被使用。而最近不多被使用的数据,很大几率下一次再也不用到。当缓存容量的满时候,优先淘汰最近不多使用的数据。node
假设如今缓存内部数据如图所示:算法
这里咱们将列表第一个节点称为头结点,最后一个节点为尾结点。数据库
当调用缓存获取 key=1 的数据,LRU 算法须要将 1 这个节点移动到头结点,其他节点不变,如图所示。编程
而后咱们插入一个 key=8 节点,此时缓存容量到达上限,因此加入以前须要先删除数据。因为每次查询都会将数据移动到头结点,未被查询的数据就将会下沉到尾部节点,尾部的数据就能够认为是最少被访问的数据,因此删除尾结点的数据。缓存
而后咱们直接将数据添加到头结点。数据结构
这里总结一下 LRU 算法具体步骤:ide
上面例子中能够看到,LRU 算法须要添加头节点,删除尾结点。而链表添加节点/删除节点时间复杂度 O(1),很是适合当作存储缓存数据容器。可是不能使用普通的单向链表,单向链表有几点劣势:this
针对以上问题,能够结合其余数据结构解决。idea
使用散列表存储节点,获取节点的复杂度将会下降为 O(1)。节点移动问题能够在节点中再增长前驱指针,记录上一个节点信息,这样链表就从单向链表变成了双向链表。
综上使用双向链表加散列表结合体,数据结构如图所示:
在双向链表中特地增长两个『哨兵』节点,不用来存储任何数据。使用哨兵节点,增长/删除节点的时候就能够不用考虑边界节点不存在状况,简化编程难度,下降代码复杂度。
LRU 算法实现代码以下,为了简化 key ,val 都认为 int 类型。
public class LRUCache {
Entry head, tail;
int capacity;
int size;
Map<Integer, Entry> cache;
public LRUCache(int capacity) {
this.capacity = capacity;
// 初始化链表
initLinkedList();
size = 0;
cache = new HashMap<>(capacity + 2);
}
/** * 若是节点不存在,返回 -1.若是存在,将节点移动到头结点,并返回节点的数据。 * * @param key * @return */
public int get(int key) {
Entry node = cache.get(key);
if (node == null) {
return -1;
}
// 存在移动节点
moveToHead(node);
return node.value;
}
/** * 将节点加入到头结点,若是容量已满,将会删除尾结点 * * @param key * @param value */
public void put(int key, int value) {
Entry node = cache.get(key);
if (node != null) {
node.value = value;
moveToHead(node);
return;
}
// 不存在。先加进去,再移除尾结点
// 此时容量已满 删除尾结点
if (size == capacity) {
Entry lastNode = tail.pre;
deleteNode(lastNode);
cache.remove(lastNode.key);
size--;
}
// 加入头结点
Entry newNode = new Entry();
newNode.key = key;
newNode.value = value;
addNode(newNode);
cache.put(key, newNode);
size++;
}
private void moveToHead(Entry node) {
// 首先删除原来节点的关系
deleteNode(node);
addNode(node);
}
private void addNode(Entry node) {
head.next.pre = node;
node.next = head.next;
node.pre = head;
head.next = node;
}
private void deleteNode(Entry node) {
node.pre.next = node.next;
node.next.pre = node.pre;
}
public static class Entry {
public Entry pre;
public Entry next;
public int key;
public int value;
public Entry(int key, int value) {
this.key = key;
this.value = value;
}
public Entry() {
}
}
private void initLinkedList() {
head = new Entry();
tail = new Entry();
head.next = tail;
tail.pre = head;
}
public static void main(String[] args) {
LRUCache cache = new LRUCache(2);
cache.put(1, 1);
cache.put(2, 2);
System.out.println(cache.get(1));
cache.put(3, 3);
System.out.println(cache.get(2));
}
}
复制代码
缓存命中率是缓存系统的很是重要指标,若是缓存系统的缓存命中率太低,将会致使查询回流到数据库,致使数据库的压力升高。
结合以上分析 LRU 算法优缺点。
LRU 算法优点在于算法实现难度不大,对于对于热点数据, LRU 效率会很好。
LRU 算法劣势在于对于偶发的批量操做,好比说批量查询历史数据,就有可能使缓存中热门数据被这些历史数据替换,形成缓存污染,致使缓存命中率降低,减慢了正常数据查询。
如下方案来源与 MySQL InnoDB LRU 改进算法
将链表拆分红两部分,分为热数据区,与冷数据区,如图所示。
改进以后算法流程将会变成下面同样:
对于偶发的批量查询,数据仅仅只会落入冷数据区,而后很快就会被淘汰出去。热门数据区的数据将不会受到影响,这样就解决了 LRU 算法缓存命中率降低的问题。
其余改进方法还有 LRU-K,2Q,LIRS 算法,感兴趣同窗能够自行查阅。
欢迎关注个人公众号:程序通事,得到平常干货推送。若是您对个人专题内容感兴趣,也能够关注个人博客:studyidea.cn