Spring Boot 的缓存注解允许开发者在不修改业务逻辑的情况下,将方法的计算结果缓存起来,从而减少重复计算和数据库查询,提高系统性能。
1、Spring Boot Cache 的基本用法及常用注解
1. 引入依赖
首先,需要在项目中引入缓存相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
2. 启用缓存
在 Spring Boot 主程序类上添加 @EnableCaching
注解,开启缓存支持:
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
3. 常用注解
@Cacheable
- 用于将方法的返回结果缓存起来。
- 方法被调用时,Spring 会先检查缓存中是否有数据,如果有则直接返回缓存结果,否则执行方法并将结果放入缓存。
@Cacheable
本身并不直接提供过期时间的配置。缓存的过期时间取决于具体的缓存提供者。
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id")#根据id不同查找缓存,在同一个缓存空间(如 responseCache)内,可以存储多个结果,每个结果与唯一的缓存键对应(即不同的请求参数对应不同的缓存结果)。
public User getUserById(Long id) {
// 假设这是一个耗时的数据库查询
return userRepository.findById(id).orElse(null);
}
}
注:
-
@Cacheable:表示该方法的结果应该被缓存。Spring 在方法第一次被调用时会执行该方法并缓存其结果,后续对同一参数的调用将直接返回缓存中的结果。
-
value = "userCache":指定缓存的名称,
value
属性对应缓存的命名空间。这里的userCache
是缓存的名称,用于存储此方法的返回结果。userCache
可以对应一个具体的缓存实现(如 Redis、EhCache 等)中一个独立的缓存区域。 -
key = "#id":指定缓存的键。使用 Spring 表达式语言(SpEL)来定义缓存键的生成规则,
#id
表示方法参数id
的值作为缓存的键。- 在这里,
#id
会直接使用方法参数id
的值。例如,当id=1
时,缓存键就是1
。 - 如果不指定
key
属性,Spring 会默认将所有方法参数的组合作为缓存键。
- 在这里,
默认键生成机制
当 @Cacheable
注解没有指定 key
时,Spring Cache 会将所有方法参数的组合作为默认缓存键。具体来说,Spring Cache 使用 SimpleKeyGenerator
生成缓存键:
- 单一参数:如果方法有一个参数,且未指定
key
,Spring 会直接使用该参数作为缓存键。 - 多参数:如果方法有多个参数,Spring 会将它们组合成一个
SimpleKey
对象作为缓存键。 - 无参数:如果方法没有参数,Spring 会使用
SimpleKey.EMPTY
作为缓存键。
如果传入的是自定义对象参数,Spring 会调用该对象的 hashCode
和 equals
方法来识别不同参数值的唯一性,以确保生成的缓存键唯一。对于自定义对象,如果没有重写 equals
和 hashCode
方法,则默认使用 Object
类的实现。Object
的 hashCode
和 equals
方法基于对象的内存地址判断两个对象是否相等,这样不同实例即使内容相同,也会被认为是不同的对象。因此,不重写 equals
和 hashCode
可能会导致缓存命中失败,从而产生重复的缓存条目。
- 不指定
key
且传入自定义对象:需要重写对象的equals
和hashCode
方法,以确保相同内容的对象具有相同的缓存键。 - 推荐方式:如果传入对象属性确定,可以使用 SpEL 表达式明确指定缓存键(如
key = "#user.id"
),这样更简洁并避免了对equals
和hashCode
的依赖。
@CachePut
- 用于强制更新缓存内容,方法每次都会执行,将返回值放入缓存。
- 常用于方法更新数据后,同时更新缓存中的数据。
@Service public class UserService { @CachePut(value = "userCache", key = "#user.id") public User updateUser(User user) { // 更新数据库中的用户信息 return userRepository.save(user); } }
@CachePut
注解表示方法执行完成后将结果更新到userCache
中,key 为user.id
。
@CacheEvict
- 用于清除缓存数据。
- 通常用于删除或更新方法,用于从缓存中移除不再需要的数据。
@Service public class UserService { @CacheEvict(value = "userCache", key = "#id") public void deleteUser(Long id) { // 删除数据库中的用户 userRepository.deleteById(id); } }
@CacheEvict
会将缓存userCache
中 key 为id
的缓存项删除,确保缓存中的数据与数据库保持一致。
@Caching
- 用于组合多个缓存注解,适用于需要同时执行多个缓存操作的场景。
@Service
public class UserService {
@Caching(
put = { @CachePut(value = "userCache", key = "#user.id") },
evict = { @CacheEvict(value = "userListCache", allEntries = true) }
)
public User saveUser(User user) {
// 保存用户信息
return userRepository.save(user);
}
}
在此示例中,@Caching
组合了 @CachePut
和 @CacheEvict
,即在缓存 userCache
中更新用户数据,同时清除 userListCache
中的所有缓存项。
4. 自定义缓存键和条件
Spring Boot Cache 允许通过 key
和 condition
参数自定义缓存键和条件。
- key:自定义缓存的 key,可以使用 SpEL 表达式。默认是方法参数的组合。
- condition:满足指定条件时才缓存,使用 SpEL 表达式。
- unless:满足条件时不缓存,优先级高于
condition
。
@Cacheable(value = "userCache", key = "#user.id", condition = "#user.age > 18", unless = "#result == null")
public User getUser(User user) {
return userRepository.findById(user.getId()).orElse(null);
}
在此例中,condition
表示只有用户年龄大于 18 时才缓存,unless
表示当方法返回值为 null 时不缓存。
5.完整示例
@Service
public class UserService {
@Cacheable(value = "userCache", key = "#id")
public Optional<User> getUserById(Long id) {
// 模拟数据库查询
return userRepository.findById(id);
}
@CachePut(value = "userCache", key = "#user.id")
public User updateUser(User user) {
// 更新数据库中的用户信息
return userRepository.save(user);
}
@CacheEvict(value = "userCache", key = "#id")
public void deleteUser(Long id) {
// 从数据库中删除用户信息
userRepository.deleteById(id);
}
}
缺点:本地缓存, 分布式场景容易产生数据不一致的情况。
2、Spring Boot 结合 Redis 实现缓存
为了解决缓存一致问题,可以引入分布式缓存Redis。
1. 引入依赖
项目中添加 Redis 的依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2. 配置 Redis 连接
在 application.yml
或 application.properties
中配置 Redis 连接信息:
# Spring Cache 配置
spring.cache.type=redis # 设置 Spring 缓存使用 Redis
# Redis 连接配置
spring.redis.host=localhost # Redis 服务器地址
spring.redis.port=6379 # Redis 服务器端口
spring.redis.password=yourpassword # Redis 连接密码,如果没有则省略
# Redis 缓存过期时间(可选)
spring.cache.redis.time-to-live=60000 # 设置缓存的默认过期时间(单位为毫秒),此处为 60 秒
spring.cache.redis.use-key-prefix=true # 使用 key 前缀,果有多种业务数据存储在同一个 Redis 实例中,建议开启 key 前缀
spring.cache.redis.key-prefix=test_cache_ # 设置 Redis 缓存的 key 前缀
3. 启用缓存支持
同上
4. 使用缓存注解
同上
这样即可使用 Redis分布式缓存替换Spring Boot 自带缓存显著提升性能,解决分布式场景下数据不一致问题。
3、引入缓存带来的问题
缓存击穿
定义
缓存击穿是指一个热点数据在缓存过期的瞬间,大量并发请求访问该数据,由于缓存刚好过期,导致请求同时涌入数据库查询。这个问题会导致数据库压力骤增,甚至崩溃。
原因
- 热点数据在缓存失效的瞬间,大量请求同时访问该数据。
- 高并发场景下,单一热点数据的缓存失效后数据库压力过大。
解决方案
- 设置热点数据永不过期(逻辑过期):对于少量非常热门的数据,可以设置缓存永不过期。然后通过后台异步线程来定时更新缓存,避免缓存失效导致的瞬间压力增大。
- 加锁机制:在缓存失效时,只有一个线程去数据库中查询数据并回填缓存,其他线程等待第一个线程完成后从缓存读取。可以使用分布式锁(如 Redis 分布式锁)来控制并发。
- 双重检查:在缓存失效时再进行二次检查。在缓存过期的情况下,多个线程查询缓存后都会去请求数据库,可以在第一次查询数据库后立即更新缓存,再次检查缓存以减少数据库的并发压力。
- 增加定时任务定期刷新缓存:通过 Spring 的定时任务来实现定时刷新缓存,保证热点数据在缓存即将失效时被重新加载。
缓存穿透
定义
缓存穿透是指大量请求查询缓存中不存在的数据,导致请求直接穿透缓存到数据库,给数据库带来极大压力。例如,用户查询一个不存在的用户 ID,由于缓存中没有该数据且数据库也没有结果,每次查询都绕过缓存直接访问数据库。
原因
- 查询的 key 不存在,缓存没有数据,直接查询数据库。
- 恶意攻击或程序错误导致频繁查询不存在的 key。
解决方案
- 缓存空结果:对于数据库查询结果为空的数据,将空结果也缓存一段时间(例如 5 分钟)。这样,短时间内相同的请求不会频繁查询数据库。可以通过设置较短的过期时间来避免大量无效数据长期占用缓存。(spring.cache.redis.cache-null-values=true)
- 布隆过滤器:在缓存之前增加一个布隆过滤器,用于判断 key 是否可能存在。布隆过滤器可以有效过滤掉不存在的数据,避免直接查询数据库。
- 参数校验:对用户输入的 key 进行校验,防止恶意请求。比如查询数据库前对 key 做基础检查,确保格式和范围有效。
缓存雪崩
定义
缓存雪崩是指缓存中大量数据在同一时间失效,导致大量请求同时打到数据库,造成数据库压力激增。缓存雪崩可能会导致系统出现短暂或持续的不可用。
原因
- 大量缓存设置了相同的过期时间,在某一时刻同时失效。
- 系统重启或崩溃,导致所有缓存失效。
解决方案
- 缓存过期时间加随机:避免所有缓存设置相同的过期时间,可以在过期时间上增加一个随机值,使得缓存过期时间分散,避免集中失效。
- 缓存预热:在系统启动时,将常用的热点数据提前加载到缓存中,避免高峰时段突然访问数据库。
- 分级降级策略:在缓存雪崩发生时,可以使用限流策略和降级策略,限制数据库的访问频率。同时,也可以用备用缓存来缓解瞬时压力。
- 多层缓存架构:使用多级缓存(如本地缓存 + 分布式缓存)或多机房缓存,实现分布式缓存冗余,避免单点失效导致大量缓存失效。
总结:
问题 | 定义 | 解决方案 |
---|---|---|
缓存穿透 | 请求大量不存在的 key,导致直接查询数据库 | 缓存空结果、布隆过滤器、参数校验 |
缓存击穿 | 热点数据过期,瞬时大量请求穿透缓存,打到数据库 | 设置热点数据永不过期、加锁机制、双重检查 |
缓存雪崩 | 缓存中大量数据同时失效,导致数据库请求激增 | 设置过期时间加随机、缓存预热、分级降级策略、多层缓存 |