- design-seckill.md: 秒杀系统设计 - design-shorturl.md: 短链接系统设计 - design-lbs.md: LBS附近的人系统设计 - design-im.md: 即时通讯系统设计 - design-feed.md: 社交信息流系统设计 Each document includes: - Requirements analysis and data volume assessment - Technical challenges - System architecture design - Database design - Caching strategies - Scalability considerations - Practical project experience - Alibaba P7 level additional points Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
524 lines
14 KiB
Markdown
524 lines
14 KiB
Markdown
# LRU 缓存实现
|
||
|
||
## 数据结构原理
|
||
|
||
### 什么是 LRU 缓存?
|
||
LRU(Least Recently Used)缓存是一种缓存淘汰算法,当缓存满时,会淘汰最近最少使用的数据。它基于局部性原理,认为最近使用的数据在将来也可能被再次使用。
|
||
|
||
### LRU 缓存的核心概念
|
||
|
||
1. **缓存容量**:缓存能存储的最大数据量
|
||
2. **访问时间**:数据被访问的时间戳
|
||
3. **淘汰策略**:当缓存满时,移除最久未使用的数据
|
||
4. **访问模式**:数据访问的时间和频率模式
|
||
|
||
### LRU 缓存的工作原理
|
||
|
||
1. **数据访问**:当数据被访问(读或写)时,将其标记为最近使用
|
||
2. **数据插入**:新数据插入时,如果缓存满,先淘汰最久未使用的数据
|
||
3. **数据查找**:查找数据时,如果存在,将其标记为最近使用
|
||
4. **缓存维护**:维护使用顺序,确保时间复杂度高效
|
||
|
||
## 图解说明
|
||
|
||
```
|
||
LRU 缓存工作流程示例:
|
||
|
||
初始状态: [] (容量=3)
|
||
|
||
1. 插入 A -> [A]
|
||
2. 插入 B -> [A, B]
|
||
3. 插入 C -> [A, B, C]
|
||
4. 访问 A -> [A, B, C] (A 被移到头部)
|
||
5. 揓入 D -> [B, C, D] (A 被淘汰)
|
||
6. 访问 C -> [B, C, D] (C 被移到头部)
|
||
7. 揓入 E -> [C, D, E] (B 被淘汰)
|
||
|
||
访问顺序: A, B, C, A, D, C, E
|
||
淘汰顺序: A, B
|
||
```
|
||
|
||
### LRU 与其他缓存策略对比
|
||
|
||
| 策略 | 淘汰标准 | 适用场景 |
|
||
|------|----------|----------|
|
||
| LRU | 最近最少使用 | 一般访问模式 |
|
||
| LFU | 最不经常使用 | 访问频率稳定 |
|
||
| FIFO | 先进先出 | 流水式数据处理 |
|
||
| Random | 随机淘汰 | 无法预测访问模式 |
|
||
|
||
## Java 代码实现
|
||
|
||
### 方法一:使用 LinkedHashMap(推荐)
|
||
|
||
```java
|
||
import java.util.LinkedHashMap;
|
||
import java.util.Map;
|
||
|
||
public class LRUCache<K, V> extends LinkedHashMap<K, V> {
|
||
private final int capacity;
|
||
|
||
public LRUCache(int capacity) {
|
||
super(capacity, 0.75f, true);
|
||
this.capacity = capacity;
|
||
}
|
||
|
||
@Override
|
||
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
|
||
return size() > capacity;
|
||
}
|
||
|
||
// 测试用例
|
||
public static void main(String[] args) {
|
||
LRUCache<Integer, String> cache = new LRUCache<>(3);
|
||
|
||
cache.put(1, "A");
|
||
cache.put(2, "B");
|
||
cache.put(3, "C");
|
||
System.out.println("Cache after insertion: " + cache);
|
||
|
||
cache.get(1);
|
||
System.out.println("Cache after accessing 1: " + cache);
|
||
|
||
cache.put(4, "D");
|
||
System.out.println("Cache after insertion 4: " + cache);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 方法二:手写实现(面试重点)
|
||
|
||
```java
|
||
import java.util.HashMap;
|
||
import java.util.Map;
|
||
|
||
class LRUCacheNode<K, V> {
|
||
K key;
|
||
V value;
|
||
LRUCacheNode<K, V> prev;
|
||
LRUCacheNode<K, V> next;
|
||
|
||
public LRUCacheNode(K key, V value) {
|
||
this.key = key;
|
||
this.value = value;
|
||
this.prev = null;
|
||
this.next = null;
|
||
}
|
||
}
|
||
|
||
public class LRUCacheImpl<K, V> {
|
||
private final int capacity;
|
||
private final Map<K, LRUCacheNode<K, V>> cache;
|
||
private final LRUCacheNode<K, V> head;
|
||
private final LRUCacheNode<K, V> tail;
|
||
|
||
public LRUCacheImpl(int capacity) {
|
||
this.capacity = capacity;
|
||
this.cache = new HashMap<>();
|
||
this.head = new LRUCacheNode<>(null, null);
|
||
this.tail = new LRUCacheNode<>(null, null);
|
||
head.next = tail;
|
||
tail.prev = head;
|
||
}
|
||
|
||
// 获取数据
|
||
public V get(K key) {
|
||
if (!cache.containsKey(key)) {
|
||
return null;
|
||
}
|
||
|
||
LRUCacheNode<K, V> node = cache.get(key);
|
||
moveToHead(node);
|
||
return node.value;
|
||
}
|
||
|
||
// 插入数据
|
||
public void put(K key, V value) {
|
||
if (cache.containsKey(key)) {
|
||
// 更新已有节点
|
||
LRUCacheNode<K, V> node = cache.get(key);
|
||
node.value = value;
|
||
moveToHead(node);
|
||
} else {
|
||
// 创建新节点
|
||
LRUCacheNode<K, V> newNode = new LRUCacheNode<>(key, value);
|
||
cache.put(key, newNode);
|
||
addToHead(newNode);
|
||
|
||
// 淘汰策略
|
||
if (cache.size() > capacity) {
|
||
LRUCacheNode<K, V> last = removeTail();
|
||
cache.remove(last.key);
|
||
}
|
||
}
|
||
}
|
||
|
||
// 移除指定节点
|
||
public void remove(K key) {
|
||
if (!cache.containsKey(key)) {
|
||
return;
|
||
}
|
||
|
||
LRUCacheNode<K, V> node = cache.get(key);
|
||
removeNode(node);
|
||
cache.remove(key);
|
||
}
|
||
|
||
// 清空缓存
|
||
public void clear() {
|
||
cache.clear();
|
||
head.next = tail;
|
||
tail.prev = head;
|
||
}
|
||
|
||
// 获取缓存大小
|
||
public int size() {
|
||
return cache.size();
|
||
}
|
||
|
||
// 检查是否包含键
|
||
public boolean containsKey(K key) {
|
||
return cache.containsKey(key);
|
||
}
|
||
|
||
// 辅助方法:添加到头部
|
||
private void addToHead(LRUCacheNode<K, V> node) {
|
||
node.prev = head;
|
||
node.next = head.next;
|
||
head.next.prev = node;
|
||
head.next = node;
|
||
}
|
||
|
||
// 辅助方法:移除节点
|
||
private void removeNode(LRUCacheNode<K, V> node) {
|
||
node.prev.next = node.next;
|
||
node.next.prev = node.prev;
|
||
}
|
||
|
||
// 辅助方法:移动到头部
|
||
private void moveToHead(LRUCacheNode<K, V> node) {
|
||
removeNode(node);
|
||
addToHead(node);
|
||
}
|
||
|
||
// 辅助方法:移除尾部节点
|
||
private LRUCacheNode<K, V> removeTail() {
|
||
LRUCacheNode<K, V> last = tail.prev;
|
||
removeNode(last);
|
||
return last;
|
||
}
|
||
|
||
// 打印缓存内容
|
||
public void printCache() {
|
||
LRUCacheNode<K, V> current = head.next;
|
||
while (current != tail) {
|
||
System.out.print("(" + current.key + "=" + current.value + ") ");
|
||
current = current.next;
|
||
}
|
||
System.out.println();
|
||
}
|
||
|
||
// 测试用例
|
||
public static void main(String[] args) {
|
||
LRUCacheImpl<Integer, String> cache = new LRUCacheImpl<>(3);
|
||
|
||
System.out.println("Inserting 1, 2, 3");
|
||
cache.put(1, "A");
|
||
cache.put(2, "B");
|
||
cache.put(3, "C");
|
||
cache.printCache();
|
||
|
||
System.out.println("Accessing 1");
|
||
cache.get(1);
|
||
cache.printCache();
|
||
|
||
System.out.println("Inserting 4");
|
||
cache.put(4, "D");
|
||
cache.printCache();
|
||
|
||
System.out.println("Removing 2");
|
||
cache.remove(2);
|
||
cache.printCache();
|
||
|
||
System.out.println("Clearing cache");
|
||
cache.clear();
|
||
cache.printCache();
|
||
}
|
||
}
|
||
```
|
||
|
||
### 方法三:使用双向队列(Deque)
|
||
|
||
```java
|
||
import java.util.Deque;
|
||
import java.util.HashMap;
|
||
import java.util.LinkedList;
|
||
import java.util.Map;
|
||
|
||
public class LRUCacheWithDeque<K, V> {
|
||
private final int capacity;
|
||
private final Map<K, V> cache;
|
||
private final Deque<K> accessQueue;
|
||
|
||
public LRUCacheWithDeque(int capacity) {
|
||
this.capacity = capacity;
|
||
this.cache = new HashMap<>();
|
||
this.accessQueue = new LinkedList<>();
|
||
}
|
||
|
||
public V get(K key) {
|
||
if (!cache.containsKey(key)) {
|
||
return null;
|
||
}
|
||
|
||
// 更新访问顺序
|
||
accessQueue.remove(key);
|
||
accessQueue.addFirst(key);
|
||
return cache.get(key);
|
||
}
|
||
|
||
public void put(K key, V value) {
|
||
if (cache.containsKey(key)) {
|
||
// 更新已有数据
|
||
cache.put(key, value);
|
||
accessQueue.remove(key);
|
||
accessQueue.addFirst(key);
|
||
} else {
|
||
// 添加新数据
|
||
if (cache.size() >= capacity) {
|
||
// 淘汰最久未使用的数据
|
||
K lruKey = accessQueue.removeLast();
|
||
cache.remove(lruKey);
|
||
}
|
||
cache.put(key, value);
|
||
accessQueue.addFirst(key);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 时间复杂度分析
|
||
|
||
### 操作时间复杂度
|
||
|
||
| 操作 | 时间复杂度 | 说明 |
|
||
|------|------------|------|
|
||
| get(K) | O(1) | 哈希查找 + 双向链表操作 |
|
||
| put(K,V) | O(1) | 哈希查找 + 双向链表操作 |
|
||
| remove(K) | O(1) | 哈希删除 + 双向链表操作 |
|
||
| clear() | O(1) | 清空哈希表和链表 |
|
||
| size() | O(1) | 哈希表大小 |
|
||
|
||
### 空间复杂度
|
||
|
||
- O(n) - 存储 n 个键值对
|
||
- 需要额外空间维护双向链表结构
|
||
|
||
### 性能分析
|
||
|
||
1. **最优实现**:HashMap + 双向链表 = O(1) 所有操作
|
||
2. **次优实现**:LinkedHashMap = O(1) 操作,但依赖 JDK 实现
|
||
3. **最差实现**:数组 + 遍历 = O(n) 操作
|
||
|
||
## 实际应用场景
|
||
|
||
### 1. Web 服务器缓存
|
||
- **静态资源缓存**:CSS、JS、图片文件
|
||
- **页面缓存**:动态生成的 HTML 页面
|
||
- **API 响应缓存**:频繁调用的 API 结果
|
||
|
||
```java
|
||
// Web 缓存示例
|
||
public class WebCache {
|
||
private final LRUCache<String, HttpResponse> cache;
|
||
|
||
public WebCache(int maxSize) {
|
||
this.cache = new LRUCacheImpl<>(maxSize);
|
||
}
|
||
|
||
public HttpResponse getPage(String url) {
|
||
HttpResponse response = cache.get(url);
|
||
if (response == null) {
|
||
response = fetchFromOrigin(url);
|
||
cache.put(url, response);
|
||
}
|
||
return response;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 2. 数据库查询缓存
|
||
- **ORM 缓存**:Hibernate、MyBatis 一级/二级缓存
|
||
- **查询结果缓存**:复杂查询结果的缓存
|
||
|
||
```java
|
||
// 数据库缓存示例
|
||
public class QueryCache {
|
||
private final LRUCache<String, ResultSet> queryCache;
|
||
|
||
public QueryCache(int maxSize) {
|
||
this.queryCache = new LRUCacheImpl<>(maxSize);
|
||
}
|
||
|
||
public ResultSet executeQuery(String sql) {
|
||
ResultSet result = queryCache.get(sql);
|
||
if (result == null) {
|
||
result = executeSql(sql);
|
||
if (result != null) {
|
||
queryCache.put(sql, result);
|
||
}
|
||
}
|
||
return result;
|
||
}
|
||
}
|
||
```
|
||
|
||
### 3. 内存数据库
|
||
- **Redis 缓存策略**:`maxmemory-policy allkeys-lru`
|
||
- **本地缓存**:Ehcache、Caffeine
|
||
|
||
```java
|
||
// 本地缓存示例
|
||
public class LocalCache {
|
||
private final LRUCache<String, Object> cache;
|
||
|
||
public LocalCache(int maxSize) {
|
||
this.cache = new LRUCacheImpl<>(maxSize);
|
||
}
|
||
|
||
public <T> T get(String key, Class<T> type) {
|
||
Object value = cache.get(key);
|
||
return type.cast(value);
|
||
}
|
||
|
||
public void put(String key, Object value) {
|
||
cache.put(key, value);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 4. 消息队列缓冲
|
||
- **消息去重**:防止重复处理消息
|
||
- **请求合并**:合并短时间内多个相同请求
|
||
|
||
```java
|
||
// 消息队列缓冲示例
|
||
public class MessageBuffer {
|
||
private final LRUCache<String, Message> messageBuffer;
|
||
private final Queue<Message> messageQueue;
|
||
|
||
public MessageBuffer(int maxSize) {
|
||
this.messageBuffer = new LRUCacheImpl<>(maxSize);
|
||
this.messageQueue = new LinkedList<>();
|
||
}
|
||
|
||
public void addMessage(Message message) {
|
||
String key = message.getId();
|
||
if (!messageBuffer.containsKey(key)) {
|
||
messageBuffer.put(key, message);
|
||
messageQueue.add(message);
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## 与其他缓存策略的对比
|
||
|
||
| 策略 | 时间复杂度 | 适用场景 | 优点 | 缺点 |
|
||
|------|------------|----------|------|------|
|
||
| LRU | O(1) | 一般访问模式 | 实现简单,效果好 | 对突发访问敏感 |
|
||
| LFU | O(1) | 频率稳定场景 | 更好处理热点数据 | 实现较复杂 |
|
||
| FIFO | O(1) | 流水式数据 | 实现简单 | 可能淘汰有用数据 |
|
||
| Random | O(1) | 随机访问模式 | 实现最简单 | 性能不稳定 |
|
||
|
||
### LRU 的优缺点
|
||
|
||
**优点**:
|
||
- 实现简单,易于理解
|
||
- 性能稳定,时间复杂度 O(1)
|
||
- 对大多数场景效果良好
|
||
- JDK 已有成熟实现
|
||
|
||
**缺点**:
|
||
- 对突发访问敏感(缓存污染)
|
||
- 需要额外维护访问顺序
|
||
- 内存占用相对较大
|
||
- 无法区分临时访问和频繁访问
|
||
|
||
## 常见面试问题
|
||
|
||
### Q1: 如何实现 LRU 缓存?为什么选择 HashMap + 双向链表?
|
||
**答**:
|
||
1. **HashMap** 提供 O(1) 时间复杂度的查找
|
||
2. **双向链表** 维护访问顺序,头节点最近访问,尾节点最久未访问
|
||
3. 结合使用可实现所有操作的 O(1) 时间复杂度
|
||
4. 其他方案(如数组)时间复杂度较高
|
||
|
||
### Q2: LRU 缓存存在什么问题?如何改进?
|
||
**答**:
|
||
**存在的问题**:
|
||
- 缓存污染:一次性大量访问可能导致有用数据被淘汰
|
||
- 无法区分临时访问和频繁访问
|
||
|
||
**改进方案**:
|
||
1. **LFU (Least Frequently Used)**:记录访问频率
|
||
2. **2Q (Two Queues)**:分为缓存队列和保留队列
|
||
3. **ARC (Adaptive Replacement Cache)**:结合 LRU 和 LFU
|
||
4. **LRU-K**:记录最近 K 次访问历史
|
||
|
||
### Q3: 缓存容量如何确定?
|
||
**答**:
|
||
考虑因素:
|
||
1. **内存限制**:系统可用内存大小
|
||
2. **访问模式**:数据访问频率和大小分布
|
||
3. **性能要求**:需要达到的响应时间
|
||
4. **命中率目标**:期望的缓存命中率
|
||
5. **业务特点**:数据的时效性和重要性
|
||
|
||
### Q4: 如何处理缓存并发问题?
|
||
**答**:
|
||
解决方案:
|
||
1. **使用线程安全容器**:如 `ConcurrentHashMap`
|
||
2. **添加同步锁**:方法或代码块同步
|
||
3. **使用读写锁**:提高并发性能
|
||
4. **不可变对象**:避免并发修改问题
|
||
|
||
```java
|
||
// 线程安全的 LRU 缓存
|
||
public class ThreadSafeLRUCache<K, V> {
|
||
private final LRUCacheImpl<K, V> cache;
|
||
private final ReadWriteLock lock = new ReentrantReadWriteLock();
|
||
|
||
public V get(K key) {
|
||
lock.readLock().lock();
|
||
try {
|
||
return cache.get(key);
|
||
} finally {
|
||
lock.readLock().unlock();
|
||
}
|
||
}
|
||
|
||
public void put(K key, V value) {
|
||
lock.writeLock().lock();
|
||
try {
|
||
cache.put(key, value);
|
||
} finally {
|
||
lock.writeLock().unlock();
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Q5: 如何处理缓存穿透、击穿、雪崩?
|
||
**答**:
|
||
**缓存穿透**:
|
||
- 查询不存在的数据
|
||
- 解决方案:布隆过滤器、空值缓存
|
||
|
||
**缓存击穿**:
|
||
- 大量请求同时查询过期热点数据
|
||
- 解决方案:互斥锁、永不过期
|
||
|
||
**缓存雪崩**:
|
||
- 大量缓存同时失效
|
||
- 解决方案:随机过期时间、集群部署 |