当然可以!以下是针对在项目中使用 Caffeine 缓存这一主题,作为面试官可能提出的一系列 面试题(含初级到高级),并附上 详细解答,帮助候选人深入理解 Caffeine 的实际应用与底层原理。
Caffeine 缓存面试题(含答案)
1. 你在项目中为什么选择 Caffeine 而不是 Redis 或 Guava Cache?
答:
这是一个考察技术选型能力的问题。可以从以下几个角度回答:
- 与 Redis 对比:
- Redis 是远程缓存,每次访问需要网络开销,延迟较高(毫秒级)。
- Caffeine 是本地缓存,数据在 JVM 内存中,访问速度极快(纳秒级),适合高频读、低延迟场景。
- 适用于:热点数据缓存、配置缓存、防缓存穿透的二级缓存等。
- 我们采用 Caffeine + Redis 的多级缓存架构,Caffeine 作为一级缓存,减少对 Redis 的压力。
- 与 Guava Cache 对比:
- Caffeine 是 Guava Cache 的增强版,性能更高,淘汰算法更先进。
- Guava 使用的是简单的 LRU 或基于权重的淘汰,而 Caffeine 使用 Window TinyLFU,命中率更高。
- Caffeine 提供了更好的统计支持(
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">recordStats()</font>
)、异步刷新、更灵活的过期策略。
✅ 结论:选择 Caffeine 是为了降低延迟、提高吞吐量、提升缓存命中率,特别是在高并发读的场景下优势明显。
2. Caffeine 的缓存淘汰策略有哪些?默认是哪种?
答:
Caffeine 支持以下几种淘汰策略:
策略 | 说明 |
---|---|
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">maximumSize(long)</font> |
按缓存条目数量淘汰(最常用) |
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">maximumWeight(long)</font> |
按权重淘汰,需配合 <font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">weigher</font> 使用,适用于对象大小差异大的场景 |
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">expireAfterWrite(long, TimeUnit)</font> |
写入后过期 |
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">expireAfterAccess(long, TimeUnit)</font> |
最后一次访问后过期 |
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">expireAfter(Expiry)</font> |
自定义过期策略(如动态 TTL) |
⚠️ 注意:Caffeine 不支持同时设置 **<font style="color:rgb(143, 145, 168);background-color:rgba(175, 184, 193, 0.2);">expireAfterWrite</font>**
和 **<font style="color:rgb(143, 145, 168);background-color:rgba(175, 184, 193, 0.2);">expireAfterAccess</font>**
,只能选其一。
默认淘汰策略:
Caffeine 没有默认淘汰策略,必须显式设置 <font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">maximumSize</font>
或 <font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">maximumWeight</font>
,否则缓存将无限增长,可能导致 OOM。
3. Caffeine 是如何实现高并发性能的?
答:
Caffeine 的高并发性能主要依赖以下几点:
- 基于
**<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">ConcurrentHashMap</font>**
实现存储:- 利用其分段锁/CAS 机制,支持高并发读写。
- 无锁设计(Lock-free):
- 在统计、淘汰、刷新等操作中尽量避免锁竞争。
- 使用
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">LongAdder</font>
、<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">Striped64</font>
等高性能并发计数器记录命中率。
- 惰性清理(Lazy Expiration):
- 不依赖定时线程轮询过期 key,而是在 读写操作时顺带清理 过期或淘汰项。
- 同时后台有调度线程定期执行清理任务,避免内存泄漏。
- 异步加载与刷新:
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">get(key, k -> load(k))</font>
支持并发访问同一个 key 时只执行一次加载(类似“缓存击穿”防护)。<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">refreshAfterWrite</font>
支持异步刷新,读操作不阻塞。
- 高效的淘汰算法(Window TinyLFU):
- 淘汰决策快速,内存占用小,命中率高。
4. 什么是 Window TinyLFU?它解决了什么问题?
答:
Window TinyLFU 是 Caffeine 的核心淘汰算法,结合了两种策略:
- Admission Window(准入窗口):
- 新 entry 先进入一个小型的 LRU 窗口。
- 只有被多次访问的 key 才会被“晋升”到主缓存区(Main Probabilistic Cache)。
- TinyLFU(频率过滤器):
- 使用 Count-Min Sketch 数据结构近似统计访问频率。
- 当缓存满时,优先淘汰频率低的 entry。
Window TinyLFU 并不是一个单一算法,而是 三层架构 的组合:
深色版本+---------------------+
| Admission Window | ← 新 key 先进入这里(小型 LRU)
+---------------------+↓(被频繁访问则晋升)
+---------------------+
| Main Probabilistic| ← 主缓存区,由 TinyLFU 管理
| Cache | 使用 Count-Min Sketch 统计频率
+---------------------+↓(冷数据被淘汰)
+---------------------+
| Victim Cache | ← 可选:防止误淘汰的“受害者缓存”
+---------------------+
准入机制 + 概率频率统计
✅ 解决的问题:
- 传统 LFU 的问题:对“突发一次性访问”敏感,容易把只访问一次的数据长期留在缓存中。
- LRU 的问题:对“周期性但不频繁”的访问不友好。
Window TinyLFU 通过“准入机制”过滤掉一次性访问,只让真正“热点”的数据进入主缓存,从而显著提升命中率。
5. 如何防止缓存击穿?Caffeine 本身是否支持?
答:
缓存击穿:某个 key 失效的瞬间,大量并发请求打到数据库,导致 DB 压力骤增。
Caffeine 通过 **<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">get(key, mappingFunction)</font>**
自动支持防止缓存击穿:
java深色版本cache.get("user:1001", key -> loadFromDB(key));
- 当多个线程同时请求同一个不存在的 key 时,Caffeine 会保证 只有一个线程执行****
**<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">loadFromDB</font>**
,其他线程等待并共享结果。 - 这是基于
<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">ConcurrentHashMap.computeIfAbsent</font>
的机制实现的。
✅ 等效于“互斥锁”方案,但无需手动加锁。
建议:结合 <font style="color:rgb(143, 145, 168);background-color:rgba(175, 184, 193, 0.2);">refreshAfterWrite</font>
使用,让缓存“提前刷新”,避免集中失效。
6. 如何实现缓存预热?在项目中你是怎么做的?
答:
缓存预热:在系统启动或低峰期,提前将热点数据加载到缓存中,避免冷启动时大量请求击穿缓存。
在项目中可以这样做:
java深色版本@PostConstruct
public void warmUpCache() {List<User> hotUsers = userService.getTopNHotUsers(100);Map<String, String> preload = hotUsers.stream().collect(Collectors.toMap(u -> "user:" + u.getId(),u -> u.getName()));cache.putAll(preload);
}
或者使用定时任务定期预热:
java深色版本@Scheduled(fixedDelay = 30, timeUnit = TimeUnit.MINUTES)
public void refreshHotCache() {// 重新加载热点数据
}
✅ 关键点:
- 预热数据要有依据(如访问日志、排行榜)。
- 避免一次性加载过多导致 OOM。
- 可结合监控系统动态调整预热策略。
7. Caffeine 支持缓存穿透吗?如何解决?
⚠️**** 注意:Caffeine 本身不直接解决缓存穿透,但可以配合策略解决。
缓存穿透:查询一个永远不存在的数据,导致每次请求都打到 DB。
解决方案:
- 空值缓存(Null Value Caching):
java深色版本String value = cache.get(key, k -> {String dbValue = userDao.findById(k);if (dbValue == null) {// 缓存空值,TTL 短一些,如 5 分钟cache.put(k, "NULL"); // 使用特殊标记return null;}return dbValue;
});
注意:<font style="color:rgb(143, 145, 168);background-color:rgba(175, 184, 193, 0.2);">"NULL"</font>
是占位符,避免缓存 <font style="color:rgb(143, 145, 168);background-color:rgba(175, 184, 193, 0.2);">null</font>
(Caffeine 不支持缓存 null)。
- 布隆过滤器(Bloom Filter)前置过滤:
- 在 Caffeine 外层加一层 Bloom Filter,判断 key 是否可能存在。
- 不存在则直接返回,不查缓存也不查 DB。
✅ 推荐:布隆过滤器 + Caffeine 空值缓存 双重防护。
8. 如何监控 Caffeine 缓存的命中率?
答:
Caffeine 提供了内置的统计功能:
java深色版本Cache<String, String> cache = Caffeine.newBuilder().maximumSize(1000).recordStats() // 启用统计.build();// 获取统计信息
CacheStats stats = cache.stats();
System.out.println("命中率: " + stats.hitRate());
System.out.println("请求总数: " + stats.requestCount());
System.out.println("命中数: " + stats.hitCount());
System.out.println("未命中数: " + stats.missCount());
System.out.println("淘汰数: " + stats.evictionCount());
✅ 在项目中:
- 将这些指标暴露给 Prometheus + Grafana 做监控大盘。
- 设置告警:命中率低于 90% 时通知,排查缓存设计问题。
9. Caffeine 的 refreshAfterWrite 和 expireAfterWrite 有什么区别?
特性 | **<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">refreshAfterWrite</font>** |
**<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">expireAfterWrite</font>** |
---|---|---|
触发时机 | 写入后达到时间,异步刷新 | 写入后达到时间,同步过期 |
读操作 | 仍可读旧值,后台刷新 | 读时发现过期,需重新加载 |
是否阻塞读 | ❌ 不阻塞 |