这是一个经典的 LRU(Least Recently Used) 变种题目。标准的 LRU 只考虑空间维度(达到 N 个),而你的需求增加了时间维度(N 小时过期)。
在 Java 中实现这个功能,最优雅的方式是结合 LinkedHashMap(处理空间淘汰)和 DelayQueue 或 定时清理任务(处理时间淘汰)。
核心设计思路
-
空间淘汰(LRU): 利用
LinkedHashMap的accessOrder模式。当调用get或put时,被访问的元素会移到链表尾部。重写removeEldestEntry方法,当 size > N 时自动删除表头(最久未访问)的元素。 -
时间淘汰(TTL): * 被动清理(推荐): 在每次
get时检查时间戳,如果超过 N 小时则删除并返回 null。-
主动清理: 使用一个后台线程(如
ScheduledExecutorService)定期扫描,或者配合DelayQueue精确清理。
-
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);
}
}
面试官可能会追问的细节:
-
为什么不直接用 Redis?
-
回答: Redis 确实支持
volatile-lru策略,非常适合这个场景。但在进程内缓存(如本地内存缓存)中,手写 LRU 可以减少网络开销。
-
-
你的实现是线程安全的吗?
-
回答: 上面的代码用了
synchronized简单处理。但在高并发下会有性能瓶颈。优化方案是使用ConcurrentHashMap结合自定义的双向链表,并对链表操作进行精细化锁控制(或者使用ReadWriteLock)。
-
-
如果数据量非常大,主动清理过期数据会卡顿吗?
-
回答: 如果
clearExpired扫描全表,确实会 O(n) 阻塞。可以采用 “分段清理” 或者 “懒惰删除”(即只有 get 到它时才检查过期,或者只在 put 时顺便查一下表头几个元素)。
-
总结:
这个实现结合了 LinkedHashMap 自带的 空间 LRU 和通过时间戳维护的 逻辑 TTL。在面试中,能说出 accessOrder 和重写 removeEldestEntry 是基本功,能考虑到时间维度的“懒删除”策略是加分项。