【Spring Cache | 让接口性能提升】

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

🎬精选专栏:数据结构与算法Java,AI与Agent

前言:

写业务接口的时候,这种事情天天发生:一个查用户信息的方法,每次请求都去摸数据库。接口调用量一大,数据库就哐哐扛。问题是------用户信息半天都不变一次,这些查询全是白干的。

就是把第一次的结果记下来,下次直接用。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 包一层代理。调用链路是这样的:

  1. 拿 key 去 CacheManager 找缓存

  2. 命中→直接返回,原方法不执行

  3. 没命中→执行原方法,结果丢进缓存

这也是那个经典坑的来源:同类内部调用,缓存注解直接不生效。

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 代理这件事,大部分坑就能绕开。