一、 核心定义与作用
Spring Cache 不是一个具体的缓存实现(它不是 Redis,也不是 EhCache),而是一套 缓存抽象层(Cache Abstraction)。
1. 核心定位
它类似于 JDBC 之于数据库。
- JDBC 定义了标准接口,你可以底层换 MySQL 或 Oracle。
- Spring Cache 定义了标准注解 (
@Cacheable),你可以底层换 Redis、Caffeine 或 ConcurrentMap,而业务代码不需要修改一行。
2. 核心价值
- 代码解耦:将业务逻辑(Service)与缓存技术细节分离。
- 消除样板代码 :通过 AOP(面向切面编程)管理缓存,不再需要手动编写
redisTemplate.get/set。 - 统一标准:无论底层用什么,上层注解用法一致。
二、 核心注解体系
Spring Cache 主要通过以下 5 个注解控制缓存行为:
| 注解 | 核心作用 | 执行逻辑 (通俗版) | 适用场景 |
|---|---|---|---|
@EnableCaching |
总开关 | 开启 Spring 的缓存代理功能。 | 启动类 / 配置类 |
@Cacheable |
查/存 | 1. 先查缓存,有则直接返回。2. 无则执行方法,并将结果存入缓存。 | 查询 (Get) |
@CachePut |
改/存 | 始终执行方法,并将返回值强制更新到缓存中。 | 新增/修改 (Save/Update) |
@CacheEvict |
删 | 执行方法(前或后),从缓存中删除指定数据。 | 删除 (Delete) |
@Caching |
组合 | 在一个方法上叠加多个操作(如删 A 缓存同时删 B 缓存)。 | 复杂联动场景 |
通用关键参数
所有注解(除开关外)都支持以下参数:
value/cacheNames:缓存名称(对应 Redis 的 Key 前缀)。key:缓存 Key,支持 SpEL 表达式 (如#id,#user.name)。condition:事前判断 。满足条件才处理缓存(例如:#id > 10)。unless:事后判断 。满足条件不 缓存(例如:#result == null)。
三、 实践案例详解
以下模拟一个用户系统,展示如何结合 Redis 使用。
1. 查询缓存(@Cacheable)
需求 :查询用户,缓存有直接返回,无则查库并回填。同时防止缓存 null 值。
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
/**
* value = "users": 对应 Redis 前缀 "users::" (或自定义的 "users:")
* key = "#id": 取参数 id 作为后缀
* unless = "#result == null": 如果查不到数据,不要把 null 存进 Redis
*/
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
System.out.println("--- 走数据库查询 ---");
return userMapper.selectById(id);
}
}
2. 保持一致性(@CacheEvict 推荐)
需求:修改用户信息后,清理缓存,保证下次查询获取最新数据。
- 最佳实践 :优先使用 删除模式 (Evict) 而非更新模式 (Put),避免并发写导致脏数据。
java
/**
* 更新完成后,删除对应的缓存 Key
* key 必须与查询时的 key 生成规则一致
*/
@CacheEvict(value = "users", key = "#user.id")
public void updateUser(User user) {
userMapper.updateById(user);
}
/**
* 场景:删除用户,或者清空整个缓存桶
* allEntries = true: 删除 users 下的所有 key
*/
@CacheEvict(value = "users", allEntries = true)
public void clearAllCache() {
System.out.println("清空用户缓存");
}
3. 复杂 Key 生成 (SpEL)
需求:根据多个参数组合生成 Key。
java
/**
* 假设 type=VIP, page=1
* Redis Key: user_list:VIP_1
*/
@Cacheable(value = "user_list", key = "#type + '_' + #page")
public List<User> getUsersByType(String type, int page) {
return userMapper.selectByType(type, page);
}
四、 避坑指南:内部调用失效 (Self-Invocation)
这是 Spring Cache 最经典的"大坑"。
1. 现象描述
在同一个 Service 类内部,方法 A 调用带 @Cacheable 的方法 B,方法 B 的缓存注解失效,每次都会执行 SQL。
2. 原理图解
Spring Cache 基于 代理模式 (Proxy Pattern)。
- 外部调用:Controller -> Proxy对象 -> 拦截处理缓存 -> 目标对象。
- 内部调用 :
this.method()是目标对象自己调用自己,绕过了 Proxy 对象,所以 AOP 切面无法执行。
3. 代码示例与修复
❌ 错误写法 (失效)
java
@Service
public class UserService {
// 方法 A:批量查询
public List<User> getUsersByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
// 【失效原因】:这里等同于 this.getUserById(id)
// 直接调用了类内部的方法,没有经过 Spring 的代理类
users.add(getUserById(id));
}
return users;
}
// 方法 B:单查(注解在此)
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
System.out.println("查询数据库...");
return new User(id, "name");
}
}
✅ 修复方案 1:自我注入 (简单有效)
通过注入自身(代理对象)来调用,强行经过代理层。
java
@Service
public class UserService {
// 1. 注入自己 (加上 @Lazy 防止循环依赖报错)
@Autowired
@Lazy
private UserService self;
public List<User> getUsersByIds(List<Long> ids) {
List<User> users = new ArrayList<>();
for (Long id : ids) {
// 2. 【修复】:使用代理对象 self 调用,而不是 this
// 流程:self -> Proxy -> 检查缓存 -> Target
users.add(self.getUserById(id));
}
return users;
}
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// ...
}
}
✅ 修复方案 2:服务拆分 (架构更优)
将缓存方法抽离到另一个 Service 中,符合单一职责原则。
java
// 新服务:专门负责缓存操作
@Service
public class UserCacheService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) { ... }
}
// 原服务:注入上面的服务
@Service
public class UserService {
@Autowired
private UserCacheService userCacheService;
public List<User> getUsersByIds(List<Long> ids) {
// 外部调用,缓存必然生效
return ids.stream().map(userCacheService::getUserById).collect(Collectors.toList());
}
}