当前位置: 首页 > news >正文

Caffeine缓存

当然可以!以下是针对在项目中使用 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 的高并发性能主要依赖以下几点:

  1. 基于 **<font style="color:rgb(44, 44, 54);background-color:rgba(175, 184, 193, 0.2);">ConcurrentHashMap</font>** 实现存储
    • 利用其分段锁/CAS 机制,支持高并发读写。
  2. 无锁设计(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> 等高性能并发计数器记录命中率。
  3. 惰性清理(Lazy Expiration)
    • 不依赖定时线程轮询过期 key,而是在 读写操作时顺带清理 过期或淘汰项。
    • 同时后台有调度线程定期执行清理任务,避免内存泄漏。
  4. 异步加载与刷新
    • <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> 支持异步刷新,读操作不阻塞。
  5. 高效的淘汰算法(Window TinyLFU)
    • 淘汰决策快速,内存占用小,命中率高。

4. 什么是 Window TinyLFU?它解决了什么问题?

答:

Window TinyLFU 是 Caffeine 的核心淘汰算法,结合了两种策略:

  1. Admission Window(准入窗口)
    • 新 entry 先进入一个小型的 LRU 窗口
    • 只有被多次访问的 key 才会被“晋升”到主缓存区(Main Probabilistic Cache)。
  2. 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。

解决方案

  1. 空值缓存(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)。

  1. 布隆过滤器(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>**
触发时机 写入后达到时间,异步刷新 写入后达到时间,同步过期
读操作 仍可读旧值,后台刷新 读时发现过期,需重新加载
是否阻塞读 不阻塞
http://www.wxhsa.cn/company.asp?id=671

相关文章:

  • Spark面试题清单
  • RocketMQ知识点梳理
  • Tekla坐标定位插件源码
  • 记录 使用PsExec启动System权限的WPF 程序
  • std::map的基本用法
  • 力扣20题 有效的括号
  • 2025年9月10日学习笔记之keil软件仿真调试
  • MySQL的explain使用
  • 力扣19题 删除链表的倒数第N个结点
  • 基于LZO的无损数据压缩IP,高性能压缩速率32Gbps,适用于FPGAASIC
  • IDEA创建文件时如何自动生成头部文档注释(简单、实用)
  • 一文带你吃透Power Platform,开启低代码开发新世界
  • docker compose 启动 redis 服务
  • MBR引导的OS Bootloader遇到被bios无视引导(自动重启)的解决办法
  • #java作业
  • 【Qt6】qt6下载地址
  • QOJ1838 Intellectual Implementation 题解
  • OpenSSH漏洞修复
  • 力扣15题三数之和
  • some plan
  • 利用废弃硬件中的零日漏洞:从Netgear路由器到BitDefender盒子的攻击链分析
  • ECT-OS-JiuHuaShan框架:自然规律的具象化智能体(附《易经》类比解析)
  • 力扣第5题最长回文子串
  • 用 Python 和 PaddleOCR 进行验证码识别
  • TASK 1 训练一个网络识别手写数字
  • 复杂背景验证码的识别思路与图像处理方法
  • Symfony学习笔记 - The Symfony Framework Best Practices
  • 大学军训
  • Vue Day3【综合案例2】vue小兔鲜儿
  • Java 基础知识解析