前言
在系统中,特别是后端,我们经常会遇到以下问题:
-
数据库 QPS 成为系统瓶颈
-
热点数据被频繁访问
-
网络 / 磁盘 IO 成本高
缓存(Cache) 的核心目标只有一个:
用空间换时间,减少昂贵资源的访问次数
而在缓存系统中,一个绕不开的问题是:
缓存满了,该淘汰谁?
这就引出了缓存淘汰算法,其中最经典、最常用的就是 ------ LRU(Least Recently Used)最近最少使用算法。
一、LRU是什么?
LRU(最近最少使用) 的核心思想非常直观:
如果一条数据最近被访问过,那么它将来被再次访问的概率也更高
因此:
-
优先保留「最近访问过」的数据
-
淘汰「很久没被访问」的数据
举个栗子:
使用情况为:A → B → C → A → D的缓存,最先被淘汰的是哪个?
答案是:B,它最久未被访问,所以会最先被淘汰
二、如何实现?
自己写一个(leetcode146LRU缓存)
java
class LRUCache {
/**
* 双向链表节点
* 用于维护访问顺序
*/
class DLinkedList {
// 前驱节点
DLinkedList prev;
// 后继节点
DLinkedList next;
// 缓存的 value
int value;
// 缓存的 key(用于从 HashMap 中删除)
int key;
// 无参构造(用于创建虚拟头尾节点)
public DLinkedList() {}
// 有参构造(用于创建真实缓存节点)
public DLinkedList(int value, int key) {
this.key = key;
this.value = value;
}
}
/**
* 虚拟头节点(不存数据)
* head.next 指向最近访问的节点
*/
private DLinkedList head;
/**
* 虚拟尾节点(不存数据)
* tail.prev 指向最久未访问的节点
*/
private DLinkedList tail;
// 缓存最大容量
private int capacity;
// 当前缓存大小
int size;
/**
* HashMap:key -> 双向链表节点
* 作用:O(1) 时间复杂度定位节点
*/
private HashMap<Integer, DLinkedList> cache = new HashMap<>();
/**
* 初始化 LRUCache
*/
public LRUCache(int capacity) {
this.capacity = capacity;
this.size = 0;
// 初始化虚拟头尾节点
head = new DLinkedList();
tail = new DLinkedList();
// 构建初始双向链表结构:head <-> tail
head.next = tail;
tail.prev = head;
}
/**
* 获取缓存
* @param key
* @return value 或 -1
*/
public int get(int key) {
// 从 HashMap 中查找节点
DLinkedList node = cache.get(key);
// 不存在,直接返回 -1
if (node == null) {
return -1;
}
// 存在则说明被访问,需要移动到链表头部
moveToHead(node);
// 返回节点值
return node.value;
}
/**
* 写入缓存
* @param key
* @param value
*/
public void put(int key, int value) {
// 尝试从缓存中获取节点
DLinkedList node = cache.get(key);
// 如果 key 不存在
if (node == null) {
// 创建新节点
DLinkedList newNode = new DLinkedList(value, key);
// 缓存数量 +1
++size;
// 放入 HashMap
cache.put(key, newNode);
// 新节点是最近访问的,插入到链表头部
addToHead(newNode);
// 如果超出容量,淘汰最久未使用节点
if (size > capacity) {
// 移除链表尾部节点
DLinkedList removed = removeTail();
// 从 HashMap 中删除对应 key
cache.remove(removed.key);
// 缓存数量 -1
size--;
}
} else {
// key 已存在,更新 value
node.value = value;
// 更新后同样视为最近访问
moveToHead(node);
}
}
/**
* 将节点添加到链表头部
* head <-> node <-> 原 head.next
*/
private void addToHead(DLinkedList node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
/**
* 从链表中移除指定节点
*/
private void removeNode(DLinkedList node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
/**
* 将指定节点移动到链表头部
* 本质:先删除,再插入头部
*/
private void moveToHead(DLinkedList node) {
removeNode(node);
addToHead(node);
}
/**
* 移除并返回链表尾部节点
* 即最久未使用的节点
*/
private DLinkedList removeTail() {
// tail.prev 是真实尾节点
DLinkedList prev = tail.prev;
removeNode(prev);
return prev;
}
}
Java自带-LinkedHashMap
LinkedHashMap 的本质
LinkedHashMap =
HashMap + 双向链表
它在 HashMap 的基础上,额外维护了一条双向链表,用来记录元素顺序
那么就可以很轻松用它实现LRU
java
int MAX_CAPACITY = 100;
Map<Integer, Integer> lruCache =
new LinkedHashMap<Integer, Integer>(16, 0.75f, true) {
@Override
protected boolean removeEldestEntry(
Map.Entry<Integer, Integer> eldest) {
return size() > MAX_CAPACITY;
}
};
总结
LRU 是一种基于"最近访问时间"的缓存淘汰算法,通常使用 HashMap + 双向链表实现,保证 get / put 操作均为 O(1),在实际工程中被广泛应用。