📚 高级篇 05. 多级缓存 - JVM 进程缓存之实现业务缓存
一、 架构图解:JVM 缓存拦截流程
在改造代码之前,我们先明确一下加入 Caffeine 之后的请求处理路径:
-
前端发来查询商品详情的请求(例如查询
id = 10001的商品)。 -
请求到达 Tomcat 中的
ItemController,随后进入ItemService。 -
ItemService首先去查 Caffeine 缓存。- 如果命中 (Cache Hit): 直接返回内存中的对象数据(耗时不到 1 毫秒),结束!
- 如果未命中 (Cache Miss): 去 MySQL 数据库查询,查到数据后,将其写入 Caffeine 缓存,然后再返回给前端。
二、 实战改造:注入 Caffeine Bean
在 Spring Boot 项目中,为了统一管理缓存的配置(比如商品缓存的容量、库存缓存的容量可能不同),我们通常会创建一个专门的配置类来定义 Cache 对象,并将它们注册为 Spring 的 Bean。
第 1 步:引入依赖
确保你的 pom.xml 中已经有了 Caffeine 的依赖:
XML
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
第 2 步:编写配置类 (CaffeineConfig.java)
由于在真实的电商系统中,商品的基本信息(标题、图片)和商品的库存信息 变化频率完全不同(库存几乎每秒都在变),我们强烈建议将它们分管在两个不同的缓存实例中。
Java
kotlin
package com.heima.item.config;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.heima.item.pojo.Item;
import com.heima.item.pojo.ItemStock;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CaffeineConfig {
/**
* 商品基本信息缓存 (变化频率低)
*/
@Bean
public Cache<Long, Item> itemCache() {
return Caffeine.newBuilder()
.initialCapacity(100) // 初始容量
.maximumSize(10_000) // 最大容量,防止 OOM
// 可以配置基于大小或时间的驱逐策略
.build();
}
/**
* 商品库存信息缓存 (变化频率极高)
*/
@Bean
public Cache<Long, ItemStock> itemStockCache() {
return Caffeine.newBuilder()
.initialCapacity(100)
.maximumSize(10_000)
.build();
}
}
三、 实战改造:业务层优雅集成
配置好了 Bean,接下来我们在 ItemService 中注入这些 Cache,并运用上一节学到的终极优雅写法 get(key, function)。
第 3 步:改造 ItemService (核心逻辑)
打开你的商品查询业务类,进行如下改造:
Java
kotlin
package com.heima.item.service.impl;
import com.github.benmanes.caffeine.cache.Cache;
import com.heima.item.mapper.ItemMapper;
import com.heima.item.pojo.Item;
import com.heima.item.service.IItemService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ItemServiceImpl implements IItemService {
@Autowired
private ItemMapper itemMapper;
// 1. 注入我们在 Config 中配置的商品缓存 Bean
@Autowired
private Cache<Long, Item> itemCache;
@Override
public Item queryItemById(Long id) {
// 2. 极其优雅的缓存处理逻辑 (一行代码搞定查缓存 + 查库 + 存缓存)
// 逻辑:用 id 去 itemCache 里找。
// 找不到?那就自动执行后面的 Lambda 表达式 (去数据库查 selectById(id)),然后存入缓存并返回。
return itemCache.get(id, key -> {
System.out.println("❌ 本地缓存未命中,正在查询 MySQL 数据库,id: " + key);
return itemMapper.selectById(key);
});
}
}
(库存逻辑也是同理,注入 itemStockCache 并在查询库存的方法中使用即可。)
四、 见证奇迹:测试与验证
改造完成后,重启你的 Spring Boot 项目。
-
第一次请求前端页面(查询 id=10001 的商品):
你会发现控制台打印了:
❌ 本地缓存未命中,正在查询 MySQL 数据库,id: 10001,并且看到了一条打印出的 SQL 语句。这说明第一次请求穿透到了数据库。 -
疯狂刷新页面(第二次、第三次、第一万次):
你会发现无论你按多少次 F5,控制台再也没有任何 SQL 语句打印出来!
请求响应时间从原来的几十毫秒,瞬间骤降到了几毫秒甚至不到 1 毫秒。
因为从第二次开始,所有的流量全被 Tomcat JVM 内存里的 Caffeine 拦截并光速返回了。数据库此时稳如泰山!
五、 大厂高频拷问:JVM 缓存的致命缺陷是什么?
在面试中,千万不要把 JVM 缓存吹得完美无缺。你需要主动抛出它的致命缺陷,展现你的全局架构观:
🗣️ 面试官发难: "既然 Caffeine 本地缓存这么快,我们以后所有项目只用它不就行了,为什么还要大费周章去搭 Redis 集群?"
💡 你的满分破局解析:
"本地缓存虽然速度达到了极致的纳秒级,但它有两个致命的缺陷,决定了它只能作为多级缓存中的前置防线,而不能完全替代 Redis:
- 内存容量极度受限: JVM 的堆内存通常配置只有几个 G 到十几个 G,还要留给业务对象和垃圾回收。把海量的业务数据全塞在 JVM 里一定会触发频繁的 Full GC,甚至 OOM 导致服务器宕机。而 Redis 分片集群可以无限横向扩容到 TB 级别。
- 极其头疼的数据一致性问题 (集群数据漂移): 在微服务架构下,商品服务通常会部署多个实例(例如 Tomcat 1、Tomcat 2)。如果后台修改了商品价格,通常只会清除其中一台 Tomcat 的本地缓存,或者请求还没来得及同步。此时,用户刷新页面,请求被网关随机路由到 Tomcat 1(看到新价格)和 Tomcat 2(看到旧价格),数据出现了严重的幻读现象!
因此,本地缓存只适合存放极度热点且容量较小的数据。对于海量数据,我们依然需要一个统一的、全局可见的分布式缓存(Redis)来作为数据共享的兜底底座。"
学习总结
这节课,你的 Spring Boot 已经进化出了极速响应的能力。Caffeine 就像是给 Tomcat 穿上了一件防弹衣,完美解决了 Redis 热点 Key 可能导致的单节点雪崩问题。