在 SpringBoot 里做缓存,除了手动操作 RedisTem)plate,更优雅、更常用的方式就是Spring 自带的缓存注解 。
不用写重复的缓存逻辑,只需要在方法上加个注解,就能自动实现缓存读写,代码干净又好维护。
本篇文章就来讲讲最核心的两个注解:
-
• @Cacheable:查询时自动缓存
-
• @CacheEvict:更新/删除时自动清理缓存
一、为什么要用缓存注解?
-
• 无侵入:业务代码和缓存代码分离,不污染逻辑
-
• 极简开发:一行注解替代一堆
get/set缓存代码 -
• 统一管理:过期时间、缓存名称、key 规则集中配置
-
• 适配多种缓存:Redis、Caffeine、内存缓存都支持
适合:
-
• 查询多、修改少的接口
-
• 商品详情、用户信息、字典数据、配置列表
-
• 不想写重复缓存模板代码的场景
二、基础环境准备
1. 引入依赖
go
<!-- Redis + 缓存 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 启动类开启缓存
go
@SpringBootApplication
@EnableCaching // 必须加
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
3. application.yml 缓存配置
go
spring:
cache:
type: redis
redis:
time-to-live: 3600000 # 默认1小时过期
cache-null-values: false # 不缓存null
三、核心注解 1:@Cacheable(查询 + 缓存)
作用:
第一次执行方法 → 查库 → 结果存入缓存
后续请求 → 直接走缓存,不执行方法体
1. 基础用法
go
@Cacheable(value = "userCache", key = "#userId")
public User getUserById(Long userId) {
return userMapper.selectById(userId);
}
-
•
value / cacheNames:缓存名(区分不同业务) -
•
key:缓存 key,支持 SpEL 表达式
最终 Redis key:
go
userCache::1001
2. 常用 key 写法
go
// 单个参数
@Cacheable(value = "user", key = "#id")
// 对象参数取id
@Cacheable(value = "user", key = "#user.id")
// 方法名当key
@Cacheable(value = "dict", key = "#root.methodName")
// 组合key
@Cacheable(value = "order", key = "'uid:'+#userId+':type:'+#type")
3. 条件缓存(满足才缓存)
go
// 只缓存成年用户
@Cacheable(value = "user", key = "#userId", condition = "#result.age >= 18")
go
// 结果不为null才缓存
@Cacheable(value = "user", key = "#userId", unless = "#result == null")
四、核心注解 2:@CacheEvict(删除缓存)
作用: 数据更新/删除后,清理旧缓存,保证数据一致。
1. 根据 key 删除
go
@CacheEvict(value = "userCache", key = "#user.id")
public void updateUser(User user) {
userMapper.updateById(user);
}
2. 删除整个缓存名下所有 key
go
@CacheEvict(value = "userCache", allEntries = true)
public void refreshAllUser() {
}
3. 方法执行前删除
go
@CacheEvict(value = "user", key = "#userId", beforeInvocation = true)
五、另外两个常用注解
1. @CachePut
强制更新缓存,每次都会执行方法体,适合实时更新缓存。
go
@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
2. @Caching
组合多个缓存操作:
go
@Caching(
evict = {
@CacheEvict(value = "user", key = "#userId"),
@CacheEvict(value = "userOrder", key = "#userId")
}
)
public void deleteUser(Long userId) {
}
六、模拟场景
场景 1:用户详情(典型查询缓存)
go
@Cacheable(value = "userInfo", key = "#userId", unless = "#result == null")
public User getUser(Long userId) {
return userMapper.selectById(userId);
}
场景 2:修改用户 → 清理缓存
go
@CacheEvict(value = "userInfo", key = "#user.id")
public void updateUser(User user) {
userMapper.updateById(user);
}
场景 3:删除用户 → 清理缓存
go
@CacheEvict(value = "userInfo", key = "#userId")
public void deleteUser(Long userId) {
userMapper.deleteById(userId);
}
场景 4:商品列表缓存
go
@Cacheable(value = "productList", key = "#categoryId")
public List<Product> getProductList(Integer categoryId) {
return productMapper.selectByCategory(categoryId);
}
场景 5:批量刷新商品缓存
go
@CacheEvict(value = "productList", allEntries = true)
public void refreshProduct() {
}
场景 6:字典/配置(几乎不变,长期缓存)
go
@Cacheable(value = "dictCache", key = "#dictType")
public List<Dict> getDict(String dictType) {
return dictMapper.selectByType(dictType);
}
七、注意事项
-
- 同类方法内调用,注解失效
因为走了代理,同类内部方法调用不经过 AOP。
解决:抽取到独立 Service 或自己注入自己。
-
- 缓存 key 冲突
不同业务一定要用不同
value/cacheNames。 -
- 缓存与数据库不一致
增删改必须配合
@CacheEvict或@CachePut。 -
- 大数据量列表缓存
列表缓存容易占内存,建议设置更短过期时间 + 分页缓存。
-
- null 值被缓存
用
unless = "#result == null"避免。 -
- 事务与缓存顺序问题
建议事务提交后再清缓存,否则会出现"脏缓存"。
八、@Cacheable vs @CachePut vs @CacheEvict 总结
| 注解 | 作用 | 执行方法体 | 典型场景 |
|---|---|---|---|
| @Cacheable | 查 + 缓存 | 缓存不存在才执行 | 查询接口 |
| @CachePut | 强制更新缓存 | 每次都执行 | 实时同步 |
| @CacheEvict | 删除缓存 | 每次都执行 | 增删改 |
九、总结
SpringBoot 缓存注解是后端最实用的简化技巧之一:
-
• 查询用
@Cacheable -
• 更新删除用
@CacheEvict -
• 实时同步用
@CachePut
你在项目里更喜欢用注解还是手动操作 Redis?有没有遇到过缓存不一致、注解不生效的坑?
欢迎在评论区留言交流,关注我!