实现一个 LRU 缓存,当缓存数据达到 N 之后需要淘汰掉最近最少使用的数据。N 小时之内没有被访问的数据也需要淘汰掉

7次阅读
没有评论

 

这是一个经典的 LRU(Least Recently Used) 变种题目。标准的 LRU 只考虑空间维度(达到 N 个),而你的需求增加了时间维度(N 小时过期)。

在 Java 中实现这个功能,最优雅的方式是结合 LinkedHashMap(处理空间淘汰)和 DelayQueue 或 定时清理任务(处理时间淘汰)。

核心设计思路

  1. 空间淘汰(LRU): 利用 LinkedHashMapaccessOrder 模式。当调用 getput 时,被访问的元素会移到链表尾部。重写 removeEldestEntry 方法,当 size > N 时自动删除表头(最久未访问)的元素。

  2. 时间淘汰(TTL): * 被动清理(推荐): 在每次 get 时检查时间戳,如果超过 N 小时则删除并返回 null。

    • 主动清理: 使用一个后台线程(如 ScheduledExecutorService)定期扫描,或者配合 DelayQueue 精确清理。


Java 代码实现(双重淘汰机制)

Java

import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class SmartLRUCache<K, V> {

    private final int capacity;
    private final long expireTimeMs; // 过期时间(毫秒)

    // 内部类,保存值和最后访问时间戳
    private static class Node<V> {
        V value;
        long lastAccessTime;

        Node(V value) {
            this.value = value;
            this.lastAccessTime = System.currentTimeMillis();
        }
    }

    private final LinkedHashMap<K, Node<V>> cache;

    public SmartLRUCache(int capacity, int expireHours) {
        this.capacity = capacity;
        this.expireTimeMs = expireHours * 60 * 60 * 1000L;

        // accessOrder 为 true,开启 LRU 模式
        this.cache = new LinkedHashMap<K, Node<V>>(capacity, 0.75f, true) {
            @Override
            protected boolean removeEldestEntry(Map.Entry<K, Node<V>> eldest) {
                // 空间维度淘汰:超过容量就删掉最近最少使用的
                return size() > SmartLRUCache.this.capacity;
            }
        };
    }

    // 同步方法保证线程安全(实际高并发可用 ConcurrentHashMap + 手写双向链表)
    public synchronized void put(K key, V value) {
        cache.put(key, new Node<>(value));
    }

    public synchronized V get(K key) {
        Node<V> node = cache.get(key);
        if (node == null) {
            return null;
        }

        // 时间维度淘汰:N 小时内没访问
        long now = System.currentTimeMillis();
        if (now - node.lastAccessTime > expireTimeMs) {
            cache.remove(key); // 已过期
            return null;
        }

        // 更新最后访问时间并返回
        node.lastAccessTime = now;
        return node.value;
    }

    // 可选:后台清理线程调用的方法,防止内存中堆积过期的死数据
    public synchronized void clearExpired() {
        long now = System.currentTimeMillis();
        cache.entrySet().removeIf(entry -> (now - entry.getValue().lastAccessTime) > expireTimeMs);
    }
}

面试官可能会追问的细节:

  1. 为什么不直接用 Redis?

    • 回答: Redis 确实支持 volatile-lru 策略,非常适合这个场景。但在进程内缓存(如本地内存缓存)中,手写 LRU 可以减少网络开销。

  2. 你的实现是线程安全的吗?

    • 回答: 上面的代码用了 synchronized 简单处理。但在高并发下会有性能瓶颈。优化方案是使用 ConcurrentHashMap 结合自定义的双向链表,并对链表操作进行精细化锁控制(或者使用 ReadWriteLock)。

  3. 如果数据量非常大,主动清理过期数据会卡顿吗?

    • 回答: 如果 clearExpired 扫描全表,确实会 O(n) 阻塞。可以采用 “分段清理” 或者 “懒惰删除”(即只有 get 到它时才检查过期,或者只在 put 时顺便查一下表头几个元素)。

总结:

这个实现结合了 LinkedHashMap 自带的 空间 LRU 和通过时间戳维护的 逻辑 TTL。在面试中,能说出 accessOrder 和重写 removeEldestEntry 是基本功,能考虑到时间维度的“懒删除”策略是加分项。

正文完
 0
bdspAdmin
版权声明:本站原创文章,由 bdspAdmin 于2026-04-08发表,共计2120字。
转载说明:除特殊说明外本站文章皆由CC-4.0协议发布,转载请注明出处。
评论(没有评论)