Files
interview/questions/alg-lru.md
yasinshaw 71e3497bfd feat: add comprehensive system design interview questions
- 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>
2026-02-28 23:43:36 +08:00

524 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# LRU 缓存实现
## 数据结构原理
### 什么是 LRU 缓存?
LRULeast 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: 如何处理缓存穿透、击穿、雪崩?
**答**
**缓存穿透**
- 查询不存在的数据
- 解决方案:布隆过滤器、空值缓存
**缓存击穿**
- 大量请求同时查询过期热点数据
- 解决方案:互斥锁、永不过期
**缓存雪崩**
- 大量缓存同时失效
- 解决方案:随机过期时间、集群部署