【Spring Cache | 让接口性能提升】 个人主页:一条泥憨鱼(欢迎各位大佬莅临)精选专栏:数据结构与算法Java ,AI与Agent前言写业务接口的时候这种事情天天发生一个查用户信息的方法每次请求都去摸数据库。接口调用量一大数据库就哐哐扛。问题是——用户信息半天都不变一次这些查询全是白干的。就是把第一次的结果记下来下次直接用。Spring Cache干的就是这个。它是个缓存抽象层。你用注解告诉 Spring「这个方法的返回值可以缓存」具体怎么存、存哪不用管。它不是缓存是缓存的遥控器新手容易搞混Spring Cache 不是 Redis不是 Caffeine不是任何一种具体的缓存技术。它是个统一接口层背后可以接不同的缓存实现- 本地ConcurrentMapCache默认底层是 Map只适合测试、Caffeine正经的高性能本地缓存- 分布式Redis生产环境主力多实例共享关键在于——业务代码不用动。换个 CacheManager 配置就能从本地缓存切到 Redis。解耦这件事才是 Spring Cache 真正值钱的地方。怎么开Spring Boot 项目加两个依赖Redis 为例dependency groupIdorg.springframework.boot/groupId artifactIdspring-boot-starter-data-redis/artifactId /dependency dependency groupIdorg.springframework.boot/groupId artifactIdspring-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 代理这件事大部分坑就能绕开。