1. 引言
在 Web 应用开发中,缓存是提升系统性能最直接、最有效的手段之一。Spring 框架从 3.1 版本开始提供了基于注解的缓存抽象(Spring Cache Abstraction),它不依赖任何具体的缓存实现,而是通过统一的注解和 API,让开发者能够以极低的侵入成本为方法添加缓存能力。
本文将带你系统掌握 Spring Cache 的核心概念、常用注解、配置方式以及实战中的最佳实践,帮助你快速在项目中落地缓存方案。
2. Spring Cache 核心概念
Spring Cache 的核心思想是对方法的返回值进行缓存。当调用一个被缓存注解标记的方法时,Spring 会先检查缓存中是否存在对应的数据,如果存在则直接返回缓存结果,不再执行方法体;如果不存在则执行方法,并将返回值存入缓存。
理解下面三个核心接口,就掌握了 Spring Cache 的骨架:
- Cache :缓存接口,定义了缓存的读写、删除等基本操作,如
get、put、evict。 - CacheManager :缓存管理器,负责管理一组
Cache实例,根据缓存名称创建和管理具体的缓存区域。 - KeyGenerator :键生成器,用于根据方法参数生成缓存 key。默认使用
SimpleKeyGenerator,基于方法参数生成 key。
3. 快速开始:环境准备
3.1 引入依赖
以 Maven 为例,在 pom.xml 中添加 Spring Cache 依赖。如果使用 Spring Boot,只需引入 spring-boot-starter-cache:
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
如果使用 Spring 传统项目,则需要引入 spring-context 和 spring-context-support。
3.2 开启缓存支持
在配置类上添加 @EnableCaching 注解,即可开启 Spring Cache 的注解驱动支持:
java
@Configuration
@EnableCaching
public class CacheConfig {
}
3.3 配置缓存管理器
Spring Boot 默认使用 ConcurrentMapCacheManager,无需额外配置即可运行。生产环境通常使用 Redis 或 Caffeine,后面章节会详细介绍。
4. 核心注解详解
4.1 @Cacheable
@Cacheable 是最常用的注解,用于标记一个方法的返回值需要被缓存。当方法被调用时,Spring 会先根据 key 查找缓存,命中则直接返回,未命中则执行方法并缓存结果。
java
@Service
public class UserService {
@Cacheable(value = "user", key = "#id")
public User getUserById(Long id) {
// 模拟耗时查询
return userMapper.selectById(id);
}
}
value/cacheNames:指定缓存名称,可以是一个数组,表示多个缓存区域。key:指定缓存的 key,支持 SpEL 表达式。默认 key 由方法参数决定。condition:满足条件才缓存,如condition = "#id > 10"。unless:满足条件则不缓存,如unless = "#result == null"。
4.2 @CachePut
@CachePut 用于更新缓存,它会始终执行方法,并将返回值写入缓存。适合在数据更新后同步刷新缓存的场景。
java
@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) {
userMapper.updateById(user);
return user;
}
4.3 @CacheEvict
@CacheEvict 用于删除缓存,通常在删除数据或数据失效时使用。
java
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
userMapper.deleteById(id);
}
allEntries = true:清空该缓存区域下的所有数据。beforeInvocation = true:在方法执行前删除缓存,默认在方法执行后删除。
4.4 @Caching
当需要在一个方法上同时应用多个缓存操作时,可以使用 @Caching 组合注解:
java
@Caching(
put = { @CachePut(value = "user", key = "#user.id") },
evict = { @CacheEvict(value = "userList", allEntries = true) }
)
public User saveUser(User user) {
userMapper.insert(user);
return user;
}
4.5 @CacheConfig
@CacheConfig 是类级别注解,用于统一指定该类中缓存注解的公共属性,如缓存名称:
java
@Service
@CacheConfig(cacheNames = "user")
public class UserService {
// 类中的 @Cacheable 无需再写 value
@Cacheable(key = "#id")
public User getUserById(Long id) { ... }
}
5. 缓存 Key 的生成策略
默认情况下,Spring 使用 SimpleKeyGenerator 生成 key:无参方法使用 SimpleKey.EMPTY,单参数方法直接使用该参数,多参数方法使用 SimpleKey 包装所有参数。
实际开发中,推荐使用 SpEL 表达式自定义 key,以更精确地控制缓存粒度:
java
// 使用对象属性作为 key
@Cacheable(value = "user", key = "#user.id")
// 组合多个参数
@Cacheable(value = "order", key = "#userId + ':' + #orderNo")
// 使用方法返回值属性(仅 @CachePut 可用)
@CachePut(value = "user", key = "#result.id")
当默认策略无法满足需求时,可以实现 KeyGenerator 接口自定义 key 生成器:
java
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return target.getClass().getSimpleName() + "_" + method.getName() + "_" + Arrays.toString(params);
}
}
使用时通过 keyGenerator 属性指定:
java
@Cacheable(value = "user", keyGenerator = "customKeyGenerator")
public User getUserById(Long id) { ... }
6. 集成 Redis 缓存
生产环境中,Redis 是最常用的分布式缓存方案。在 Spring Boot 中集成 Redis 缓存非常简单。
6.1 引入依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
6.2 配置 Redis 连接
在 application.yml 中配置 Redis 连接信息:
yaml
spring:
redis:
host: localhost
port: 6379
password:
database: 0
6.3 自定义 RedisCacheManager
为了让缓存值以 JSON 格式存储,并设置合理的过期时间,需要自定义 RedisCacheManager:
java
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
// 配置序列化方式
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
.entryTtl(Duration.ofMinutes(30)) // 默认过期时间 30 分钟
.disableCachingNullValues();
// 为不同缓存名称设置不同的过期时间
Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
cacheConfigurations.put("user", config.entryTtl(Duration.ofHours(1)));
cacheConfigurations.put("order", config.entryTtl(Duration.ofMinutes(10)));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.withInitialCacheConfigurations(cacheConfigurations)
.build();
}
}
7. 集成 Caffeine 本地缓存
Caffeine 是高性能的本地缓存库,适合单机场景或作为多级缓存的一级缓存。
7.1 引入依赖
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
7.2 配置 CaffeineCacheManager
java
@Configuration
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("user", "order");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(500) // 最大缓存条目数
.expireAfterWrite(Duration.ofMinutes(10)) // 写入后 10 分钟过期
.recordStats()); // 开启统计
return cacheManager;
}
}
8. 多级缓存实战
在大型系统中,常采用「本地缓存 + 分布式缓存」的多级缓存架构,兼顾性能与一致性。下面是一个简单的二级缓存示例:
java
@Service
public class ProductService {
@Autowired
private CacheManager cacheManager;
public Product getProduct(Long id) {
// 1. 先查本地缓存(Caffeine)
Cache localCache = cacheManager.getCache("localProduct");
Product product = localCache.get(id, Product.class);
if (product != null) {
return product;
}
// 2. 再查 Redis 缓存
Cache redisCache = cacheManager.getCache("redisProduct");
product = redisCache.get(id, Product.class);
if (product != null) {
localCache.put(id, product);
return product;
}
// 3. 最后查数据库并回填缓存
product = productMapper.selectById(id);
if (product != null) {
redisCache.put(id, product);
localCache.put(id, product);
}
return product;
}
}
9. 常见问题与最佳实践
9.1 缓存穿透
缓存穿透指查询一个不存在的数据,导致请求直接打到数据库。解决方案:
- 缓存空值:在
@Cacheable中设置unless = "#result == null"会跳过缓存,应改为缓存空值并设置较短过期时间。 - 使用布隆过滤器:在缓存前先判断 key 是否存在。
9.2 缓存雪崩
缓存雪崩指大量缓存同时过期,导致请求全部打到数据库。解决方案:
- 设置随机过期时间,避免同时失效。
- 使用多级缓存,本地缓存兜底。
- 限流降级,保护数据库。
9.3 缓存击穿
缓存击穿指某个热点 key 过期瞬间,大量请求同时打到数据库。解决方案:
- 使用互斥锁(如
synchronized、Redisson 分布式锁)保证只有一个线程去查询数据库。 - 热点数据设置永不过期,后台异步更新。
9.4 缓存一致性
当数据更新时,需要保证缓存与数据库的一致性。推荐策略:
- 先更新数据库,再删除缓存(Cache Aside 模式)。
- 对于强一致场景,可使用分布式事务或消息队列异步同步。
9.5 注意事项
@Cacheable基于 Spring AOP 实现,同类内部方法调用不会触发缓存,因为代理不生效。- 缓存注解作用于
public方法,private方法无法被代理。 - 序列化对象必须实现
Serializable接口(使用 JDK 序列化时)。 - 合理设置缓存过期时间,避免数据长期不一致。
10. 总结
Spring Cache 通过简洁的注解抽象,大幅降低了缓存接入的成本。本文从核心概念出发,依次介绍了常用注解、Key 生成策略、Redis 与 Caffeine 集成、多级缓存实战以及常见问题的最佳实践。
在实际项目中,建议根据业务场景选择合适的缓存方案:单机小规模应用可使用 Caffeine,分布式系统优先选择 Redis,追求极致性能可组合使用多级缓存。同时务必关注缓存穿透、雪崩、击穿等经典问题,做好兜底设计,才能真正发挥缓存的威力。