引言
缓存是高并发架构中提升系统响应速度、降低数据库压力的常规手段,Spring Framework 提供了一套优雅、简洁且高度抽象的缓存机制——Spring Cache,让我们无需编写繁琐的缓存逻辑,仅通过几个注解即可实现方法级别的缓存控制。
本文将从核心原理、常用注解、配置方式以及在实际项目中的最佳实践等多维度对 Spring Cache详解。
一、Spring Cache 是什么?
Spring Cache 并不是一个具体的缓存实现,而是一套缓存抽象层。它定义了一组标准接口和注解,允许开发者以统一的方式接入各种底层缓存框架,如:
- 本地缓存:Caffeine、EhCache、ConcurrentMap
- 分布式缓存:Redis、Hazelcast、Memcached(需适配)
- JSR-107 兼容实现:如 Infinispan
其核心思想是:“缓存方法的返回结果”。当一个被 @Cacheable 注解的方法被调用时,Spring 会根据方法参数生成一个 key,检查缓存中是否存在该 key 对应的值:
- 若存在 → 直接返回缓存值,跳过方法执行
- 若不存在 → 执行方法,将结果存入缓存后返回
这种“透明化”的缓存机制,让业务代码几乎无需关心缓存细节。
二、快速上手:启用 Spring Cache
1. 添加依赖(以 Spring Boot 为例)
<!-- Spring Boot Starter Cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- 可选:具体缓存实现,例如 Caffeine -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
如果不指定具体实现,Spring Boot 会自动配置一个基于 ConcurrentHashMap 的简单缓存(适合开发测试)。
2. 启用缓存支持
在主配置类或启动类上添加 @EnableCaching:
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
至此,Spring Cache 已启用,可以开始使用注解了!
三、核心注解详解
Spring Cache 提供了 6 个核心注解,覆盖缓存的“读、写、删”全生命周期。
1. @Cacheable—— 缓存读取(最常用)
作用:将方法结果缓存,后续一样参数调用直接返回缓存值。
@Cacheable("users")
public User findUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
关键属性:
|
属性 |
说明 |
|
value/ cacheNames |
缓存名称(必须) |
|
key |
SpEL 表达式,自定义缓存 key(默认使用所有参数) |
|
condition |
SpEL 表达式,满足条件才尝试缓存(方法执行前判断) |
|
unless |
SpEL 表达式,满足条件则不缓存结果(方法执行后判断) |
|
sync |
是否同步加载(防止缓存击穿,默认 false) |
示例场景:
// 仅缓存 VIP 用户
@Cacheable(
value = "users",
key = "#id",
condition = "#id > 1000",
unless = "#result == null || !#result.vip"
)
public User findUserById(Long id) { ... }
注意:#result 只能在 unless 中使用,由于此时方法已执行完毕。
2. @CachePut—— 缓存更新
作用:总是执行方法,并将结果写入缓存(用于更新操作)。
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user); // 方法必定会执行
}
与 @Cacheable 不同,@CachePut不会跳过方法体,适用于“先更新 DB,再刷新缓存”的场景。
3. @CacheEvict—— 缓存清除
作用:删除缓存条目。
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
常用属性:
- allEntries = true:清空整个缓存(慎用!)
- beforeInvocation = true:在方法执行前清除缓存(默认为 false,即执行后清除)
// 清空所有用户缓存
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() { ... }
4. @Caching—— 组合操作
当一个方法需要同时进行多个缓存操作时使用:
@Caching(
evict = {
@CacheEvict("users"),
@CacheEvict(value = "userProfiles", key = "#user.id")
},
put = @CachePut(value = "recentUsers", key = "#user.id")
)
public User processUser(User user) {
// 处理逻辑
return user;
}
5. @CacheConfig—— 类级别配置
避免在每个方法上重复写 cacheNames:
@Service
@CacheConfig(cacheNames = "books")
public class BookService {
@Cacheable(key = "#isbn")
public Book getBook(String isbn) { ... }
@CachePut(key = "#book.isbn")
public Book saveBook(Book book) { ... }
}
四、缓存 Key 的生成策略
默认策略
Spring 使用 SimpleKeyGenerator:
- 无参 → SimpleKey.EMPTY
- 单参 → 直接使用该参数
- 多参 → 封装为 SimpleKey 对象
但这种方式在分布式环境下可能不够灵活或可读性差。
自定义 KeyGenerator
@Bean
public KeyGenerator customKeyGenerator() {
return (target, method, params) -> {
StringBuilder sb = new StringBuilder();
sb.append(method.getDeclaringClass().getSimpleName())
.append(".")
.append(method.getName());
for (Object param : params) {
sb.append(":").append(param);
}
return sb.toString(); // 如:BookService.getBook:978-3-16-148410-0
};
}
使用:
@Cacheable(value = "books", keyGenerator = "customKeyGenerator")
public Book getBook(String isbn) { ... }
五、集成主流缓存实现
1. 本地缓存:Caffeine(推荐)
高性能、内存友善的 Java 本地缓存库。
@Configuration
@EnableCaching
public class CaffeineCacheConfig {
@Bean
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "books");
cacheManager.setCaffeine(Caffeine.newBuilder()
.maximumSize(1000) // 最大条目数
.expireAfterWrite(Duration.ofMinutes(10)) // 写入后10分钟过期
.recordStats() // 开启统计(用于监控)
);
return cacheManager;
}
}
2. 分布式缓存:Redis
适用于集群环境,支持高可用和持久化。
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) // TTL 1小时
.disableCachingNullValues() // 不缓存 null(可选)
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
return RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
}
注意:序列化方式影响兼容性和安全性,提议使用 JSON 而非 JDK 原生序列化。
六、高级特性与实战技巧
1. 防缓存穿透:缓存 null 值
当查询一个不存在的 ID 时,若不缓存 null,每次都会穿透到 DB。
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {
return userRepository.findById(id).orElse(null);
}
但需注意:null 值也应设置较短 TTL,避免长期占用缓存空间。
2. 防缓存击穿:sync = true
热点 key 过期瞬间,大量请求涌入 DB。
@Cacheable(value = "hotData", key = "#id", sync = true)
public Data loadHotData(String id) {
// 耗时操作
}
sync = true 保证同一 key 的并发请求中,只有一个线程执行方法,其余等待结果。
3. 防缓存雪崩:随机 TTL
大量 key 同时过期导致 DB 压力骤增。
// 在 Caffeine 或 Redis 配置中加入随机过期时间
.expireAfterWrite(Duration.ofMinutes(10 + new Random().nextInt(5)))
七、事务与缓存的一致性问题
重大警告:Spring Cache 不是事务感知的!
思考以下场景:
@Transactional
@Cacheable("users")
public User createUser(User user) {
return userRepository.save(user); // 事务中
}
如果事务回滚,但缓存已写入,就会导致缓存与数据库不一致。
推荐做法:Cache-Aside 模式
- 读操作:先查缓存 → 未命中查 DB → 回填缓存
- 写操作:先更新 DB → 再删除缓存(而非更新)
public User updateUser(User user) {
// 1. 更新数据库
User saved = userRepository.save(user);
// 2. 删除缓存(下次读时自动重建)
// 注意:这里不能用 @CachePut,由于事务可能未提交
redisTemplate.delete("users::" + user.getId());
return saved;
}
更严谨的做法是使用“延迟双删”或消息队列保证最终一致性。
八、监控与调试
开启缓存日志
logging.level.org.springframework.cache=DEBUG
输出示例:
DEBUG o.s.c.i.CacheInterceptor - Computed cache key "123" for operation ...
DEBUG o.s.c.i.CacheInterceptor - No cache entry for key "123" in cache(s) [users]
Caffeine 统计信息
启用 .recordStats() 后,可通过 JMX 或 Actuator 查看命中率、加载次数等。
九、生产环境最佳实践总结
|
问题 |
解决方案 |
|
缓存穿透 |
缓存 null 值 + 短 TTL + 布隆过滤器(可选) |
|
缓存击穿 |
@Cacheable(sync = true) 或互斥锁 |
|
缓存雪崩 |
设置随机 TTL,避免聚焦过期 |
|
数据一致性 |
采用 Cache-Aside 模式,写 DB 后删缓存 |
|
序列化安全 |
Redis 中避免使用 JDK 序列化,改用 JSON |
|
缓存命名 |
使用清晰的命名空间,如 user:profile:{id} |
|
容量规划 |
本地缓存设最大 size,Redis 设内存上限和淘汰策略 |
十、结语
Spring Cache 通过注解极大简化了缓存集成,但需注意:
- 选择合适的底层缓存(本地 or 分布式)
- 合理设计 key 和过期策略
- 处理缓存穿透/击穿/雪崩
- 保证缓存与数据库的一致性
- 在事务环境中谨慎使用
掌握这些,你才能高效、安全地使用 Spring Cache 提升系统性能。


