目录
一、主题介绍
缓存是提高系统性能的重要技术手段,LRU(Least Recently Used,最近最少使用)和 LFU(Least Frequently Used,最不经常使用)是两种常见的缓存替换算法。它们在不同的场景下有各自的优势和适用范围。LRU 根据数据的最近访问时间来决定淘汰哪些数据,而 LFU 则依据数据的访问频率。了解它们的原理和实现方式对于优化系统性能,特别是在处理大量数据访问时非常关键。
二、LRU 算法
-
原理
- LRU 算法认为最近被访问的数据在未来一段时间内更有可能被再次访问,因此当缓存空间满时,它会淘汰最近最少使用的数据。例如,在一个网页浏览器的缓存中,最近浏览过的页面更有可能被再次查看,而很久之前浏览过的页面则可以被替换掉以释放空间给新的页面数据。
-
实现方式
- 通常可以使用双向链表和哈希表来实现 LRU 算法。双向链表用于维护数据的顺序,按照最近访问的时间顺序排列,头部是最近访问的数据,尾部是最近最少访问的数据。哈希表则用于快速查找数据在链表中的位置,以提高访问效率。
- 当有数据被访问时,将其从链表中移除并插入到头部,表示它是最近被访问的。当需要淘汰数据时,直接删除链表尾部的数据。
-
Java 代码示例
import java.util.HashMap;
import java.util.Map;
// 节点类,用于双向链表
class Node {
int key;
int value;
Node prev;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
}
}
// LRU缓存类
class LRUCache {
private int capacity;
private Map<Integer, Node> map;
private Node head;
private Node tail;
public LRUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
public int get(int key) {
if (map.containsKey(key)) {
Node node = map.get(key);
removeNode(node);
addToHead(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
if (map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
removeNode(node);
addToHead(node);
} else {
Node newNode = new Node(key, value);
if (map.size() >= capacity) {
Node lastNode = tail.prev;
removeNode(lastNode);
map.remove(lastNode.key);
}
addToHead(newNode);
map.put(key, newNode);
}
}
private void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
private void addToHead(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
}
- Vue 3 + TS 前端使用场景示例
假设在一个前端应用中,需要缓存一些频繁访问的用户数据。可以创建一个 LRU 缓存对象来存储这些数据,当用户切换页面或进行某些操作时,快速获取缓存中的数据,减少对后端的重复请求。
import { ref } from 'vue';
class VueLRUCache {
private lruCache: LRUCache;
constructor(capacity: number) {
this.lruCache = new LRUCache(capacity);
}
getFromCache(key: string): any {
const intKey = parseInt(key);
const value = this.lruCache.get(intKey);
if (value!== -1) {
return value;
}
return null;
}
putInCache(key: string, value: any) {
const intKey = parseInt(key);
this.lruCache.put(intKey, value);
}
}
// 在组件中使用
const userDataCache = new VueLRUCache(10); // 假设缓存容量为10
const userData = ref(null);
const userId = '123';
// 尝试从缓存中获取用户数据
const cachedData = userDataCache.getFromCache(userId);
if (cachedData) {
userData.value = cachedData;
} else {
// 如果缓存中没有,从后端获取数据(这里只是模拟,实际需要调用API)
const fetchedData = { name: 'John Doe', age: 30 };
userData.value = fetchedData;
// 将数据放入缓存
userDataCache.putInCache(userId, fetchedData);
}
- Python 代码示例(模拟 LRU 缓存操作)
from collections import OrderedDict
# LRU缓存类
class LRUCachePython:
def __init__(self, capacity):
self.capacity = capacity
self.cache = OrderedDict()
def get(self, key):
if key in self.cache:
value = self.cache.pop(key)
self.cache[key] = value
return value
return -1
def put(self, key, value):
if key in self.cache:
self.cache.pop(key)
elif len(self.cache) >= self.capacity:
self.cache.popitem(last=False)
self.cache[key] = value
# 使用示例
lru_cache = LRUCachePython(3)
lru_cache.put(1, "value1")
lru_cache.put(2, "value2")
lru_cache.put(3, "value3")
print(lru_cache.get(1)) # 输出 "value1",此时1被移到最前面
lru_cache.put(4, "value4") # 容量满,删除最久未使用的2
print(list(lru_cache.cache.keys())) # 输出 [1, 3, 4]
三、LFU 算法
-
原理
- LFU 算法基于数据的访问频率来决定淘汰哪些数据。它会记录每个数据被访问的次数,当缓存空间满时,淘汰访问频率最低的数据。例如,在一个内容推荐系统中,那些很少被用户点击的文章或视频可以被替换掉,为更热门的内容腾出空间。
-
实现方式
- 一种常见的实现方式是使用哈希表和多个桶(buckets)来实现。哈希表用于存储数据和对应的访问频率信息,每个桶代表一个不同的访问频率级别。数据按照访问频率分配到不同的桶中,当有新的数据被访问时,其访问频率增加,可能会从当前桶移动到更高频率的桶中。当需要淘汰数据时,从最低频率的桶中选择数据进行删除。
-
Java 代码示例
import java.util.HashMap;
import java.util.Map;
// 节点类,用于链表
class Node {
int key;
int value;
int frequency;
Node prev;
Node next;
public Node(int key, int value) {
this.key = key;
this.value = value;
frequency = 1;
}
}
// 桶类,用于管理相同频率的节点
class Bucket {
Node head;
Node tail;
public Bucket() {
head = new Node(0, 0);
tail = new Node(0, 0);
head.next = tail;
tail.prev = head;
}
// 添加节点到桶的头部
public void addNode(Node node) {
node.next = head.next;
node.prev = head;
head.next.prev = node;
head.next = node;
}
// 删除节点
public void removeNode(Node node) {
node.prev.next = node.next;
node.next.prev = node.prev;
}
// 是否为空桶
public boolean isEmpty() {
return head.next == tail;
}
}
// LFU缓存类
class LFUCache {
private int capacity;
private Map<Integer, Node> map;
private Map<Integer, Bucket> frequencyMap;
private int minFrequency;
public LFUCache(int capacity) {
this.capacity = capacity;
map = new HashMap<>();
frequencyMap = new HashMap<>();
minFrequency = 0;
}
public int get(int key) {
if (map.containsKey(key)) {
Node node = map.get(key);
increaseFrequency(node);
return node.value;
}
return -1;
}
public void put(int key, int value) {
if (capacity == 0) return;
if (map.containsKey(key)) {
Node node = map.get(key);
node.value = value;
increaseFrequency(node);
} else {
if (map.size() >= capacity) {
Bucket minBucket = frequencyMap.get(minFrequency);
Node lastNode = minBucket.tail.prev;
minBucket.removeNode(lastNode);
map.remove(lastNode.key);
}
Node newNode = new Node(key, value);
addToBucket(newNode);
map.put(key, newNode);
minFrequency = 1;
}
}
private void increaseFrequency(Node node) {
int oldFrequency = node.frequency;
Bucket oldBucket = frequencyMap.get(oldFrequency);
oldBucket.removeNode(node);
node.frequency++;
Bucket newBucket = frequencyMap.getOrDefault(node.frequency, new Bucket());
newBucket.addNode(node);
frequencyMap.put(node.frequency, newBucket);
if (oldBucket.isEmpty() && oldFrequency == minFrequency) {
minFrequency++;
}
}
private void addToBucket(Node node) {
Bucket bucket = frequencyMap.getOrDefault(1, new Bucket());
bucket.addNode(node);
frequencyMap.put(1, bucket);
}
}
- Vue 3 + TS 前端使用场景示例(假设用于缓存频繁访问的页面组件数据)
import { ref } from 'vue';
class VueLFUCache {
private lfuCache: LFUCache;
constructor(capacity: number) {
this.lfuCache = new LFUCache(capacity);
}
getFromCache(key: string): any {
const intKey = parseInt(key);
const value = this.lfuCache.get(intKey);
if (value!== -1) {
return value;
}
return null;
}
putInCache(key: string, value: any) {
const intKey = parseInt(key);
this.lfuCache.put(intKey, value);
}
}
// 在组件中使用
const componentDataCache = new VueLFUCache(5); // 假设缓存容量为5
const componentData = ref(null);
const componentKey = 'component1';
// 尝试从缓存中获取组件数据
const cachedData = componentDataCache.getFromCache(componentKey);
if (cachedData) {
componentData.value = cachedData;
} else {
// 如果缓存中没有,从后端获取数据(这里只是模拟,实际需要调用API)
const fetchedData = { name: 'Component Data', props: { someProp: 'value' } };
componentData.value = fetchedData;
// 将数据放入缓存
componentDataCache.putInCache(componentKey, fetchedData);
}
- Python 代码示例(模拟 LFU 缓存操作)
from collections import defaultdict, OrderedDict
# LFU缓存类
class LFUCachePython:
def __init__(self, capacity):
self.capacity = capacity
self.cache = {}
self.frequency = defaultdict(OrderedDict)
self.min_frequency = 0
def get(self, key):
if key in self.cache:
value, freq = self.cache[key]
del self.frequency[freq][key]
if not self.frequency[freq]:
del self.frequency[freq]
freq += 1
self.frequency[freq][key] = value
self.cache[key] = (value, freq)
if freq == 1:
self.min_frequency = 1
elif self.min_frequency == freq - 1 and not self.frequency[freq - 1]:
self.min_frequency = freq
return value
return -1
def put(self, key, value):
if self.capacity == 0:
return
if key in self.cache:
_, freq = self.cache[key]
self.cache[key] = (value, freq)
self.get(key)
else:
if len(self.cache) >= self.capacity:
oldest_key = next(iter(self.frequency[self.min_frequency]))
del self.cache[oldest_key]
del self.frequency[self.min_frequency][oldest_key]
if not self.frequency[self.min_frequency]:
del self.frequency[self.min_frequency]
self.cache[key] = (value, 1)
self.frequency[1][key] = value
self.min_frequency = 1
# 使用示例
lfu_cache = LFUCachePython(3)
lfu_cache.put(1, "value1")
lfu_cache.put(2, "value2")
lfu_cache.put(3, "value3")
lfu_cache.get(1)
lfu_cache.put(4, "value4") # 容量满,删除频率最低的2(假设1和3访问频率增加到2,2还是1)
print(lfu_cache.cache) # 输出 {1: ('value1', 2), 3: ('value3', 2), 4: ('value4', 1)}
四、LRU 与 LFU 的比较
-
适用场景
- LRU 适用于数据访问模式具有时间局部性的场景,即最近访问的数据更有可能被再次访问。例如,浏览器缓存页面、操作系统的内存管理等。
- LFU 适用于数据访问模式具有频率局部性的场景,即经常被访问的数据更有可能被再次访问。比如,缓存热点数据、频繁使用的功能模块的缓存等。
-
优缺点
- LRU 优点:
- 实现相对简单,通常只需要维护一个数据链表和一个哈希表。
- 对于那些访问模式具有明显时间局部性的数据,能够快速适应并保留最近常用的数据,提高缓存命中率。
- LRU 缺点:
- 可能会因为某些数据的偶尔频繁访问而将其他长期有用但近期未访问的数据淘汰,导致缓存抖动。
- 对于访问频率相同但最近访问时间不同的数据,无法区分优先级,可能会不合理地淘汰一些数据。
- LFU 优点:
- 更准确地反映数据的使用频率,能够优先保留真正的热点数据,减少因偶尔频繁访问导致的缓存替换错误。
- LFU 缺点:
- 实现相对复杂,需要维护数据的访问频率信息以及不同频率的链表或桶结构。
- 当数据的访问频率分布不均匀时,可能会导致某些低频但近期有用的数据过早被淘汰,因为它只关注频率而忽略了时间因素。
- LRU 优点:
在实际应用中,需要根据具体的业务需求和数据访问模式来选择合适的缓存算法。有时候也可以结合两者的优点,采用一些改进的算法或者策略来优化缓存性能。例如,可以设置一个频率阈值,当数据的访问频率超过该阈值时,将其按照 LRU 算法进行管理,以兼顾时间局部性和频率局部性。或者在 LFU 算法中引入时间衰减因子,使较旧的高频数据有一定几率被淘汰,以避免数据过度老化在缓存中。