📚 高级篇 04. 多级缓存 - JVM 进程缓存之 Caffeine 初识
一、 核心认知:为什么需要 JVM 进程缓存?
在引入 Caffeine 之前,我们先理清一个架构常识:有了 Redis,为什么还要搞本地缓存?
-
突破网络 I/O 的物理极限:
Tomcat 去读 Redis,需要经过网卡序列化、网络传输、Redis 处理、网络传回、反序列化。哪怕在同一个内网,这个过程通常也需要 1~5 毫秒 (ms) 。
而读取 JVM 进程内的缓存,直接在内存地址中寻址,没有任何网络开销,速度是 纳秒级 (ns) !两者相差成千上万倍。
-
终结 Redis 热点 Key 灾难:
如果某个明星宣布结婚,相关微博的访问量瞬间暴涨。如果全靠 Redis 扛,存放该微博数据的单一 Redis 节点会被瞬间打瘫。如果把这条数据缓存在每个 Tomcat 实例的 JVM 内存里,就能完美实现流量的“去中心化分流”。
🌟 什么是 Caffeine?
Caffeine 是一个基于 Java 8 的高性能本地缓存库。它极度压榨了 Java 内存的读取性能,并且提供了一种近乎完美的缓存淘汰算法(W-TinyLFU),号称是目前 Java 领域命中率最高、性能最强的本地缓存之王。
二、 核心机制:缓存驱逐策略 (Eviction)
由于 JVM 堆内存(Heap)极其宝贵(通常就几个 G),我们绝对不能把海量的商品数据无限量地塞进 Caffeine 里,否则一定会引发恐怖的 OOM(内存溢出) 导致系统崩溃。
因此,Caffeine 提供了三种核心的“缓存驱逐(淘汰)策略”,告诉框架什么时候该把老数据扔掉:
1. 基于容量驱逐 (maximumSize)
这是最简单粗暴、也是最常用的兜底策略。
- 规则: 指定缓存里最多能放多少个对象。当数量快达到上限时,Caffeine 会根据内部算法,把最不常用的数据踢出去。
- 场景: “我只给本地缓存留了 10000 个位置,多出来的就给我滚。”
2. 基于时间驱逐 (expireAfterWrite / expireAfterAccess)
缓存的数据不能永远有效,否则后台改了商品价格,前端看到的永远是老价格。
expireAfterWrite(写后过期): 数据被存入缓存后,经过指定时间就失效。(强烈推荐! 性能极高,时间一到绝对过期)。expireAfterAccess(读写后过期): 数据被**访问(读或写)**后,经过指定时间失效。如果一个热点数据一直被读,它就永远不过期。(适合那些“只要有人看就一直留着”的场景,但维护成本稍高)。
3. 基于引用驱逐 (Reference)
- 规则: 利用 Java 的软引用 (Soft) 和弱引用 (Weak) 机制,把缓存对象的存活权交给 JVM 的垃圾回收器 (GC)。当 JVM 内存不足时,自动回收这些缓存。
- 缺点: 极度不可控!线上排查问题简直是噩梦,企业级开发中强烈不推荐使用!
三、 极速上手:Caffeine API 核心实操
让我们用原生 Java 代码来感受一下 Caffeine 的优雅。
第 1 步:引入依赖
XML
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
第 2 步:基础用法演示 (面试常考手撕)
Java
public class CaffeineDemo {
public static void main(String[] args) {
// 1. 构建 Caffeine 缓存对象 (配置驱逐策略)
Cache<String, String> cache = Caffeine.newBuilder()
.maximumSize(10_000) // 最大容量 1万个
.expireAfterWrite(Duration.ofSeconds(60)) // 写入后 60 秒过期
.build();
// 2. 存入数据
cache.put("user:1001", "张三");
// 3. 读取数据 (如果不存,返回 null)
String user = cache.getIfPresent("user:1001");
System.out.println(user); // 输出: 张三
// 🌟 4. 【企业级王炸用法】:优雅的 get 方法
// 逻辑:去缓存里找 "user:1002"。如果有,直接返回;
// 如果没有,就自动执行后面的 Lambda 表达式 (去数据库查),并把查到的结果自动存入缓存,然后返回!
String user2 = cache.get("user:1002", key -> {
System.out.println("缓存未命中,开始查询数据库...");
return "李四(从数据库查出)"; // 模拟数据库查询结果
});
System.out.println(user2);
}
}
💡 亮点解析: 第 4 步的
cache.get(key, function)方法,完美解决了一个痛点:以往我们写缓存逻辑,总是要写一堆if (cacheData == null) { 查库; 存缓存; } return cacheData;的臃肿代码。Caffeine 一行代码全部搞定,极其优雅!
四、 大厂高频拷问:Caffeine 为什么那么强?(W-TinyLFU)
在秋招大厂面试中,如果你在简历里写了“使用 Caffeine 优化多级缓存”,面试官一定会问: “Caffeine 凭什么取代 Guava Cache?它的核心优势是什么?”
💡 满分防身话术:
“Caffeine 最核心的优势在于它的超高命中率,这得益于它独创的 W-TinyLFU 缓存淘汰算法。
传统的淘汰算法有两个痛点:
LRU(最近最少使用): 很容易被偶发的“全量扫描”或者“突发冷门流量”把真正的热点数据洗掉(缓存污染)。
LFU(最不经常使用): 能够抵御偶发流量,但它需要维护所有数据的访问频率,极度消耗内存;而且如果一个数据曾经是热点,后来过气了(比如旧新闻),它因为历史频率极高,很难被淘汰出局(历史包袱)。
Caffeine 的 W-TinyLFU 完美融合了这两者的优点:
- 它使用了一种类似布隆过滤器的
Count-Min Sketch数据结构来记录访问频率,把频率统计的内存占用压缩到了极致(Tiny)。- 它引入了基于时间的衰减机制,解决了 LFU 的历史包袱问题,让老热点数据能够顺利退休。
- 它引入了一个前端的 Window Cache(W),专门用来接纳新进入的数据,确保那些“刚刚爆发的新热点”在还没积攒够访问频率之前,不会被立刻踢出缓存。
凭借这个算法,Caffeine 在有限的内存下,做到了业界天花板级别的命中率。”
学习总结
这一节,你成功解锁了 Java 性能最霸道的进程内缓存框架——Caffeine。
它不依赖网络,极致压榨了 CPU 和内存的性能,是我们多级缓存架构中最靠近应用层、速度最快的一道防线。