Files
interview/questions/03-缓存/LRU缓存实现.md
yasinshaw 0e46a367c4 refactor: rename files to Chinese and organize by category
Organized 50 interview questions into 12 categories:
- 01-分布式系统 (9 files): 分布式事务, 分布式锁, 一致性哈希, CAP理论, etc.
- 02-数据库 (2 files): MySQL索引优化, MyBatis核心原理
- 03-缓存 (5 files): Redis数据结构, 缓存问题, LRU算法, etc.
- 04-消息队列 (1 file): RocketMQ/Kafka
- 05-并发编程 (4 files): 线程池, 设计模式, 限流策略, etc.
- 06-JVM (1 file): JVM和垃圾回收
- 07-系统设计 (8 files): 秒杀系统, 短链接, IM, Feed流, etc.
- 08-算法与数据结构 (4 files): B+树, 红黑树, 跳表, 时间轮
- 09-网络与安全 (3 files): TCP/IP, 加密安全, 性能优化
- 10-中间件 (4 files): Spring Boot, Nacos, Dubbo, Nginx
- 11-运维 (4 files): Kubernetes, CI/CD, Docker, 可观测性
- 12-面试技巧 (1 file): 面试技巧和职业规划

All files renamed to Chinese for better accessibility and
organized into categorized folders for easier navigation.

Generated with [Claude Code](https://claude.com/claude-code)
via [Happy](https://happy.engineering)

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Happy <yesreply@happy.engineering>
2026-03-01 00:10:53 +08:00

14 KiB
Raw Permalink Blame History

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推荐

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);
    }
}

方法二:手写实现(面试重点)

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

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 结果
// 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 一级/二级缓存
  • 查询结果缓存:复杂查询结果的缓存
// 数据库缓存示例
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
// 本地缓存示例
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. 消息队列缓冲

  • 消息去重:防止重复处理消息
  • 请求合并:合并短时间内多个相同请求
// 消息队列缓冲示例
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. 不可变对象:避免并发修改问题
// 线程安全的 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: 如何处理缓存穿透、击穿、雪崩?

缓存穿透

  • 查询不存在的数据
  • 解决方案:布隆过滤器、空值缓存

缓存击穿

  • 大量请求同时查询过期热点数据
  • 解决方案:互斥锁、永不过期

缓存雪崩

  • 大量缓存同时失效
  • 解决方案:随机过期时间、集群部署