Spring Boot 缓存体系
目录
- 一、为什么需要缓存?
- [二、Spring Cache 抽象体系总览](#二、Spring Cache 抽象体系总览 "#%E4%BA%8Cspring-cache-%E6%8A%BD%E8%B1%A1%E4%BD%93%E7%B3%BB%E6%80%BB%E8%A7%88")
- 三、核心概念详解
- 四、缓存注解详解
- [五、Spring Boot 缓存自动配置流程](#五、Spring Boot 缓存自动配置流程 "#%E4%BA%94spring-boot-%E7%BC%93%E5%AD%98%E8%87%AA%E5%8A%A8%E9%85%8D%E7%BD%AE%E6%B5%81%E7%A8%8B")
- [六、运行时调用流程 ------ 注解触发后发生了什么](#六、运行时调用流程 —— 注解触发后发生了什么 "#%E5%85%AD%E8%BF%90%E8%A1%8C%E6%97%B6%E8%B0%83%E7%94%A8%E6%B5%81%E7%A8%8B--%E6%B3%A8%E8%A7%A3%E8%A7%A6%E5%8F%91%E5%90%8E%E5%8F%91%E7%94%9F%E4%BA%86%E4%BB%80%E4%B9%88")
- [七、实战 Demo:从零搭建 Redis 缓存](#七、实战 Demo:从零搭建 Redis 缓存 "#%E4%B8%83%E5%AE%9E%E6%88%98-demo%E4%BB%8E%E9%9B%B6%E6%90%AD%E5%BB%BA-redis-%E7%BC%93%E5%AD%98")
- [八、扩展:@CacheableExt 如何实现按缓存名设置过期时间](#八、扩展:@CacheableExt 如何实现按缓存名设置过期时间 "#%E5%85%AB%E6%89%A9%E5%B1%95cacheableext-%E5%A6%82%E4%BD%95%E5%AE%9E%E7%8E%B0%E6%8C%89%E7%BC%93%E5%AD%98%E5%90%8D%E8%AE%BE%E7%BD%AE%E8%BF%87%E6%9C%9F%E6%97%B6%E9%97%B4")
- [九、经典踩坑:Redisson 导致 RedisCacheManager 失效](#九、经典踩坑:Redisson 导致 RedisCacheManager 失效 "#%E4%B9%9D%E7%BB%8F%E5%85%B8%E8%B8%A9%E5%9D%91redisson-%E5%AF%BC%E8%87%B4-rediscachemanager-%E5%A4%B1%E6%95%88")
- 十、总结
一、为什么需要缓存?
1.1 一个生活中的类比
想象你去图书馆借书:
| 场景 | 对应技术 | 说明 |
|---|---|---|
| 每次都去书架上找书 | 无缓存 | 每次请求都查数据库,慢 |
| 把常用书放在桌上 | 有缓存 | 第一次查数据库,结果放入缓存,后续直接读缓存 |
1.2 缓存的核心思想
第一次请求: 调用方法 → 查数据库 → 得到结果 → 存入缓存 → 返回结果
后续请求: 调用方法 → 查缓存 → 命中 → 直接返回结果(不再查数据库)
1.3 没有缓存 vs 有缓存
java
// ========== 没有缓存:每次都查数据库 ==========
public User getUserById(Long id) {
// 每次调用都执行 SQL,数据库压力大
return userMapper.selectById(id);
}
// ========== 有缓存:第一次查数据库,后续走缓存 ==========
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
// 只有缓存未命中时才执行 SQL
return userMapper.selectById(id);
}
二、Spring Cache 抽象体系总览
2.1 核心设计理念
Spring Cache 的设计遵循了一个重要原则:对缓存操作进行抽象,与具体实现解耦。
就像 JDBC 是数据库的抽象层一样,Spring Cache 是缓存的抽象层。你写代码时只关心"我要缓存这个结果",不关心底层用的是 Redis、Caffeine 还是 Ehcache。
2.2 体系架构图
less
┌─────────────────────────────────────────────────────────────┐
│ 你的业务代码 │
│ @Cacheable / @CacheEvict / @CachePut │
└──────────────────────────┬──────────────────────────────────┘
│ AOP 拦截
▼
┌─────────────────────────────────────────────────────────────┐
│ Spring Cache 抽象层(接口) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │
│ │ CacheManager │ │ Cache │ │ CacheResolver │ │
│ │ 缓存管理器 │ │ 缓存实例 │ │ 缓存解析器 │ │
│ └──────┬──────┘ └──────┬───────┘ └────────┬─────────┘ │
└─────────┼────────────────┼───────────────────┼──────────────┘
│ │ │
▼ ▼ ▼
┌─────────────────────────────────────────────────────────────┐
│ 具体实现层(可替换) │
│ │
│ ┌─────────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ RedisCacheManager│ │CaffeineCache │ │JCacheCache │ │
│ │ RedisCache │ │Manager │ │Manager │ │
│ │ │ │CaffeineCache │ │JCacheCache │ │
│ └─────────────────┘ └──────────────┘ └───────────────┘ │
└─────────────────────────────────────────────────────────────┘
2.3 核心接口关系
scss
CacheManager(缓存管理器)
│
├── getCache(String name) ──→ Cache(缓存实例)
│ │
│ ├── get(Object key) → 读取缓存
│ ├── put(Object key, Object value) → 写入缓存
│ └── evict(Object key) → 删除缓存
│
└── getCacheNames() ──→ 获取所有缓存名称
CacheResolver(缓存解析器)
│
└── resolveCaches(CacheOperationInvocationContext) ──→ 根据上下文解析出 Cache 集合
三、核心概念详解
3.1 CacheManager ------ 缓存管理器
作用:管理多个缓存实例,是整个缓存体系的"管家"。
java
public interface CacheManager {
// 根据名称获取缓存实例
// 返回 null 表示不存在该缓存
@Nullable
Cache getCache(String name);
// 获取所有已知的缓存名称
Collection<String> getCacheNames();
}
不同实现的差异(关键!):
| 实现类 | 遇到未知缓存名时的行为 | 说明 |
|---|---|---|
RedisCacheManager |
自动创建新的缓存实例 | 宽松模式,对开发者友好 |
CaffeineCacheManager |
自动创建新的缓存实例 | 同上 |
JCacheCacheManager |
返回 null,不自动创建 | 严格模式,遵循 JSR-107 规范 |
ConcurrentMapCacheManager |
可配置是否自动创建 | 默认自动创建 |
3.2 Cache ------ 缓存实例
作用:一个具体的缓存区域,类似 Redis 中的一个命名空间。
java
public interface Cache {
// 缓存名称
String getName();
// 获取底层实现(如 RedisTemplate)
Object getNativeCache();
// 读取缓存
@Nullable
ValueWrapper get(Object key);
// 写入缓存
void put(Object key, @Nullable Object value);
// 删除缓存
void evict(Object key);
// 清空缓存
void clear();
}
3.3 CacheResolver ------ 缓存解析器
作用:根据方法调用的上下文信息,解析出应该使用哪些 Cache 实例。
java
public interface CacheResolver {
// 根据调用上下文解析缓存集合
Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context);
}
默认实现 :SimpleCacheResolver
- 从
CacheManager中根据注解中的cacheNames获取对应的Cache实例。 - 如果
CacheManager.getCache()返回 null,则抛出IllegalArgumentException
四、缓存注解详解
4.1 @Cacheable ------ 读取缓存
作用:方法执行前先查缓存,命中则直接返回,未命中则执行方法并将结果存入缓存。
java
@Cacheable(
value = "userCache", // 缓存名称(别名 cacheNames)
key = "#id", // 缓存键(SpEL 表达式)
condition = "#id > 0", // 满足条件才缓存
unless = "#result == null", // 结果满足条件则不缓存
sync = true // 同步模式(防止缓存击穿)
)
public User getUserById(Long id) {
return userMapper.selectById(id);
}
执行流程:
markdown
方法被调用
│
▼
① 解析注解,获取 cacheNames 和 key
│
▼
② 通过 CacheResolver → CacheManager → Cache 查找缓存
│
├── 命中 → 直接返回缓存值,不执行方法
│
└── 未命中 → 执行方法 → 将结果写入缓存 → 返回结果
4.2 @CachePut ------ 更新缓存
作用:始终执行方法,然后将结果更新到缓存中。
java
@CachePut(
value = "userCache",
key = "#user.id"
)
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
与 @Cacheable 的区别:
| 注解 | 是否执行方法 | 何时写缓存 | 典型场景 |
|---|---|---|---|
@Cacheable |
缓存命中时不执行 | 缓存未命中时写 | 查询 |
@CachePut |
始终执行 | 每次都写 | 更新 |
4.3 @CacheEvict ------ 删除缓存
作用:删除缓存中的数据。
java
@CacheEvict(
value = "userCache",
key = "#id" // 删除指定 key
)
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
@CacheEvict(
value = "userCache",
allEntries = true // 清空整个缓存
)
public void clearAllUserCache() {
// 不需要操作数据库,只清缓存
}
4.4 @Caching ------ 组合使用
作用:一个方法上组合多个缓存注解。
java
@Caching(
cacheable = @Cacheable(value = "userCache", key = "#id"),
evict = @CacheEvict(value = "userListCache", allEntries = true)
)
public User getUserAndClearList(Long id) {
return userMapper.selectById(id);
}
4.5 @CacheConfig ------ 类级别公共配置
作用:在类级别统一配置缓存名称、Key 生成器等,方法上的注解可以覆盖。
java
@CacheConfig(cacheNames = "userCache") // 类中所有缓存注解默认使用此名称
@Service
public class UserService {
@Cacheable(key = "#id") // 不需要再写 cacheNames
public User getUserById(Long id) { ... }
@Cacheable(value = "userDetailCache", key = "#id") // 覆盖类级别配置
public UserDetail getUserDetail(Long id) { ... }
}
4.6 注解参数汇总
| 参数 | 说明 | 示例 |
|---|---|---|
value / cacheNames |
缓存名称 | "userCache" |
key |
缓存键(SpEL) | "#id", "#user.name" |
condition |
缓存条件(SpEL) | "#id > 0" |
unless |
否定条件(SpEL) | "#result == null" |
keyGenerator |
Key 生成器 | "myKeyGenerator" |
cacheManager |
指定 CacheManager | "redisCacheManager" |
cacheResolver |
指定 CacheResolver | "myCacheResolver" |
sync |
同步模式 | true |
allEntries |
清空所有(仅 @CacheEvict) | true |
beforeInvocation |
方法前删除(仅 @CacheEvict) | true |
4.7 SpEL 表达式常用写法
| 表达式 | 说明 | 示例场景 |
|---|---|---|
#p0, #a0 |
第一个参数 | key = "#p0" |
#id |
参数名(需编译时保留) | key = "#id" |
#user.name |
参数属性 | key = "#user.name" |
#result |
返回值(仅 @CachePut 和 unless) | unless = "#result == null" |
#root.method |
当前方法 | 调试用 |
#root.target |
当前对象 | 调试用 |
五、Spring Boot 缓存自动配置流程
5.1 总览:从启动到 CacheManager 创建
java
Spring Boot 应用启动
│
▼
① 加载 spring.factories 中的自动配置类
│
▼
② CacheAutoConfiguration 被触发
│
▼
③ CacheConfigurationImportSelector 导入所有缓存类型的配置类
│
▼
④ 逐个检查每个配置类的 @Conditional 条件
│
▼
⑤ 第一个满足条件的配置类生效,创建对应的 CacheManager
│
▼
⑥ 其余配置类被跳过(@ConditionalOnMissingBean 谦让规则)
5.2 第一步:CacheAutoConfiguration 入口
类 :org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
java
// Spring Boot 缓存自动配置的入口类
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(CacheManager.class) // 条件:classpath 上有 CacheManager 接口
@ConditionalOnBean(CacheAspectSupport.class) // 条件:容器中有缓存切面支持
@EnableConfigurationProperties(CacheProperties.class) // 绑定配置属性
@Import(CacheAutoConfiguration.CacheConfigurationImportSelector.class) // 关键!导入选择器
public class CacheAutoConfiguration {
// 内部类:导入所有缓存类型的配置类
static class CacheConfigurationImportSelector implements ImportSelector {
@Override
public String[] selectImports(AnnotationMetadata importingClassMetadata) {
// 遍历所有缓存类型:GENERIC, JCACHE, EHCACHE, REDIS, CAFFEINE, ...
CacheType[] types = CacheType.values();
String[] imports = new String[types.length];
for (int i = 0; i < types.length; i++) {
// 根据类型获取对应的配置类全限定名
imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
}
// 返回所有配置类,让 Spring 去逐个检查条件
return imports;
}
}
}
关键点 :CacheConfigurationImportSelector 会把所有缓存类型的配置类都导入,但最终哪个生效取决于条件注解。
5.3 第二步:各配置类的条件竞争
Spring Boot 内置的缓存配置类及其生效优先级(从高到低):
| 优先级 | 配置类 | 条件 | 创建的 CacheManager |
|---|---|---|---|
| 1 | GenericCacheConfiguration |
容器中已有 CacheManager Bean | 直接使用已有的 |
| 2 | JCacheCacheConfiguration |
classpath 有 javax.cache.CacheManager |
JCacheCacheManager |
| 3 | EhCacheCacheConfiguration |
classpath 有 Ehcache |
EhCacheCacheManager |
| 4 | HazelcastCacheConfiguration |
classpath 有 HazelcastInstance |
HazelcastCacheManager |
| 5 | InfinispanCacheConfiguration |
classpath 有 Infinispan | InfinispanCacheManager |
| 6 | CouchbaseCacheConfiguration |
classpath 有 Couchbase | CouchbaseCacheManager |
| 7 | RedisCacheConfiguration |
classpath 有 RedisConnectionFactory |
RedisCacheManager |
| 8 | CaffeineCacheConfiguration |
classpath 有 Caffeine |
CaffeineCacheManager |
| 9 | SimpleCacheConfiguration |
无其他条件(兜底) | ConcurrentMapCacheManager |
| 10 | NoOpCacheConfiguration |
spring.cache.type=none |
NoOpCacheManager |
java
final class CacheConfigurations {
private static final Map<CacheType, Class<?>> MAPPINGS;
private CacheConfigurations() {
}
static String getConfigurationClass(CacheType cacheType) {
Class<?> configurationClass = (Class)MAPPINGS.get(cacheType);
Assert.state(configurationClass != null, () -> "Unknown cache type " + cacheType);
return configurationClass.getName();
}
static CacheType getType(String configurationClassName) {
for(Map.Entry<CacheType, Class<?>> entry : MAPPINGS.entrySet()) {
if (((Class)entry.getValue()).getName().equals(configurationClassName)) {
return (CacheType)entry.getKey();
}
}
throw new IllegalStateException("Unknown configuration class " + configurationClassName);
}
//顺序
static {
Map<CacheType, Class<?>> mappings = new EnumMap(CacheType.class);
mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
MAPPINGS = Collections.unmodifiableMap(mappings);
}
}
5.4 第三步:RedisCacheConfiguration 的条件详解
类 :org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration
java
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class) // ① classpath 有 Redis 连接工厂
@AutoConfigureAfter(RedisAutoConfiguration.class) // ② 在 Redis 自动配置之后
@ConditionalOnBean(RedisConnectionFactory.class) // ③ 容器中有 RedisConnectionFactory Bean
@ConditionalOnMissingBean(CacheManager.class) // ④ 容器中没有 CacheManager(谦让规则!)
@Conditional(CacheCondition.class) // ⑤ 配置属性条件
class RedisCacheConfiguration {
@Bean
RedisCacheManager cacheManager(
CacheProperties cacheProperties,
RedisCacheConfiguration redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> customizers) {
// 创建 RedisCacheManager 的 Builder
RedisCacheManagerBuilder builder = RedisCacheManager.builder(connectionFactory)
.cacheDefaults(defaultCacheConfiguration);
// 应用所有自定义器(包括我们的 RedisCacheBuilderCustomizer)
customizers.orderedStream().forEach(customizer -> customizer.customize(builder));
// 构建 RedisCacheManager
return builder.build();
}
}
条件④是关键 :@ConditionalOnMissingBean(CacheManager.class) 意味着------如果容器中已经存在任何 CacheManager 类型的 Bean,这个配置类就不生效。
5.5 完整启动时序图
java
时间线 ──────────────────────────────────────────────────────────→
Spring Boot 启动
│
├─ 1. 加载 spring.factories
│ └─ 发现 CacheAutoConfiguration
│
├─ 2. 执行 CacheConfigurationImportSelector
│ └─ 导入所有缓存配置类
│
├─ 3. 【关键分叉】检查各配置类的条件
│ │
│ ├─ RedissonAutoConfiguration 先执行(@AutoConfigureBefore)
│ │ └─ 创建 JCacheCacheManager ← 抢先注册了 CacheManager!
│ │
│ ├─ RedisCacheConfiguration 检查条件
│ │ ├─ @ConditionalOnClass(RedisConnectionFactory) → ✅
│ │ ├─ @ConditionalOnBean(RedisConnectionFactory) → ✅
│ │ ├─ @ConditionalOnMissingBean(CacheManager.class) → ❌ JCacheCacheManager 已存在!
│ │ └─ ❌ 条件不满足,整个配置类被跳过
│ │
│ └─ 最终容器中只有 JCacheCacheManager
│
├─ 4. CacheExtAnnotationBeanPostProcessor 扫描注解
│ └─ 收集 @CacheableExt 的缓存配置
│
├─ 5. RedisCacheBuilderCustomizer 无法执行
│ └─ 因为 RedisCacheManager 没有被创建,customize() 不会被调用
│
└─ 6. 应用启动完成
└─ 缓存体系使用 JCacheCacheManager(不支持自动创建缓存)
六、运行时调用流程 ------ 注解触发后发生了什么
6.1 AOP 代理机制
Spring Cache 的注解之所以能生效,是因为 AOP(面向切面编程) 机制。
javascript
你的代码 Spring 生成的代理
┌──────────────┐ ┌──────────────────────────────────┐
│ │ │ 代理对象(Proxy) │
│ UserService │ ──→ │ │
│ │ │ getUserById(id) { │
│ │ │ ① 检查缓存 │
│ │ │ ② 命中 → 返回缓存值 │
│ │ │ ③ 未命中 → 调用真实方法 │
│ │ │ ④ 将结果写入缓存 │
│ │ │ ⑤ 返回结果 │
│ │ │ } │
└──────────────┘ └──────────────────────────────────┘
6.2 完整调用链(类级别)
当调用 userService.getUserById(1L) 时,完整的类调用链如下:
scss
① Controller 调用方法
│
▼
② JDK 动态代理 / CGLIB 代理拦截调用
│ 类:org.springframework.aop.framework.CglibAopProxy
│ 或:org.springframework.aop.framework.JdkDynamicAopProxy
│
▼
③ CacheInterceptor 拦截器执行
│ 类:org.springframework.cache.interceptor.CacheInterceptor
│ 方法:invoke(MethodInvocation)
│
▼
④ CacheAspectSupport 执行核心缓存逻辑
│ 类:org.springframework.cache.interceptor.CacheAspectSupport
│ 方法:execute(CacheOperationInvoker, Object, Method, Object[])
│
├── 4.1 获取方法上的缓存操作(注解信息)
│ 类:org.springframework.cache.interceptor.CacheOperationSource
│ 方法:getCacheOperations(Method, Class<?>)
│
├── 4.2 创建缓存操作上下文
│ 类:org.springframework.cache.interceptor.CacheAspectSupport.CacheOperationContexts
│
├── 4.3 解析需要使用的缓存
│ 类:org.springframework.cache.interceptor.CacheResolver
│ 实现:org.springframework.cache.interceptor.AbstractCacheResolver
│ 方法:resolveCaches(CacheOperationInvocationContext)
│ │
│ ├── 4.3.1 从注解获取 cacheNames
│ │
│ ├── 4.3.2 调用 CacheManager.getCache(name)
│ │ │
│ │ ├── 如果是 RedisCacheManager:
│ │ │ → 缓存不存在时自动创建 RedisCache
│ │ │ → 返回 Cache 实例 ✅
│ │ │
│ │ └── 如果是 JCacheCacheManager(如何没有提前配置的缓存名称):
│ │ → 缓存不存在时返回 null
│ │ → 抛出 IllegalArgumentException ❌ ← 报错点!
│ │
│ └── 4.3.3 返回 Cache 集合
│
├── 4.4 查找缓存
│ 类:org.springframework.cache.Cache
│ 方法:get(Object key)
│
├── 4.5 缓存命中 → 直接返回
│
└── 4.6 缓存未命中 → 执行真实方法 → 写入缓存
│
├── 执行目标方法
│
└── 写入缓存
类:org.springframework.cache.Cache
方法:put(Object key, Object value)
6.3 关键源码解析
CacheInterceptor ------ 入口
java
// Spring Cache 拦截器,实现了 MethodInterceptor 接口
// 当代理对象的方法被调用时,会先进入这个拦截器
public class CacheInterceptor extends CacheAspectSupport implements MethodInterceptor {
@Override
@Nullable
public Object invoke(final MethodInvocation invocation) throws Throwable {
Method method = invocation.getMethod();
// 将方法调用封装为 CacheOperationInvoker
CacheOperationInvoker aopAllianceInvoker = () -> {
try {
return invocation.proceed(); // 执行真实方法
} catch (Throwable ex) {
throw new CacheOperationInvoker.ThrowableWrapper(ex);
}
};
// 委托给父类 CacheAspectSupport 执行
return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments());
}
}
CacheAspectSupport ------ 核心逻辑
java
// 缓存切面支持类,包含缓存操作的核心逻辑
public abstract class CacheAspectSupport extends AbstractCacheInvoker
implements BeanFactoryAware, InitializingBean, SmartInitializingSingleton {
@Nullable
protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) {
// 1. 获取方法上的缓存操作(注解信息)
Collection<CacheOperation> operations = cacheOperationSource.getCacheOperations(method, targetClass);
// 2. 如果没有缓存操作,直接执行方法
if (CollectionUtils.isEmpty(operations)) {
return invoker.invoke();
}
// 3. 创建缓存操作上下文
CacheOperationContexts contexts = new CacheOperationContexts(operations, method, args, target, targetClass);
// 4. 处理 @CacheEvict(beforeInvocation=true)
processCacheEvicts(contexts.get(CacheEvictOperation.class), true, NO_RESULT);
// 5. 查找 @Cacheable 的缓存
Cache.ValueWrapper cacheHit = findCachedItem(contexts.get(CacheableOperation.class));
List<CachePutRequest> cachePutRequests = new LinkedList<>();
if (cacheHit == null) { // 缓存未命中
collectPutRequests(contexts.get(CacheableOperation.class), NO_RESULT, cachePutRequests);
}
Object returnValue;
if (cacheHit != null && !hasCachePut(contexts)) {
// 缓存命中,直接返回
returnValue = cacheHit.get();
} else {
// 缓存未命中,执行真实方法
returnValue = invokeOperation(invoker);
}
// 6. 处理 @CachePut
collectPutRequests(contexts.get(CachePutOperation.class), returnValue, cachePutRequests);
// 7. 执行缓存写入
for (CachePutRequest cachePutRequest : cachePutRequests) {
cachePutRequest.apply(returnValue);
}
// 8. 处理 @CacheEvict(beforeInvocation=false)
processCacheEvicts(contexts.get(CacheEvictOperation.class), false, returnValue);
return returnValue;
}
}
AbstractCacheResolver ------ 报错发生点
java
// 缓存解析器的抽象基类
// 作用:根据方法调用的上下文信息,解析出应该使用哪些 Cache 实例
public abstract class AbstractCacheResolver implements CacheResolver, InitializingBean {
private CacheManager cacheManager; // 持有 CacheManager 的引用
@Override
public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) {
// 1. 从注解中获取缓存名称
Collection<String> cacheNames = getCacheNames(context);
if (cacheNames == null) {
return Collections.emptyList();
}
// 2. 逐个获取 Cache 实例
Collection<Cache> caches = new ArrayList<>(cacheNames.size());
for (String cacheName : cacheNames) {
// 关键!调用 CacheManager.getCache()
Cache cache = this.cacheManager.getCache(cacheName);
if (cache == null) {
// ⚠️ 如果返回 null,直接抛异常!
// 这就是报错 "Cannot find cache named 'xxx'" 的地方
throw new IllegalArgumentException(
"Cannot find cache named '" + cacheName + "' for " + context.getOperation());
}
caches.add(cache);
}
return caches;
}
}
RedisCacheManager ------ 自动创建缓存
java
// Redis 实现的 CacheManager
// 特点:遇到未知的缓存名称时,会自动创建一个新的 RedisCache
public class RedisCacheManager extends AbstractTransactionSupportingCacheManager {
// 默认缓存配置
private final RedisCacheConfiguration defaultCacheConfiguration;
// 每个缓存名称对应的专属配置
private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;
@Override
protected RedisCache getMissingCache(String name) {
// 遇到未知的缓存名称时,自动创建!
// 这就是为什么 RedisCacheManager 不会报错的原因
return createCache(name, defaultCacheConfiguration);
}
@Override
protected Cache lookupCache(String name) {
Cache cache = super.lookupCache(name);
if (cache == null) {
// 缓存不存在,调用 getMissingCache 创建
cache = getMissingCache(name);
if (cache != null) {
this.cacheMap.put(name, cache); // 放入缓存池
}
}
return cache;
}
}
JCacheCacheManager ------ 不自动创建缓存
java
// JSR-107 (JCache) 规范实现的 CacheManager
// 特点:遇到未知的缓存名称时,返回 null,不会自动创建
public class JCacheCacheManager implements CacheManager {
private final javax.cache.CacheManager cacheManager;
@Override
@Nullable
public Cache getCache(String name) {
// 直接从底层 JCache CacheManager 中查找
javax.cache.Cache<Object, Object> cache = this.cacheManager.getCache(name);
if (cache == null) {
// ⚠️ 找不到就返回 null,不会自动创建!
return null;
}
return new JCacheCache(cache);
}
}
6.4 两种 CacheManager 的行为对比
scss
调用 cacheManager.getCache("product:detailConfig")
│
├── RedisCacheManager
│ │
│ ├── lookupCache("product:detailConfig")
│ │ └── 缓存池中没有 → getMissingCache()
│ │ └── 自动创建 RedisCache → 放入缓存池 → 返回 ✅
│ │
│ └── 后续正常执行:查 Redis → 未命中 → 执行方法 → 写入缓存
│
└── JCacheCacheManager
│
├── getCache("product:detailConfig")
│ └── 底层 JCache CacheManager 中没有 → 返回 null
│
└── AbstractCacheResolver 中 cache == null
└── 抛出 IllegalArgumentException ❌
"Cannot find cache named 'product:detailConfig'"
总结
Spring Cache 核心概念速查
| 概念 | 作用 | 类比 |
|---|---|---|
CacheManager |
管理多个缓存实例 | 图书馆管理员 |
Cache |
一个具体的缓存区域 | 一个书架 |
CacheResolver |
根据上下文解析缓存 | 找书架的导航员 |
@Cacheable |
读缓存(未命中时写入) | 先查书架,没有再去仓库 |
@CachePut |
写缓存(始终执行方法) | 更新书架上的书 |
@CacheEvict |
删缓存 | 从书架上撤掉书 |
Spring Boot 缓存自动配置核心流程
启动 → CacheAutoConfiguration → 导入所有配置类 → 条件竞争 → 创建 CacheManager
运行时核心调用链
arduino
方法调用 → AOP 代理 → CacheInterceptor → CacheAspectSupport
→ CacheResolver → CacheManager.getCache() → Cache.get/put/evict
关键注意事项
- 必须加
@EnableCaching,否则缓存注解不生效 - 缓存的对象必须实现
Serializable,因为需要序列化存入 Redis - 注意 CacheManager 的竞争,多个缓存组件可能抢夺 CacheManager 的位置
- 推荐设置
spring.cache.type=redis,避免自动配置的不确定性 @Cacheable的sync=true可以防止缓存击穿(同一时刻大量请求查同一个 key)unless = "#result == null"可以防止缓存空值(除非有意防止缓存穿透)
常见问题排查清单
| 现象 | 可能原因 | 排查方法 |
|---|---|---|
| 缓存注解不生效 | 未加 @EnableCaching |
检查启动类 |
| 缓存注解不生效 | 方法是 private 的 | Spring AOP 无法代理私有方法 |
| 缓存注解不生效 | 同一个类内部调用 | 内部调用不经过代理,缓存不生效 |
| Cannot find cache named xxx | CacheManager 不是 RedisCacheManager | 用 CacheDebugRunner 诊断 |
| 缓存数据乱码 | 序列化方式不对 | 配置 RedisCacheConfiguration |
| 缓存 key 不符合预期 | SpEL 表达式写错 | 检查 key 属性 |
| 缓存过期时间不对 | 未配置 spring.cache.type=redis |
检查 application.yml |