外观
Spring Boot 缓存配置
约 2696 字大约 9 分钟
2025-08-16
一、缓存概述
1. 什么是 Spring Cache
Spring Cache 是 Spring 框架提供的一层缓存抽象,旨在简化应用程序中的缓存管理。
核心价值:
- 简化缓存使用:通过注解方式实现缓存,无需编写重复的缓存代码
- 统一接口:提供统一的缓存操作接口,屏蔽底层缓存实现差异
- 提升性能:减少数据库访问,提高应用响应速度
- 易于切换:可轻松更换底层缓存实现(如从 EhCache 切换到 Redis)
工作原理:
- 通过 AOP 在方法执行前后拦截
- 根据注解配置决定是否使用缓存
- 将方法结果存储到缓存或从缓存获取结果
提示:Spring Cache 是一个抽象层,本身不提供缓存实现,需要配合具体的缓存技术(如 Redis、EhCache)使用。
2. 适用场景
适合使用缓存的场景:
- 读多写少的数据(如配置信息、字典数据)
- 计算成本高的结果(如复杂报表)
- 频繁访问但变化不频繁的数据
- 需要减轻数据库压力的场景
不适合使用缓存的场景:
- 频繁变化的数据
- 实时性要求极高的数据
- 敏感数据(需考虑缓存安全)
- 每次请求结果都不同的场景
二、核心缓存注解
1. @EnableCaching
作用:开启 Spring Cache 功能,必须添加在配置类或启动类上。
使用方式:
@SpringBootApplication
@EnableCaching // 启用缓存功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}提示:这是使用 Spring Cache 的前提,没有这个注解,其他缓存注解不会生效。
2. @Cacheable
作用:标记方法结果应该被缓存。方法执行前先检查缓存,有则直接返回缓存数据;否则执行方法并将结果存入缓存。
常用参数:
| 参数 | 说明 | 示例 |
|---|---|---|
value/cacheNames | 缓存名称(必需) | @Cacheable("users") |
key | 缓存的 key(可选) | @Cacheable(value = "users", key = "#id") |
condition | 缓存条件 | @Cacheable(condition = "#id < 10") |
unless | 不缓存条件 | @Cacheable(unless = "#result == null") |
使用示例:
// 基本用法:缓存所有查询结果
@Cacheable("users")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
// 指定 key:使用方法参数作为 key
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// ...
}
// 条件缓存:只缓存 id < 100 的用户
@Cacheable(value = "users", condition = "#id < 100")
public User getUserById(Long id) {
// ...
}
// 排除 null 值:不缓存 null 结果
@Cacheable(value = "users", unless = "#result == null")
public User getUserById(Long id) {
// ...
}提示:
cacheNames是必填项,key需要用单引号包裹,否则会被识别为 SpEL 变量。
3. @CachePut
作用:无论缓存是否存在,都会执行方法,并将结果放入缓存。通常用于更新操作。
使用场景:
- 数据更新后需要刷新缓存
- 需要保证缓存与数据库一致性
使用示例:
// 更新用户后,将新数据放入缓存
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
return userRepository.save(user);
}提示:与
@Cacheable的区别是@CachePut总是执行方法,并更新缓存,而@Cacheable会先检查缓存。
4. @CacheEvict
作用:清除缓存,通常用于删除或更新操作后使缓存失效。
常用参数:
| 参数 | 说明 | 示例 |
|---|---|---|
allEntries | 是否清除整个缓存区 | @CacheEvict(allEntries = true) |
beforeInvocation | 是否在方法执行前清除 | @CacheEvict(beforeInvocation = true) |
使用示例:
// 删除用户后,清除对应缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
// 更新菜单后,清除整个菜单缓存
@CacheEvict(value = "menu", allEntries = true)
public boolean saveMenu(MenuForm menuForm) {
// 保存菜单逻辑
}
// 在方法执行前清除缓存(避免删除失败后缓存已清除)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUser(Long id) {
// 可能会抛出异常的删除操作
}提示:
beforeInvocation = true可以避免方法执行失败后缓存已被清除的问题。
5. @Caching
作用:组合多个缓存操作到一个方法上。
使用示例:
// 同时执行多个缓存操作
@Caching(
evict = {
@CacheEvict(value = "users", key = "#id"),
@CacheEvict(value = "userStats", allEntries = true)
},
put = {
@CachePut(value = "userDetails", key = "#result.id")
}
)
public User updateUser(User user) {
return userRepository.save(user);
}三、Redis 缓存配置(最常用)
1. 依赖配置
添加依赖:
<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>提示:
spring-boot-starter-cache是 Spring Cache 的核心依赖,spring-boot-starter-data-redis提供 Redis 支持。
2. 基本配置
application.yml:
spring:
redis:
host: localhost
port: 6379
password: # 如果有密码
timeout: 10s
lettuce:
pool:
max-active: 8
max-wait: -1
max-idle: 8
min-idle: 0
cache:
type: redis
redis:
time-to-live: 3600000 # 缓存过期时间,单位毫秒
cache-null-values: true # 缓存null值,防止缓存穿透
use-key-prefix: true # 使用key前缀关键配置说明:
spring.cache.type=redis:指定使用 Redis 作为缓存实现time-to-live:设置缓存默认过期时间cache-null-values:是否缓存 null 值(防止缓存穿透)use-key-prefix:是否使用前缀(默认为cacheNames::key格式)
3. 自定义缓存配置
创建 RedisCacheConfig 类:
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;
@Configuration
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
public class RedisCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置 key 使用 String 序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()));
// 设置 value 使用 JSON 序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));
// 设置默认过期时间
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
// 是否缓存 null 值
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
// 是否使用 key 前缀
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}为什么需要自定义配置:
- 默认使用 JDK 序列化,可读性差
- 默认 key 格式不友好(包含双冒号)
- 需要设置合理的序列化方式
提示:JSON 序列化使缓存数据可读,便于调试和排查问题。
四、实用技巧与最佳实践
1. 缓存键设计
最佳实践:
- 使用有意义的缓存名称(
cacheNames) - 合理设计 key 表达式,避免 key 冲突
- 对于复杂对象,考虑使用其关键属性作为 key
推荐 key 格式:
// 使用方法参数
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) { ... }
// 使用多个参数
@Cacheable(value = "userRoles", key = "#userId + '_' + #roleId")
public UserRole getUserRole(Long userId, Long roleId) { ... }
// 使用对象属性
@Cacheable(value = "users", key = "#user.id")
public User saveUser(User user) { ... }2. 缓存策略选择
常见策略:
| 策略 | 说明 | 适用场景 |
|---|---|---|
| Cache-Aside | 先查缓存,不存在再查DB,然后更新缓存 | 最常用,适合读多写少 |
| Read-Through | 由缓存层负责与DB交互 | 适合缓存层封装 |
| Write-Through | 先更新DB再更新缓存 | 适合写操作不多的场景 |
| Write-Behind | 先更新缓存,异步更新DB | 适合写操作频繁的场景 |
推荐策略:
- 大多数场景使用 Cache-Aside(通过
@Cacheable+@CachePut/@CacheEvict实现) - 对于高一致性要求的场景,考虑使用 Write-Through
- 对于高写入频率场景,考虑使用 Write-Behind
3. 缓存问题解决方案
缓存穿透(大量请求不存在的数据):
- 问题:查询不存在的数据,缓存不命中,请求直达数据库
- 解决方案:
- 缓存空值(设置较短过期时间)
- 布隆过滤器(Bloom Filter)预检
- 参数校验
@Cacheable(value = "users", key = "#id", unless = "#result == null")
@CachePut(value = "users", key = "#id", condition = "#result == null")
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}缓存击穿(热点数据过期瞬间大量请求):
- 问题:热点数据过期时,大量请求同时击穿缓存
- 解决方案:
- 设置热点数据永不过期
- 互斥锁(只允许一个线程重建缓存)
- 随机过期时间
@Cacheable(value = "hotData", key = "#key", sync = true) // sync=true 表示同步重建缓存
public String getHotData(String key) {
// ...
}缓存雪崩(大量缓存同时过期):
- 问题:大量缓存同时过期,导致数据库压力骤增
- 解决方案:
- 设置不同的过期时间(添加随机值)
- 缓存预热
- 高可用架构
# 设置随机过期时间
spring:
cache:
redis:
time-to-live: 3600000 # 基础过期时间
# 实际过期时间 = 3600000 + 随机值五、常见问题与解决方案
1. 缓存不生效问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| 注解不生效 | 未添加 @EnableCaching | 在启动类或配置类添加注解 |
| 方法内部调用 | 自调用问题(AOP失效) | 通过代理对象调用自身方法 |
| 缓存键冲突 | key 设计不合理 | 优化 key 表达式 |
| 序列化问题 | 对象无法序列化 | 实现 Serializable 或自定义序列化 |
自调用问题解决方案:
@Service
public class UserService {
@Autowired
private UserService self; // 注入自身代理
@Cacheable("users")
public User getUser(Long id) {
// ...
}
public User processUser(Long id) {
// 通过代理调用缓存方法
return self.getUser(id);
}
}2. 缓存数据一致性
问题:缓存与数据库数据不一致
解决方案:
设置合理的过期时间:
spring: cache: redis: time-to-live: 1800000 # 30分钟更新时双写:
@CachePut(value = "users", key = "#user.id") public User updateUser(User user) { return userRepository.save(user); }删除缓存而非更新:
@CacheEvict(value = "users", key = "#id") public void deleteUser(Long id) { userRepository.deleteById(id); }使用消息队列最终一致性:
- 更新数据库后发送消息
- 消费者更新/删除缓存
3. 缓存监控
使用 Micrometer 监控:
management:
endpoints:
web:
exposure:
include: cache
metrics:
export:
prometheus:
enabled: true监控指标:
cache.gets.miss:缓存未命中次数cache.gets.hit:缓存命中次数cache.puts:缓存写入次数cache.evictions:缓存驱逐次数
提示:通过监控缓存命中率,可以评估缓存效果和调整缓存策略。
六、实用技巧速查表
| 场景 | 推荐方案 | 注意事项 |
|---|---|---|
| 开启缓存 | @EnableCaching | 必须添加在配置类上 |
| 查询缓存 | @Cacheable | 设置合理的 key 和条件 |
| 更新缓存 | @CachePut | 通常与更新操作配合使用 |
| 删除缓存 | @CacheEvict | 考虑 beforeInvocation 设置 |
| 缓存空值 | cache-null-values: true | 防止缓存穿透 |
| JSON 序列化 | 自定义 RedisCacheConfiguration | 提高可读性 |
| 缓存预热 | 启动时加载热点数据 | 提升系统启动后性能 |
| 缓存监控 | Micrometer + Prometheus | 结合 Grafana 可视化 |
七、最佳实践总结
合理选择缓存策略:
- 读多写少:优先使用缓存
- 实时性要求高:减少缓存时间或使用直写策略
- 数据一致性要求高:考虑使用消息队列保证最终一致性
缓存粒度控制:
- 避免缓存大对象
- 按业务需求设计缓存粒度
- 考虑使用二级缓存(本地缓存 + 分布式缓存)
缓存失效策略:
- 设置合理的过期时间
- 重要数据添加随机过期时间
- 使用
@CacheEvict及时清除过期数据
性能考量:
- 避免在循环中使用缓存注解
- 考虑使用批量操作减少网络开销
- 监控缓存命中率,持续优化
