🌈个人主页 :一条泥憨鱼 (欢迎各位大佬莅临)

前言:
写业务接口的时候,这种事情天天发生:一个查用户信息的方法,每次请求都去摸数据库。接口调用量一大,数据库就哐哐扛。问题是------用户信息半天都不变一次,这些查询全是白干的。
就是把第一次的结果记下来,下次直接用。Spring Cache干的就是这个。
它是个缓存抽象层。你用注解告诉 Spring**「这个方法的返回值可以缓存」**,具体怎么存、存哪,不用管。
它不是缓存,是缓存的遥控器
新手容易搞混:Spring Cache 不是 Redis,不是 Caffeine,不是任何一种具体的缓存技术。它是个统一接口层 ,背后可以接不同的缓存实现:
本地:ConcurrentMapCache(默认,底层是 Map,只适合测试)、Caffeine(正经的高性能本地缓存)
分布式:Redis(生产环境主力,多实例共享)
关键在于------业务代码不用动。换个 CacheManager 配置,就能从本地缓存切到 Redis。解耦这件事,才是 Spring Cache 真正值钱的地方。
怎么开
Spring Boot 项目加两个依赖(Redis 为例):
XML
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
启动类上拍一个注解,总开关就打开了:
java
@SpringBootApplication
@EnableCaching
public class CacheDemoApplication {
public static void main(String[] args) {
SpringApplication.run(CacheDemoApplication.class, args);
}
}
四个核心注解
@Cacheable------查
用得最多的一个。先去缓存里找,有就直接返回;没有就执行方法,把结果塞进缓存。
java
@Cacheable(value = "userCache", key = "#id")
}
第一次调 getUserById(1L) 打日志、查库。第二次同样参数,日志不打了,直接走缓存。
@CachePut------更新时刷新
跟 @Cacheable 的区别:它每次都会执行方法,只是顺手把返回值写回缓存。更新操作用这个:
java
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
@CacheEvict------删
数据删了、失效了,缓存也得跟着清。不然数据库已经变了,缓存里还是老数据:
java
@CacheEvict(value = "userCache", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
也可以直接把整个缓存分组清掉:
java
@CacheEvict(value = "userCache", allEntries = true)
public void clearAllUserCache() {
}
@Caching------组合
一个方法要同时操作多个缓存,用这个拼起来:
java
@Caching(
put = { @CachePut(value = "userCache", key = "#user.id") },
evict = { @CacheEvict(value = "userListCache", allEntries = true) }
)
public User saveAndRefresh(User user) {
userMapper.insert(user);
return user;
}
底层就是 AOP
跟 @Transactional 一模一样------动态代理。
Spring 检测到 Bean 的方法上有缓存注解,就给这个 Bean 包一层代理。调用链路是这样的:
拿 key 去 CacheManager 找缓存
命中→直接返回,原方法不执行
没命中→执行原方法,结果丢进缓存
这也是那个经典坑的来源:同类内部调用,缓存注解直接不生效。
java
public void doSomething() {
this.getUserById(1L); // this 调用,不是代理对象,AOP 被绕过去了
}
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
return userMapper.selectById(id);
}
this.getUserById() 跳过了代理,AOP 根本没有介入的机会。修法通常是拆到另一个 Bean,或者注入自己的代理对象(AopContext.currentProxy())。
几个实用点
条件缓存:condition 和 unless
java
// condition:执行前判断,满足才缓存
@Cacheable(value = "userCache", key = "#id", condition = "#id > 0")
// unless:执行后判断,满足就不缓存(能拿到返回值 #result)
@Cacheable(value = "userCache", key = "#id", unless = "#result == null")
unless 特别有用。null 结果不缓存,不然缓存穿透问题会放大。
自定义 key
参数多的时候用 SpEL 拼:
java
@Cacheable(value = "orderCache", key = "#userId + '_' + #orderId")
public Order getOrder(Long userId, Long orderId) {
return orderMapper.selectOrder(userId, orderId);
}
过期时间(Redis)
注解上没法直接设,要在 CacheManager 配置里统一处理:
java
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory).cacheDefaults(config).build();
}
避坑清单
1. 缓存穿透:
大量请求查不存在的数据,每次都绕过缓存打到 DB。用 unless 缓存空对象,或者上前置的布隆过滤器。
2. 缓存雪崩:
一堆 key 同时过期,瞬间流量全压到数据库。TTL 加随机偏移,别让它们集体去世。
3. 数据一致性:
更新数据库忘了清缓存,或者顺序搞反了。常规做法是先更新数据库再删缓存(反过来会有并发问题)。
4. 同类自调用失效:
上面说过了,AOP 只在外部调用时拦截。
总结
Spring Cache 把**「要不要缓存」** 和**「用什么缓存」**拆开了。业务代码只需要几个注解,底层从 ConcurrentMap 换到 Redis 一行业务代码都不用改。理解了 AOP 代理这件事,大部分坑就能绕开。