# 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 extends LinkedHashMap { private final int capacity; public LRUCache(int capacity) { super(capacity, 0.75f, true); this.capacity = capacity; } @Override protected boolean removeEldestEntry(Map.Entry eldest) { return size() > capacity; } // 测试用例 public static void main(String[] args) { LRUCache 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 key; V value; LRUCacheNode prev; LRUCacheNode next; public LRUCacheNode(K key, V value) { this.key = key; this.value = value; this.prev = null; this.next = null; } } public class LRUCacheImpl { private final int capacity; private final Map> cache; private final LRUCacheNode head; private final LRUCacheNode 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 node = cache.get(key); moveToHead(node); return node.value; } // 插入数据 public void put(K key, V value) { if (cache.containsKey(key)) { // 更新已有节点 LRUCacheNode node = cache.get(key); node.value = value; moveToHead(node); } else { // 创建新节点 LRUCacheNode newNode = new LRUCacheNode<>(key, value); cache.put(key, newNode); addToHead(newNode); // 淘汰策略 if (cache.size() > capacity) { LRUCacheNode last = removeTail(); cache.remove(last.key); } } } // 移除指定节点 public void remove(K key) { if (!cache.containsKey(key)) { return; } LRUCacheNode 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 node) { node.prev = head; node.next = head.next; head.next.prev = node; head.next = node; } // 辅助方法:移除节点 private void removeNode(LRUCacheNode node) { node.prev.next = node.next; node.next.prev = node.prev; } // 辅助方法:移动到头部 private void moveToHead(LRUCacheNode node) { removeNode(node); addToHead(node); } // 辅助方法:移除尾部节点 private LRUCacheNode removeTail() { LRUCacheNode last = tail.prev; removeNode(last); return last; } // 打印缓存内容 public void printCache() { LRUCacheNode 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 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 { private final int capacity; private final Map cache; private final Deque 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 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 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 cache; public LocalCache(int maxSize) { this.cache = new LRUCacheImpl<>(maxSize); } public T get(String key, Class 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 messageBuffer; private final Queue 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 { private final LRUCacheImpl 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: 如何处理缓存穿透、击穿、雪崩? **答**: **缓存穿透**: - 查询不存在的数据 - 解决方案:布隆过滤器、空值缓存 **缓存击穿**: - 大量请求同时查询过期热点数据 - 解决方案:互斥锁、永不过期 **缓存雪崩**: - 大量缓存同时失效 - 解决方案:随机过期时间、集群部署