Spring Boot 缓存实战Chapter06项目:从入门到整合 Ehcache 与 Redis
最近完成了 Spring Boot 整合缓存的学习,涵盖了声明式缓存注解、Ehcache 本地缓存、Redis 分布式缓存三大模块。本文将记录学习过程中的核心知识点、踩坑经历,以及该项目能否写入简历的分析。
一、为什么需要缓存?
企业级应用中,数据库的 I/O 操作往往是性能瓶颈。缓存作为一种介于应用程序与数据库之间的临时数据存储机制,可以有效减少磁盘 I/O 次数,大幅提升系统响应速度。
核心思想:用空间换时间。
二、Spring Boot 默认缓存管理
2.1 核心注解
| 注解 | 作用 | 适用场景 |
|---|---|---|
@EnableCaching |
开启缓存支持 | 启动类上添加 |
@Cacheable |
先查缓存,没有再执行方法并缓存结果 | 查询方法 |
@CachePut |
先执行方法,再将结果更新到缓存 | 更新方法 |
@CacheEvict |
清除缓存 | 删除方法 |
@Caching |
组合多个缓存注解 | 复杂缓存规则 |
@CacheConfig |
类级别统一配置缓存公共属性 | Service 类 |
2.2 执行流程
用户请求 → @Cacheable 拦截
↓
检查缓存中是否有对应 key 的数据
↓
有 → 直接返回缓存数据(不执行方法)
↓
无 → 执行方法 → 存入缓存 → 返回结果
2.3 常用属性
@Cacheable(
value = "book", // 缓存名称
key = "#id", // SpEL 表达式指定 key
condition = "#id > 0", // 满足条件才缓存
unless = "#result == null" // 返回结果为 null 时不缓存
)
三、整合 Ehcache 本地缓存
3.1 Ehcache 特点
-
快速轻量,配置简单
-
支持堆内存、堆外内存、磁盘、集群多层存储
-
Hibernate 默认缓存提供者
3.2 核心配置(ehcache.xml)
<?xml version="1.0" encoding="UTF-8"?>
<ehcache updateCheck="false">
<diskStore path="D:/Spring Boot Application/GBg"/>
<cache name="book"
eternal="true"
diskPersistent="true"
maxElementsInMemory="1000"
overflowToDisk="false"
memoryStoreEvictionPolicy="LRU">
<bootstrapCacheLoaderFactory
class="net.sf.ehcache.store.DiskStoreBootstrapCacheLoaderFactory"
properties="bootstrapAsynchronously=true"/>
</cache>
</ehcache>
3.3 关键配置说明
| 属性 | 说明 |
|---|---|
diskStore path |
磁盘持久化路径 |
diskPersistent |
开启磁盘持久化(重启后缓存不丢失) |
maxElementsInMemory |
内存中最大缓存数量 |
memoryStoreEvictionPolicy |
淘汰策略(LRU、LFU、FIFO) |
3.4 实体类要求
使用 Ehcache 磁盘持久化时,实体类必须实现 Serializable 接口:
public class Book implements Serializable {
private static final long serialVersionUID = 1L;
// ...
}
四、整合 Redis 分布式缓存
4.1 为什么用 Redis?
-
支持丰富的数据结构(String、List、Set、Hash、ZSet)
-
分布式部署,多应用共享缓存
-
高性能,支持持久化
4.2 配置(application.yml)
spring:
redis:
host: localhost
port: 6379
password: cems
database: 2 # 使用 db2,避免与旧数据冲突
cache:
type: redis
redis:
use-key-prefix: true
key-prefix: book_ # key 前缀
cache-null-values: false # 不缓存 null 值
time-to-live: 400s # 过期时间
4.3 缓存切换
Spring Boot 的缓存抽象层允许一行配置切换底层缓存技术:
# 使用 Redis
cache:
type: redis
# 使用 Ehcache
cache:
type: ehcache
ehcache:
config: classpath:ehcache.xml
五、踩坑记录与解决方案
坑 1:Redis 连接失败 —— NOAUTH Authentication required
现象:
io.lettuce.core.RedisCommandExecutionException: NOAUTH Authentication required.
原因:Redis 设置了密码,但配置文件中未添加。
解决:
spring:
redis:
password: 你的密码
坑 2:缓存不生效
现象:每次请求都执行 SQL,缓存没起作用。
原因:忘记在启动类添加 @EnableCaching。
解决:
@SpringBootApplication
@EnableCaching // ← 必须添加
public class Chapter06Application {
public static void main(String[] args) {
SpringApplication.run(Chapter06Application.class, args);
}
}
坑 3:Ehcache 磁盘持久化失效
现象:重启项目后,缓存丢失,再次查询执行 SQL。
原因:
-
ehcache.xml中未配置diskPersistent="true" -
未指定
diskStore path -
实体类未实现
Serializable
解决:确保以上三点都满足。
坑 4:同时存在 Redis 和 Ehcache 依赖时,项目用哪个?
规则:
-
有
cache.type配置 → 听你的 -
无
cache.type配置 → 按优先级(Ehcache > Redis)
建议:显式指定 cache.type,避免歧义。
六、该项目能写入简历吗?
6.1 作为实习项目 —— ✅ 完全合适
项目名称:Spring Boot 缓存系统实践 技术栈:Spring Boot + Redis + Ehcache + MySQL + JPA 核心工作: 1. 使用 Spring Cache 抽象层整合 Ehcache 和 Redis 两种缓存技术 2. 掌握 @Cacheable、@CachePut、@CacheEvict 等缓存注解的使用 3. 实现 Ehcache 磁盘持久化,验证重启后缓存恢复 4. 通过 RESP.app 观察 Redis 缓存数据结构 5. 对比本地缓存与分布式缓存的适用场景 项目收获: - 理解缓存的核心思想:空间换时间 - 掌握 Spring Boot 自动配置原理 - 学会根据业务场景选择合适的缓存策略
6.2 简历中的技术占比
| 技术 | 核心程度 | 说明 |
|---|---|---|
| Spring Boot | ⭐⭐⭐⭐⭐ | 项目基础框架 |
| JPA/Hibernate | ⭐⭐⭐⭐ | 数据持久化 |
| MySQL | ⭐⭐⭐⭐ | 数据库 |
| Spring Cache | ⭐⭐⭐⭐⭐ | 缓存抽象层 |
| Redis | ⭐⭐⭐⭐ | 分布式缓存 |
| Ehcache | ⭐⭐⭐ | 本地缓存 |
6.3 需要完善的地方(进阶方向)
| 完善项 | 难度 | 说明 |
|---|---|---|
| 多级缓存 | ⭐⭐⭐ | L1 本地 + L2 分布式,提升性能 |
| 缓存穿透解决 | ⭐⭐⭐ | 布隆过滤器 + 缓存空值 |
| 缓存击穿解决 | ⭐⭐⭐⭐ | 分布式锁 + 双检锁 |
| 缓存雪崩解决 | ⭐⭐ | 随机过期时间 |
| 缓存一致性 | ⭐⭐⭐⭐ | 延迟双删 + Canal 监听 binlog |
| 缓存监控 | ⭐⭐⭐ | Actuator + Micrometer 暴露命中率 |
| 压力测试 | ⭐⭐ | JMeter 压测,量化性能提升 |
七、基础层面(必考)
1. 为什么要用缓存?缓存的核心思想是什么?
答:缓存是为了解决数据库 I/O 性能瓶颈。核心思想是用空间换时间——将经常访问的数据暂存在内存中,减少对磁盘数据库的访问次数,从而提升系统响应速度。
2. Spring Boot 缓存抽象层的工作原理?
答:Spring 通过 Cache 和 CacheManager 接口统一不同缓存技术。执行流程:
-
方法调用时,通过 AOP 代理拦截
-
根据
@Cacheable等注解生成缓存 key -
调用
CacheManager获取对应的Cache -
检查缓存中是否存在该 key 的数据
-
存在则返回;不存在则执行原方法,将结果存入缓存
3. @Cacheable、@CachePut、@CacheEvict 的区别?
| 注解 | 执行顺序 | 用途 |
|---|---|---|
| @Cacheable | 先查缓存,再决定是否执行方法 | 查询方法 |
| @CachePut | 先执行方法,再更新缓存 | 更新方法 |
| @CacheEvict | 先执行方法(默认),再清除缓存 | 删除方法 |
常见追问:@CachePut 和 @Cacheable 能一起用吗?
不建议。
@Cacheable会因缓存命中而跳过方法执行,@CachePut却强制要求执行方法,两者冲突。
4. @Cacheable 的 key 是如何生成的?
答:默认使用 SimpleKeyGenerator:
-
无参数 → 空
SimpleKey[]对象 -
单参数 → 参数值本身
-
多参数 → 包含所有参数的
SimpleKey对象
自定义方式:
@Cacheable(value = "book", key = "#id") // SpEL
@Cacheable(value = "book", keyGenerator = "myKeyGenerator") // 自定义生成器
5. condition 和 unless 的区别?
| 属性 | 作用 | 示例 |
|---|---|---|
condition |
满足条件才缓存 | condition = "#id > 0" |
unless |
满足条件不缓存 | unless = "#result == null" |
八、进阶层面(加分项)
6. 什么是缓存穿透?如何解决?
定义:查询一个不存在的数据,请求直接打到数据库。
解决方案:
-
缓存空值:将 null 作为值缓存,设置较短过期时间
-
布隆过滤器:先判断 key 是否存在,不存在则直接返回
@Cacheable(value = "book", key = "#id", unless = "#result == null")
public Book findById(Integer id) {
return repository.findById(id).orElse(null);
}
7. 什么是缓存击穿?如何解决?
定义:某个热点数据过期瞬间,大量请求同时打到数据库。
解决方案:分布式锁,只让一个线程去查数据库,其他线程等待。
public Book getProductWithLock(Integer id) {
String lockKey = "lock:book:" + id;
RLock lock = redissonClient.getLock(lockKey);
try {
lock.lock();
// 双检锁:再次检查缓存
Book book = getFromCache(id);
if (book == null) {
book = getFromDB(id);
putToCache(id, book);
}
return book;
} finally {
lock.unlock();
}
}
8. 什么是缓存雪崩?如何解决?
定义:大量缓存同时过期,导致请求全部涌入数据库。
解决方案:
-
随机过期时间:在基础过期时间上加随机值
-
热点数据永不过期:异步更新
-
熔断降级:使用 Sentinel/Hystrix
// 过期时间:300秒 + 0~300秒随机值
long ttl = 300 + new Random().nextInt(300);
9. 如何保证缓存与数据库的一致性?
策略一:延迟双删
@Transactional
public void updateBook(Book book) {
deleteCache(book.getId()); // 第一次删除
bookRepository.save(book); // 更新数据库
CompletableFuture.runAsync(() -> {
Thread.sleep(500); // 延迟
deleteCache(book.getId()); // 第二次删除
});
}
策略二:订阅 binlog(Canal):监听数据库变更,自动更新缓存。
10. 项目中同时有 Ehcache 和 Redis,Spring Boot 如何选择?
规则:
-
配置了
spring.cache.type→ 使用指定的 -
未配置 → 按优先级顺序查找(PPT 中的列表)
优先级顺序(部分):
1. Generic
2. JCache
3. EhCache 2.x ← 比 Redis 优先级高
4. Redis ← 优先级较低
...
11. Redis 和 Ehcache 的区别?如何选择?
| 维度 | Ehcache | Redis |
|---|---|---|
| 架构 | 本地缓存(JVM 内) | 分布式缓存(独立进程) |
| 速度 | 非常快 | 较快(网络 I/O) |
| 持久化 | 支持磁盘 | 支持 RDB/AOF |
| 集群支持 | 需 Terracotta | 原生支持 |
| 适用场景 | 单机应用、热点数据 | 多应用共享、分布式环境 |
选择建议:
-
单机应用 + 高速需求 → Ehcache
-
分布式应用 + 共享需求 → Redis
-
最佳实践:多级缓存(L1 Ehcache + L2 Redis)
九、项目实战层面(展示你的项目)
12. 你的项目中如何验证缓存生效?
答:通过控制台 SQL 日志判断。
-
第一次查询:打印 SQL
-
第二次查询:无 SQL,说明命中缓存
13. 你在做 Ehcache 持久化时遇到什么问题?
答:重启项目后缓存丢失。原因是:
-
未配置
diskPersistent="true" -
实体类未实现
Serializable
解决后,重启项目再次查询不打印 SQL,说明从磁盘缓存加载成功。
14. 你做 Redis 缓存时遇到什么问题?
答:报错 NOAUTH Authentication required。原因是 Redis 设置了密码,配置文件未添加 spring.redis.password。添加后解决。
15. 你觉得这个项目还有什么可以完善的地方?
答(展现思考深度):
-
加入多级缓存(Ehcache + Redis)
-
使用布隆过滤器解决缓存穿透
-
用 Redisson 分布式锁解决缓存击穿
-
接入 Actuator 监控缓存命中率
-
用 JMeter 做压测,量化性能提升
十、快速记忆卡片
| 概念 | 一句话解释 | |
|---|---|---|
| 缓存穿透 | 查不到的数据 | 布隆过滤器 / 缓存空值 |
| 缓存击穿 | 热点数据过期 | 分布式锁 |
| 缓存雪崩 | 大量数据同时过期 | 随机过期时间 |
| 延迟双删 | 先删缓存 → 更新 DB → 延迟再删 | 保证一致性 |
| @Cacheable | 先查缓存,没有再执行 | 查询 |
| @CachePut | 先执行,再更新缓存 | 更新 |
| @CacheEvict | 清除缓存 | 删除 |
十一、面试官还会追问什么?
| 追问方向 | 你的回答要点 |
|---|---|
| 看过 Spring Cache 源码吗? | 看过核心流程:CacheAspectSupport.execute() |
| Redis 的持久化方式? | RDB(快照)+ AOF(日志) |
| 如何保证 Redis 的高可用? | 主从复制 + 哨兵 / 集群模式 |
| 本地缓存会不会有 OOM 风险? | 设置 maxElementsInMemory + 淘汰策略 LRU |
更多推荐




所有评论(0)