面试题答案
一键面试1. 设计思路
- 多级缓存架构:采用至少两级缓存,如一级缓存(L1)使用
ConcurrentHashMap
,因为它支持高并发读写,适合快速响应常用数据请求;二级缓存(L2)使用WeakHashMap
,用于存储相对不那么频繁访问的数据,利用其弱引用特性在内存紧张时自动回收。 - 缓存策略:制定动态的缓存淘汰策略,如基于时间(Time - To - Live, TTL)和基于访问频率(Least Frequently Used, LFU)相结合的策略。对于L1缓存,优先使用LFU策略淘汰长时间未使用且访问频率低的数据;对于L2缓存,由于
WeakHashMap
本身的特性,无需额外的淘汰策略,但可结合TTL确保数据的时效性。
2. WeakHashMap的使用方式
- 初始化:在初始化二级缓存时,创建
WeakHashMap
实例,例如:
WeakHashMap<String, Object> weakCache = new WeakHashMap<>();
- 存储数据:将数据存入
WeakHashMap
时,键使用强引用,值使用弱引用。这样当值不再被其他地方强引用时,在内存不足时会被垃圾回收器回收。例如:
weakCache.put(key, new WeakReference<>(value));
- 读取数据:从
WeakHashMap
读取数据时,需要从WeakReference
中获取实际的值,并检查值是否已被回收。例如:
WeakReference<Object> ref = weakCache.get(key);
Object value = ref != null? ref.get() : null;
3. 数据结构设计
- 缓存数据结构:设计一个通用的缓存项类,例如:
class CacheEntry {
private Object value;
private long lastAccessTime;
private int accessCount;
public CacheEntry(Object value) {
this.value = value;
this.lastAccessTime = System.currentTimeMillis();
this.accessCount = 1;
}
// Getters and setters
public Object getValue() {
return value;
}
public void setValue(Object value) {
this.value = value;
}
public long getLastAccessTime() {
return lastAccessTime;
}
public void updateAccessTime() {
this.lastAccessTime = System.currentTimeMillis();
this.accessCount++;
}
public int getAccessCount() {
return accessCount;
}
}
- 一级缓存数据结构:
ConcurrentHashMap<String, CacheEntry>
,使用ConcurrentHashMap
存储键值对,其中值为CacheEntry
对象,方便记录访问时间和访问频率等信息。 - 二级缓存数据结构:
WeakHashMap<String, WeakReference<CacheEntry>>
,如上述WeakHashMap
的使用方式,存储二级缓存数据。
4. 相关算法实现
- 读取算法:
- 首先从L1缓存中查找数据。如果找到,更新
CacheEntry
的访问时间和访问频率,然后返回数据。 - 如果L1缓存未命中,则从L2缓存中查找。找到后,将数据移至L1缓存(如果L1缓存未满),并更新L1缓存中对应
CacheEntry
的信息,然后返回数据。如果L2缓存也未命中,则从数据源获取数据,存入L1缓存(如果L1缓存未满),并根据需要存入L2缓存。
- 首先从L1缓存中查找数据。如果找到,更新
public Object get(String key) {
CacheEntry entry = l1Cache.get(key);
if (entry != null) {
entry.updateAccessTime();
return entry.getValue();
}
WeakReference<CacheEntry> ref = l2Cache.get(key);
if (ref != null) {
CacheEntry l2Entry = ref.get();
if (l2Entry != null) {
l2Entry.updateAccessTime();
l1Cache.put(key, l2Entry);
return l2Entry.getValue();
}
}
// 从数据源获取数据
Object data = getDataFromSource(key);
if (data != null) {
CacheEntry newEntry = new CacheEntry(data);
l1Cache.put(key, newEntry);
if (l1Cache.size() > l1CacheCapacity) {
removeLeastFrequentlyUsedFromL1();
}
l2Cache.put(key, new WeakReference<>(newEntry));
}
return data;
}
- 写入算法:
- 直接将数据写入L1缓存。如果L1缓存已满,根据LFU策略淘汰一个
CacheEntry
,并将新数据写入。 - 同时,将数据写入L2缓存(如果需要)。
- 直接将数据写入L1缓存。如果L1缓存已满,根据LFU策略淘汰一个
public void put(String key, Object value) {
CacheEntry entry = new CacheEntry(value);
l1Cache.put(key, entry);
if (l1Cache.size() > l1CacheCapacity) {
removeLeastFrequentlyUsedFromL1();
}
l2Cache.put(key, new WeakReference<>(entry));
}
- LFU淘汰算法:在L1缓存满时,遍历
ConcurrentHashMap
,找到访问频率最低且最早访问的CacheEntry
进行淘汰。
private void removeLeastFrequentlyUsedFromL1() {
CacheEntry lfuEntry = null;
for (CacheEntry entry : l1Cache.values()) {
if (lfuEntry == null || entry.getAccessCount() < lfuEntry.getAccessCount() ||
(entry.getAccessCount() == lfuEntry.getAccessCount() && entry.getLastAccessTime() < lfuEntry.getLastAccessTime())) {
lfuEntry = entry;
}
}
if (lfuEntry != null) {
l1Cache.values().remove(lfuEntry);
l2Cache.values().removeIf(ref -> ref.get() == lfuEntry);
}
}
- TTL过期检查算法:可以通过一个定时任务(如
ScheduledExecutorService
)定期检查L1和L2缓存中的数据是否过期(根据CacheEntry
中的lastAccessTime
和设定的TTL),过期则删除。
ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {
long currentTime = System.currentTimeMillis();
l1Cache.entrySet().removeIf(entry -> currentTime - entry.getValue().getLastAccessTime() > ttl);
l2Cache.entrySet().removeIf(entry -> {
WeakReference<CacheEntry> ref = entry.getValue();
CacheEntry cacheEntry = ref.get();
return cacheEntry == null || currentTime - cacheEntry.getLastAccessTime() > ttl;
});
}, 0, 1, TimeUnit.MINUTES);
通过上述设计和算法实现,可以满足复杂缓存场景中高效读写、内存回收合理以及动态调整缓存策略的需求。