🌈个人主页:一条泥憨鱼(欢迎各位大佬莅临)
🎬精选专栏:数据结构与算法,Java ,AI与Agent
前言:
写业务接口的时候,这种事情天天发生:一个查用户信息的方法,每次请求都去摸数据库。接口调用量一大,数据库就哐哐扛。问题是——用户信息半天都不变一次,这些查询全是白干的。
就是把第一次的结果记下来,下次直接用。Spring Cache干的就是这个。
它是个缓存抽象层。你用注解告诉 Spring「这个方法的返回值可以缓存」,具体怎么存、存哪,不用管。
它不是缓存,是缓存的遥控器
新手容易搞混:Spring Cache 不是 Redis,不是 Caffeine,不是任何一种具体的缓存技术。它是个统一接口层,背后可以接不同的缓存实现:
- 本地:ConcurrentMapCache(默认,底层是 Map,只适合测试)、Caffeine(正经的高性能本地缓存)
- 分布式:Redis(生产环境主力,多实例共享)
关键在于——业务代码不用动。换个 CacheManager 配置,就能从本地缓存切到 Redis。解耦这件事,才是 Spring Cache 真正值钱的地方。
怎么开
Spring Boot 项目加两个依赖(Redis 为例):
<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>启动类上拍一个注解,总开关就打开了:
@SpringBootApplication @EnableCaching public class CacheDemoApplication { public static void main(String[] args) { SpringApplication.run(CacheDemoApplication.class, args); } }四个核心注解
@Cacheable——查
用得最多的一个。先去缓存里找,有就直接返回;没有就执行方法,把结果塞进缓存。
@Cacheable(value = "userCache", key = "#id") }第一次调 getUserById(1L) 打日志、查库。第二次同样参数,日志不打了,直接走缓存。
@CachePut——更新时刷新
跟 @Cacheable 的区别:它每次都会执行方法,只是顺手把返回值写回缓存。更新操作用这个:
@CachePut(value = "userCache", key = "#user.id") public User updateUser(User user) { userMapper.updateById(user); return user; }@CacheEvict——删
数据删了、失效了,缓存也得跟着清。不然数据库已经变了,缓存里还是老数据:
@CacheEvict(value = "userCache", key = "#id") public void deleteUser(Long id) { userMapper.deleteById(id); }也可以直接把整个缓存分组清掉:
@CacheEvict(value = "userCache", allEntries = true) public void clearAllUserCache() { }@Caching——组合
一个方法要同时操作多个缓存,用这个拼起来:
@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 包一层代理。调用链路是这样的:
1. 拿 key 去 CacheManager 找缓存
2. 命中→直接返回,原方法不执行
3. 没命中→执行原方法,结果丢进缓存
这也是那个经典坑的来源:同类内部调用,缓存注解直接不生效。
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
// condition:执行前判断,满足才缓存 @Cacheable(value = "userCache", key = "#id", condition = "#id > 0") // unless:执行后判断,满足就不缓存(能拿到返回值 #result) @Cacheable(value = "userCache", key = "#id", unless = "#result == null")unless 特别有用。null 结果不缓存,不然缓存穿透问题会放大。
自定义 key
参数多的时候用 SpEL 拼:
@Cacheable(value = "orderCache", key = "#userId + '_' + #orderId") public Order getOrder(Long userId, Long orderId) { return orderMapper.selectOrder(userId, orderId); }过期时间(Redis)
注解上没法直接设,要在 CacheManager 配置里统一处理:
@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 代理这件事,大部分坑就能绕开。