Spring Boot 整合 Caffeine 本地缓存实战

Spring Boot 整合 Caffeine 本地缓存实战

前言

在微服务中,有些配置数据(如文档保留规则)变更频率极低,但每次业务操作都要查库,造成不必要的 DB 压力。本文以实际项目为例,记录 Spring Boot + Caffeine 本地缓存的完整配置过程。

整体架构

复制代码
@Cacheable / @CacheEvict(Spring 声明式缓存注解)
        ↓
Spring Cache Abstraction(CacheManager 接口)
        ↓
CaffeineCacheManager(桥接实现)
        ↓
Caffeine(高性能 JVM 本地内存缓存)

Spring 提供缓存抽象和 AOP 拦截,Caffeine 负责实际的缓存存储和淘汰策略。

一、引入依赖

复制代码
xml
<!-- Spring 缓存抽象:@Cacheable、@CacheEvict、CacheManager -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 提供 CaffeineCacheManager 桥接类 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

<!-- Caffeine 缓存库 -->
<dependency>
    <groupId>com.github.benmanes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

Caffeine 是 Guava Cache 的继任者,基于 W-TinyLFU 淘汰算法,命中率极高,是 Spring Boot 推荐的本地缓存实现。

二、配置 CacheManager

复制代码
java
@Configuration
@EnableCaching
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
                .expireAfterWrite(3600, TimeUnit.SECONDS)  // 写入后 1 小时过期
                .maximumSize(1000);                         // 最多缓存 1000 个条目
        cacheManager.setCaffeine(caffeine);
        return cacheManager;
    }
}

关键配置说明:

配置项 含义
@EnableCaching 开启缓存注解的 AOP 代理,没有它注解不会生效
expireAfterWrite TTL 策略,缓存条目写入后固定时间过期
maximumSize 容量上限,超出后按 W-TinyLFU 算法淘汰冷数据

三、Service 层使用缓存注解

3.1 读缓存 --- @Cacheable

复制代码
java
@Service
@Slf4j
public class DocumentRetentionServiceImpl implements DocumentRetentionService {

    @Autowired
    DocumentRetentionRepo documentRetentionRepo;

    @Cacheable(cacheNames = "document_retention", key = "#root.methodName")
    @Override
    public List<DocumentRetentionPo> getAllDocumentRetentionPoList() {
        log.info("Get document retention rules from DB");
        return documentRetentionRepo.findAll();
    }
}

参数解析:

参数 含义
cacheNames 缓存空间名称(逻辑分区)
key = "#root.methodName" SpEL 表达式,取方法名作为缓存 key

执行流程:

复制代码
调用 getAllDocumentRetentionPoList()
    ↓
Spring AOP 代理拦截
    ↓
用 key="getAllDocumentRetentionPoList" 查 Caffeine
    ├── 命中 → 直接返回缓存值,方法体不执行(日志不打印)
    └── 未命中 → 执行方法体 → 查 DB → 结果写入缓存 → 返回

3.2 写操作清缓存 --- @CacheEvict

复制代码
java
@CacheEvict(allEntries = true, cacheNames = "document_retention")
@Override
public void addRetention(DocumentRetentionDto dto) {
    // 新增规则...
    documentRetentionRepo.save(documentRetentionPo);
}

@CacheEvict(allEntries = true, cacheNames = "document_retention")
@Override
public void updateRetentionByRetentionId(DocumentRetentionDto dto) {
    // 更新规则...
    documentRetentionRepo.save(documentRetentionPo);
}

@CacheEvict(allEntries = true, cacheNames = "document_retention")
@Override
public void deleteRetention(Integer id) {
    // 删除规则...
    documentRetentionRepo.deleteById(id);
}

allEntries = true:清空该 cacheName 下所有 key。因为规则列表是整体缓存的,任何一条变更都需要全量失效。

3.3 定时兜底清缓存 --- @Scheduled + @CacheEvict

复制代码
java
@CacheEvict(allEntries = true, cacheNames = {"document_retention", "document_permission"})
@Scheduled(cron = "0 0 * * * ?")  // 每小时整点执行
public void removeCache() {
    log.info("clear document retention rules cache");
}

这是一个兜底策略:即使没有触发增删改,也定期刷新缓存,防止脏数据长期驻留。

四、单元测试验证缓存行为

复制代码
java
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE)
@ActiveProfiles("test")
public class CacheTest {

    @Autowired
    CacheManager manager;

    @Autowired
    DocumentRetentionService documentRetentionService;

    @Autowired
    DocumentService documentService;

    @MockBean
    DocumentRetentionRepo documentRetentionRepo;

    @BeforeEach
    void clearCache() {
        documentService.removeCache(); // 每个测试前清空,避免测试间干扰
    }

    @Test
    void should_save_to_cache_when_call_get_retention() {
        // 初始缓存为空
        assertNull(manager.getCache("document_retention").get("getAllDocumentRetentionPoList"));

        when(documentRetentionRepo.findAll())
            .thenReturn(List.of(DocumentRetentionPo.builder().bucket("test").build()));

        documentRetentionService.getAllDocumentRetentionPoList(); // 第1次:miss → 查DB
        documentRetentionService.getAllDocumentRetentionPoList(); // 第2次:hit → 不查DB

        // 缓存已填充
        assertNotNull(manager.getCache("document_retention").get("getAllDocumentRetentionPoList"));
        // DB 只被调用 1 次,证明第 2 次走了缓存
        verify(documentRetentionRepo, times(1)).findAll();
    }

    @Test
    void should_clear_cache_when_call_clear() {
        when(documentRetentionRepo.findAll())
            .thenReturn(List.of(DocumentRetentionPo.builder().bucket("test").build()));

        documentRetentionService.getAllDocumentRetentionPoList(); // 填充缓存

        documentService.removeCache(); // 清除缓存

        assertNull(manager.getCache("document_retention").get("getAllDocumentRetentionPoList"));
    }
}

验证思路: 通过 verify(repo, times(1)) 确认方法调用了两次但 DB 只查了一次,即可证明缓存命中。

五、踩坑记录

坑 1:同类内部调用,缓存不生效

复制代码
java
// ❌ 错误:this 调用绕过了 AOP 代理
public void methodA() {
    this.getAllDocumentRetentionPoList(); // 缓存不生效!
}

原因: @Cacheable 依赖 Spring AOP 代理,this.method() 是直接调用目标对象,不经过代理。

解决: 注入自身、使用 AopContext.currentProxy()、或拆分到不同类。

坑 2:测试间缓存互相干扰

同一 @SpringBootTest 上下文共享 CacheManager,前一个测试填充的缓存会影响后续测试。

解决: @BeforeEach 中调用 removeCache() 清空缓存。

坑 3:多实例部署缓存不一致

Caffeine 是 JVM 本地缓存,多实例部署时各节点缓存独立,无法自动同步。

解决:

  • 定时清除兜底(本项目方案)
  • 换用 Redis 等分布式缓存
  • 通过消息广播通知各节点失效

六、总结

步骤 内容
1 引入 spring-boot-starter-cache + caffeine 依赖
2 @EnableCaching + 配置 CaffeineCacheManager Bean
3 读方法加 @Cacheable,写方法加 @CacheEvict
4 定时任务兜底清除,防止脏数据
5 单元测试通过 Mock 验证缓存命中/失效行为

本地缓存适合读多写少、对一致性要求不高的场景。如果业务需要强一致或跨实例共享,应考虑 Redis 等分布式方案。

复制代码
直接复制即可发布。
相关推荐
Revolution611 小时前
Nest.js 是什么:怎样用它写出第一个后端接口
后端·node.js·nestjs
aiopencode1 小时前
SwiftUI Introspect生产环境完全指南:为什么它是安全可靠的选择
后端·ios
shengjk11 小时前
x86架构发展史:从8086到x86-64,一文看懂40多年CPU指令集如何改变世界
后端
天丁o2 小时前
Spring Boot + MyBatis Plus 考勤日报统计报表:打卡记录聚合、异常分类和明细下钻 Demo
spring boot·mybatis plus·企业数字化·考勤系统·报表统计
zx1154502 小时前
【热点 Key 探测与加长缓存 TTL 实战】
缓存
Devin~Y3 小时前
从本地生活电商到 AI RAG:互联网大厂 Java 面试场景完整实战
java·spring boot·redis·elasticsearch·spring cloud·kafka·rag
米码收割机3 小时前
SA508-3钢回火焊道焊接温度场数值模拟(模拟图+论文)
spring boot·express·宠物
JackSparrow4143 小时前
前端安全之JS混淆+请求加密+请求签名以提升爬虫难度
前端·javascript·后端·爬虫·python·安全
geovindu3 小时前
go:loghelper
开发语言·后端·golang