最近完成了 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 持久化时遇到什么问题?

:重启项目后缓存丢失。原因是:

  1. 未配置 diskPersistent="true"

  2. 实体类未实现 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

项目源码:https://gitee.com/luo-ziqi_luoziqi/chapter06.git

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐