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 等分布式方案。
直接复制即可发布。