配置与优化Spring的缓存抽象,提升系统的缓存命中率与性能
一、Spring 缓存抽象概述

Spring 的缓存抽象为开发者提供了一种强大而灵活的缓存管理方式。它从 3.1 版本开始为 Spring 应用程序提供透明式添加缓存的支持,从 4.1 版本开始支持 JSR-107 注释和更多自定义选项,得到了显著改进。
Spring 的缓存抽象通过解耦具体的缓存实现,如 Ehcache、Hazelcast、Redis 等,让开发者使用统一的接口进行交互,无需直接与底层缓存实现耦合。它定义了一系列注解和接口,其中核心注解包括 @Cacheable、@CacheEvict、@CachePut、@Caching 和 @CacheConfig。
@Cacheable 用于标识可缓存的方法,在方法执行前检查缓存中是否有现存结果,如果有则直接返回,否则执行方法并将结果存入缓存。它可以指定一个或多个缓存名称,当指定多个缓存时,会在执行方法前检查每个缓存,若有命中则返回缓存值,且即使未实际执行方法,其他不包含该值的缓存也会被更新。
@CacheEvict 用于从缓存中移除条目。@CachePut 确保方法总是被执行,并且将结果放入缓存。@Caching 可以组合多个缓存操作到一个方法上。@CacheConfig 则在类级别共享缓存的相关设置。
要在 Spring 应用中启用缓存抽象,需要按照以下步骤进行:
- 添加依赖:确保构建配置中有缓存抽象所需要的依赖,如 Spring Boot 启动器 spring-boot-starter-cache。
- 启用缓存:使用 @EnableCaching 注解来启用缓存支持。
- 配置缓存管理器:根据使用的缓存库,配置一个或多个缓存管理器(CacheManager)。
- 使用缓存注解:将缓存注解应用于希望缓存的方法上。
例如,以下是一个使用 Spring 缓存抽象的例子:
@Configuration
@EnableCaching
public class CachingConfig {
@Bean
public CacheManager cacheManager() {
return new ConcurrentMapCacheManager("books");
}
}
@Service
public class BookService {
@Cacheable("books")
public Book findBook(ISBN isbn) {
// simulate slow service call
return expensiveMethodToFindBook(isbn);
}
}
在这个例子中,CachingConfig 类通过 @EnableCaching 注解启用了缓存,并配置了一个使用 ConcurrentHashMap 作为存储的简单缓存管理器。BookService 类中的 findBook 方法使用 @Cacheable 注解,以便将查询结果缓存起来,下次查询相同的 ISBN 时可以快速获取。
使用 Spring 缓存抽象时还需要注意一些事项:
- 避免将缓存放在事务性方法上,因为缓存操作通常不是事务性的,可能导致不一致的行为。
- @Cacheable 和 @CachePut 可以指定多个缓存,通过在注解中提供缓存名称的数组来实现。
- 使用 key 属性来定义复杂的缓存键。
- 缓存抽象不直接提供缓存内容的序列化与反序列化机制,需要确保选择的基础缓存提供者支持。
通过 Spring 的缓存抽象,开发者可以轻松地为提高应用性能而在服务层添加缓存,同时保持代码的清晰度和可维护性。
二、基于声明式注解的 Spring Cache 使用案例
1. 代码上下文环境准备
为了在 Spring 中基于声明式注解使用 Redis 作为缓存,需要进行以下准备工作。
引入 Redis 和相关依赖:添加spring-boot-starter-data-redis和spring-boot-starter-cache依赖,同时可以添加commons-pool2依赖以支持 Spring 2.x 集成 Redis 所需的 common-pool2。例如:
<!--redis-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!--spring2.x集成redis所需common-pool2-->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-pool2</artifactId>
<version>2.6.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
设置配置文件中的 cache 类型为 redis,并添加配置类:在配置文件(如application.properties)中设置spring.cache.type=redis,指定缓存类型为 Redis。同时,可以配置 Redis 的连接信息和其他相关参数,如spring.redis.host、spring.redis.port等。
添加配置类,使用@Configuration和@EnableCaching注解来开启缓存功能。例如:
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {
/**
* 配置文件的配置没有用上
* 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
* public class CacheProperties
* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class)
*/
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置key的序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 设置value的序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//将配置文件中所有的配置都生效
if(redisProperties.getTimeToLive()!=null){
config = config.entryTtl(redisProperties.getTimeToLive());
}
if(redisProperties.getKeyPrefix()!=null){
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if(!redisProperties.isCacheNullValues()){
config = config.disableCachingNullValues();
}
if(!redisProperties.isUseKeyPrefix()){
config = config.disableKeyPrefix();
}
return config;
}
}
2. @Cacheable 注解
@Cacheable注解用于标记方法,其返回值会被缓存。通过配置文件可以设置 RedisCacheConfiguration,并指定自定义序列化方式。
当使用@Cacheable注解标记一个方法时,Spring 会在方法执行前检查缓存中是否有现存结果。如果有,则直接返回缓存中的值,否则执行方法并将结果存入缓存。
例如,可以在方法上添加@Cacheable注解来实现缓存功能:
@Service
public class SomeService {
@Cacheable(cacheNames = "someCache", key = "#param")
public Result someMethod(Param param) {
// 实际的方法逻辑
return calculateResult(param);
}
}
在上述代码中,someMethod方法的返回值会被缓存到名为someCache的缓存中,缓存的键由参数param决定。
通过配置 RedisCacheConfiguration,可以指定自定义的序列化方式,替代默认的 JDK 序列化机制。例如,可以使用 Jackson2JsonRedisSerializer 来序列化缓存的值,以便在 Redis 中更方便地查看和管理缓存数据:
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置key的序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 设置value的序列化为 Jackson2JsonRedisSerializer
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new Jackson2JsonRedisSerializer(Object.class)));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 将配置文件中所有的配置都生效
if(redisProperties.getTimeToLive()!=null){
config = config.entryTtl(redisProperties.getTimeToLive());
}
if(redisProperties.getKeyPrefix()!=null){
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if(!redisProperties.isCacheNullValues()){
config = config.disableCachingNullValues();
}
if(!redisProperties.isUseKeyPrefix()){
config = config.disableKeyPrefix();
}
return config;
}
三、结合 Redis 和 Spring Cache 的最佳实践
1. 添加依赖
为了构建基于 Redis 和 Spring Cache 的分布式缓存方案,需要添加相应的依赖。具体来说,需要在项目的构建文件(如 Maven 的 pom.xml 文件)中添加 Redis 和 Spring Cache 的依赖。以下是添加依赖的步骤:
- 添加spring-boot-starter-data-redis依赖,它会自动引入 Spring Data Redis 和相关的库,用于与 Redis 服务器进行交互。
- 添加spring-boot-starter-cache依赖,为 Spring 应用程序提供透明式添加缓存的支持。
例如:
<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>
2. 配置 Redis 连接
在配置文件中设置 Redis 的连接信息是构建分布式缓存方案的关键步骤之一。通常,在 Spring Boot 应用中,可以在application.properties或application.yml文件中进行配置。
- 设置spring.cache.type=redis,指定缓存类型为 Redis。
- 配置 Redis 的连接信息,如spring.redis.host指定 Redis 服务器的主机地址,spring.redis.port指定端口号。如果 Redis 设置了密码,还可以通过spring.redis.password进行配置。
例如:
spring.cache.type=redis
spring.redis.host=localhost
spring.redis.port=6379
# 如果 Redis 设置了密码,添加以下配置
# spring.redis.password=yourpassword
3. 配置缓存管理器
在配置类中配置缓存管理器可以设置缓存的各种属性,如缓存过期时间、禁止缓存 null 值等。以下是配置缓存管理器的步骤:
- 创建一个配置类,并使用@Configuration注解进行标记。
- 在配置类中添加@EnableCaching注解以启用缓存支持。
- 通过定义一个RedisCacheConfiguration对象来配置 Redis 缓存的属性,如序列化方式、缓存过期时间等。
- 使用RedisCacheManager.builder创建一个RedisCacheManager对象,并传入RedisConnectionFactory和配置好的RedisCacheConfiguration。
例如:
@Configuration
@EnableCaching
public class MyCacheConfig {
@Bean
public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// 设置key的序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// 设置value的序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
//将配置文件中所有的配置都生效
if(redisProperties.getTimeToLive()!=null){
config = config.entryTtl(redisProperties.getTimeToLive());
}
if(redisProperties.getKeyPrefix()!=null){
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if(!redisProperties.isCacheNullValues()){
config = config.disableCachingNullValues();
}
if(!redisProperties.isUseKeyPrefix()){
config = config.disableKeyPrefix();
}
return config;
}
}
4. 使用 Spring Cache 注解
在需要缓存的方法上使用注解进行缓存操作可以方便地实现缓存功能。Spring Cache 提供了几个注解,如@Cacheable、@CachePut、@CacheEvict等。
- @Cacheable用于标记可缓存的方法,在方法执行前检查缓存中是否有现存结果,如果有则直接返回,否则执行方法并将结果存入缓存。可以指定缓存名称和键生成策略。
- @CachePut确保方法总是被执行,并且将结果放入缓存。
- @CacheEvict用于从缓存中移除条目。
例如:
@Service
public class SomeService {
@Cacheable(cacheNames = "someCache", key = "#param")
public Result someMethod(Param param) {
// 实际的方法逻辑
return calculateResult(param);
}
}
5. 缓存配置选项
通过配置项可以进一步优化缓存方案。以下是一些常见的缓存配置选项:
- 设置缓存过期时间:可以在配置文件中通过spring.cache.redis.time-to-live属性设置缓存的过期时间,也可以在配置类中通过RedisCacheConfiguration.entryTtl方法设置。
- 缓存空值:可以在配置文件中通过spring.cache.redis.cache-null-values属性设置是否缓存空值,也可以在配置类中通过RedisCacheConfiguration.disableCachingNullValues方法设置。
- 使用缓存前缀:可以在配置文件中通过spring.cache.redis.key-prefix属性设置缓存的键前缀,也可以在配置类中通过RedisCacheConfiguration.prefixKeysWith方法设置。
例如:
spring.cache.type=redis
spring.cache.redis.time-to-live=360000 # 设置缓存过期时间为 1 小时
spring.cache.redis.key-prefix=CACHE_ # 设置缓存键前缀
spring.cache.redis.use-key-prefix=true # 开启使用键前缀
spring.cache.redis.cache-null-values=true # 缓存空值
四、Caffeine 与 Redis 分层缓存架构
1. 缓存层次设计
在构建缓存解决方案时,通常采用分层缓存设计模式。将本地缓存(如 Caffeine)作为一级缓存,远程缓存(如 Redis)作为二级缓存。这样的设计可以充分利用不同缓存的优势。Caffeine 作为本地缓存,提供极高的读写性能,能够快速响应频繁访问的数据请求。Redis 作为远程缓存,提供更大的存储容量和更好的数据共享性,适合存储需要在多个节点间共享的数据。
2. Caffeine 本地缓存集成
添加 Caffeine 依赖:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
配置 Caffeine CacheManager:
@Configuration
public class CacheConfig {
@Bean
public CacheManager caffeineCache() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterAccess(10, TimeUnit.MINUTES));
return cacheManager;
}
}
3. Redis 远程缓存集成
添加 Redis 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
配置 Redis 连接:
spring.redis.host: 192.168.1.100
spring.redis.port: 6379
配置 Redis CacheManager:
@Configuration
public class CacheConfig {
@Bean
public CacheManager redisCache(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory);
return redisCacheManager;
}
}
4. 分层缓存应用
有了 Caffeine 和 Redis 的配置,我们就可以在业务代码中使用分层缓存架构。以获取用户数据为例:
@Service
public class UserService {
@Autowired
private CacheManager caffeineCache;
@Autowired
private CacheManager redisCache;
@Cacheable(cacheManager = "caffeineCache", cacheNames = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 1. 尝试从 Caffeine 缓存中获取
User user = caffeineCache.getCache("users").get(id, User.class);
if (user!= null) {
return user;
}
// 2. 尝试从 Redis 缓存中获取
user = redisCache.getCache("users").get(id, User.class);
if (user!= null) {
// 将数据写入 Caffeine 缓存
caffeineCache.getCache("users").put(id, user);
return user;
}
// 3. 缓存未命中,从数据库获取并写入缓存
user = fetchFromDatabase(id);
caffeineCache.getCache("users").put(id, user);
redisCache.getCache("users").put(id, user);
return user;
}
}
5. 缓存策略设计与优化
- 过期策略:对于高频访问、变化不频繁的数据,可以设置较长的过期时间,提高命中率;对于低频访问、变化频繁的数据,可以设置较短的过期时间,避免数据不一致。
- 缓存回写:当数据发生变更时,需要及时更新缓存中的数据。可以使用 @CacheEvict 和 @CachePut 注解,或手动编码回写逻辑。
- 缓存防雪崩与穿透:可以使用互斥锁、布隆过滤器等策略,解决缓存雪崩和缓存穿透问题。
- 监控与调优:持续监控缓存命中率、缓存大小等指标,并根据实际情况对缓存策略进行调优。
自定义组合缓存除了使用 Spring 提供的缓存实现外,我们还可以自定义组合缓存,将 Caffeine 和 Redis 进一步结合,形成更加灵活的分层缓存架构。
public class CombinedCacheManager implements CacheManager {
private CaffeineCache caffeineCache;
private RedisCache redisCache;
public Object getFromCache(String key) {
// 先从 Caffeine 缓存中获取
Object value = caffeineCache.get(key);
if (value!= null) {
return value;
}
// 从 Redis 缓存中获取
value = redisCache.get(key);
if (value!= null) {
// 将值写入 Caffeine 缓存
caffeineCache.put(key, value);
}
return value;
}
// 其他方法...
}
通过自定义组合缓存,我们可以更加灵活地控制缓存策略,以满足不同的业务需求。
总结在 Spring 应用中,通过结合使用 Caffeine 和 Redis 缓存,我们可以构建出高效、可扩展的分层缓存架构。同时,合理设计缓存策略、防范常见的缓存问题,并持续进行监控和调优,是获得最佳缓存效果的关键。相信通过本文的介绍,您已经对 Spring 缓存实践有了更深入的理解。如有任何疑问或建议,欢迎留言探讨。
五、提升 Spring 缓存命中率的技巧
1. 确定待缓存的对象
在提升 Spring 缓存命中率的过程中,明确适合缓存的对象特征至关重要。适合缓存的对象通常具有以下特点:
- 频繁访问:经常被访问和重复访问的数据是良好的缓存候选对象,例如用户频繁查询的商品信息、热门文章等。
- 代价高昂获取:需要大量时间或计算资源来检索或处理的数据适合缓存。比如复杂的数据库查询、网络服务调用或耗时的计算结果。
- 静态变化少:变化不频繁的数据能确保缓存的数据在较长时间内保持有效。像配置信息、静态资源等。
- 高读写比率:当数据被访问的频率远高于修改或更新的频率时,可以有效地进行缓存。这保证了缓存快速读取的优势超过其更新成本。
- 可预测访问模式:遵循可预测访问模式的数据允许更高效的缓存管理。例如按照特定时间周期被访问的数据。
2. 缓存过期淘汰策略
缓存过期淘汰策略对于优化缓存性能至关重要。Spring 缓存提供了多种过期淘汰策略:
基于时间的过期策略:
- 定义缓存条目的生存时间(TTL)在不同缓存提供者中有所不同。例如,在 Spring Boot 应用程序中使用 Redis 进行缓存时,可以通过以下配置指定生存时间:spring.cache.redis.time-to-live=10m。如果缓存提供者不支持 TTL,可以使用@CacheEvict注解和调度器来实现,例如:
@CacheEvict(value = "cache1", allEntries = true)
@Scheduled(fixedRateString = "${your.config.key.for.ttl.in.milli}")
public void emptyCache1() {
// 刷新缓存,这里无需编写任何代码,除了描述性日志!
}
自定义淘汰策略:
- 通过根据事件或情况为单个缓存条目或所有条目定义自定义过期策略,可以防止缓存污染并保持其一致性。Spring Boot 具有多种注解来支持自定义过期策略:
- @CacheEvict:从缓存中删除一个或所有条目。
- @CachePut:用新值更新条目。
- CacheManager:可以使用 Spring 的CacheManager和Cache接口实现自定义淘汰策略。可以使用evict()、put()或clear()等方法进行操作,还可以通过getNativeCache()方法访问底层缓存提供者,以获得更多功能。
常见的淘汰策略包括:
- LRU(Least Recently Used):优先淘汰最近最少访问的对象。
- LFU(Least Frequently Used):优先淘汰访问频率最低的对象。
- FIFO(First In First Out):优先淘汰最早放入缓存的对象。虽然 Spring Cache 抽象本身不直接支持这些淘汰策略,但可以根据所选的缓存提供者使用其特定配置。
3. 条件缓存
条件缓存确保只有符合特定条件的数据才会存储在缓存中,这可以防止缓存中存储不必要的数据,从而优化资源利用。
@Cacheable和@CachePut注解都具有condition和unless属性,允许我们为缓存项定义条件:
- condition:指定一个 SpEL(Spring 表达式语言)表达式,该表达式必须评估为true,数据才会被缓存(或更新)。例如:
@Cacheable(cacheNames = "dept", key = "#dname", condition = "#dname.length()>3")
public String getLocByDname(@RequestParam("dname") String dname) {
// key动态参数
QueryWrapper<Dept> queryMapper = new QueryWrapper<>();
queryMapper.eq("dname", dname);
Dept dept = deptService.getOne(queryMapper);
return dept.getLoc();
}
- unless:指定一个 SpEL 表达式,该表达式必须评估为false,数据才会被缓存(或更新)。例如:
@Cacheable(cacheNames = "dept", key = "#dname", unless = "#result.code!= 200")
public Result<Dept> getDeptByDname(@RequestParam("dname") String dname) {
// key动态参数
QueryWrapper<Dept> queryMapper = new QueryWrapper<>();
queryMapper.eq("dname", dname);
Dept dept = deptService.getOne(queryMapper);
if (dept == null) return ResultUtil.error(120, "dept is null");
else return ResultUtil.success(dept);
}
我们也可以使用CacheManager和Cache接口以编程方式实现条件缓存,这提供了更多的灵活性和对缓存行为的控制。
4. 分布式缓存与本地缓存
理解两者差异:
- 谈到缓存,我们通常会想到分布式缓存,如 Redis、Memcached 或 Hazelcast。在微服务架构盛行的时代,本地缓存也在提升应用性能方面发挥了重要作用。
- 分布式缓存是指应用在分布式系统中的缓存,所谓分布式系统是指将一套服务器部署到多台服务器上,并且通过负载分发将用户的请求按照一定规则分发到不同的服务器上。但是在众多的服务器上,他们使用的缓存是同一个缓存,也就是其中任何一台修改了缓存中的数据,其他服务器也是可以感应知道的。例如 Redis 作为分布式缓存,支持数据持久化、多种数据结构存储、有专业团队维护,并且具有响应快速、支持多种数据类型、操作都是原子的、MultiUtility 工具等特点。
- 本地缓存也叫单机缓存,也就是说可以应用在单机环境下的缓存,简单来说就是部署到一个服务器上。本地缓存的特征是只适用于当前系统,访问速度快,但无法进行大数据存储;集群的数据更新问题需要同步更新不同部署节点的本地缓存的数据来保证数据一致性,复杂度较高并且容易出错;数据随应用进程的重启而丢失。本地缓存一般适合于缓存只读数据,如统计类数据。或者每个部署节点独立的数据,如长连接服务中,每个连接的数据都是独立的,并且随着连接的断开而删除。
选择合适的策略优化缓存:
- 在微服务架构中,可以采用分层缓存设计模式。将本地缓存(如 Caffeine)作为一级缓存,远程缓存(如 Redis)作为二级缓存。这样的设计可以充分利用不同缓存的优势。Caffeine 作为本地缓存,提供极高的读写性能,能够快速响应频繁访问的数据请求。Redis 作为远程缓存,提供更大的存储容量和更好的数据共享性,适合存储需要在多个节点间共享的数据。
- 配置本地缓存 Caffeine:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
@Configuration
public class CacheConfig {
@Bean
public CacheManager caffeineCache() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterAccess(10, TimeUnit.MINUTES));
return cacheManager;
}
}
- 配置远程缓存 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring.redis.host: 192.168.1.100
spring.redis.port: 6379
@Configuration
public class CacheConfig {
@Bean
public CacheManager redisCache(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory);
return redisCacheManager;
}
}
- 在业务代码中使用分层缓存架构:
@Service
public class UserService {
@Autowired
private CacheManager caffeineCache;
@Autowired
private CacheManager redisCache;
@Cacheable(cacheManager = "caffeineCache", cacheNames = "users", key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 1. 尝试从 Caffeine 缓存中获取
User user = caffeineCache.getCache("users").get(id, User.class);
if (user!= null) {
return user;
}
// 2. 尝试从 Redis 缓存中获取
user = redisCache.getCache("users").get(id, User.class);
if (user!= null) {
// 将数据写入 Caffeine 缓存
caffeineCache.getCache("users").put(id, user);
return user;
}
// 3. 缓存未命中,从数据库获取并写入缓存
user = fetchFromDatabase(id);
caffeineCache.getCache("users").put(id, user);
redisCache.getCache("users").put(id, user);
return user;
}
}
- 缓存策略设计与优化:
- 过期策略:对于高频访问、变化不频繁的数据,可以设置较长的过期时间,提高命中率;对于低频访问、变化频繁的数据,可以设置较短的过期时间,避免数据不一致。
- 缓存回写:当数据发生变更时,需要及时更新缓存中的数据。可以使用 @CacheEvict 和 @CachePut 注解,或手动编码回写逻辑。
- 缓存防雪崩与穿透:可以使用互斥锁、布隆过滤器等策略,解决缓存雪崩和缓存穿透问题。
- 监控与调优:持续监控缓存命中率、缓存大小等指标,并根据实际情况对缓存策略进行调优。
通过自定义组合缓存,我们可以更加灵活地控制缓存策略,以满足不同的业务需求。
public class CombinedCacheManager implements CacheManager {
private CaffeineCache caffeineCache;
private RedisCache redisCache;
public Object getFromCache(String key) {
// 先从 Caffeine 缓存中获取
Object value = caffeineCache.get(key);
if (value!= null) {
return value;
}
// 从 Redis 缓存中获取
value = redisCache.get(key);
if (value!= null) {
// 将值写入 Caffeine 缓存
caffeineCache.put(key, value);
}
return value;
}
// 其他方法...
}
六、提高 Spring 缓存性能的技巧
1. 自定义注解
在 Spring 中,定义自定义缓存注解可以简化代码,减少重复使用缓存注解的工作量。通过自定义注解,可以根据特定的业务需求来定制缓存行为,提高代码的可读性和可维护性。
例如,可以定义一个类似于 Spring 的@Cacheable注解的自定义注解,用于标记需要缓存的方法。以下是一个简单的自定义缓存注解的示例:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface CustomCacheable {
String value() default "";
String key() default "";
int expireInSeconds() default 0;
}
在这个示例中,CustomCacheable注解定义了三个属性:value表示缓存的名称,key用于指定缓存的键,expireInSeconds表示缓存的过期时间(以秒为单位)。
使用自定义注解时,可以在需要缓存的方法上添加该注解,并指定相应的属性值。例如:
@Service
public class SomeService {
@CustomCacheable(value = "someCache", key = "#param", expireInSeconds = 60)
public Result someMethod(Param param) {
// 实际的方法逻辑
return calculateResult(param);
}
}
在上述代码中,someMethod方法被CustomCacheable注解标记,指定了缓存的名称为someCache,缓存的键由参数param决定,并且缓存的过期时间为 60 秒。
通过自定义注解,可以更加灵活地控制缓存的行为,满足不同的业务需求。同时,自定义注解也可以与 AOP(面向切面编程)结合使用,实现更加复杂的缓存逻辑。
2. 切换缓存实现
在 Spring 中,可以配置多个CacheManager,以便灵活地选择不同的缓存实现。常见的缓存实现包括 Redis、ConcurrentMap、Ehcache 等。通过配置多个CacheManager,可以根据不同的业务场景和性能需求,选择最合适的缓存实现。
以下是配置多个CacheManager并指定使用的CacheManager的步骤:
- 添加不同缓存实现的依赖:
- 如果要使用 Redis 作为缓存实现,需要添加spring-boot-starter-data-redis依赖。
- 如果要使用 ConcurrentMap 作为缓存实现,无需添加额外的依赖,因为 Spring Boot 自带了ConcurrentMapCacheManager。
- 如果要使用 Ehcache 作为缓存实现,需要添加ehcache依赖。
- 配置多个CacheManager:
- 配置 RedisCacheManager:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(3600));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
- 配置 ConcurrentMapCacheManager:
@Configuration
public class ConcurrentMapCacheConfig {
@Bean
public CacheManager concurrentMapCacheManager() {
return new ConcurrentMapCacheManager();
}
}
- 配置 EhcacheCacheManager:
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
@Configuration
public class EhcacheCacheConfig {
@Bean
public EhCacheCacheManager ehCacheCacheManager() {
EhCacheManagerFactoryBean factoryBean = new EhCacheManagerFactoryBean();
factoryBean.setConfigLocation(new ClassPathResource("ehcache.xml"));
factoryBean.afterPropertiesSet();
return new EhCacheCacheManager(factoryBean.getObject());
}
}
- 在需要使用缓存的地方指定使用的CacheManager:
- 使用@Cacheable注解指定cacheManager属性:
@Service
public class SomeService {
@Cacheable(value = "someCache", cacheManager = "redisCacheManager", key = "#param")
public Result someMethod(Param param) {
// 实际的方法逻辑
return calculateResult(param);
}
}
在上述代码中,someMethod方法被@Cacheable注解标记,指定了缓存的名称为someCache,使用的CacheManager为redisCacheManager,缓存的键由参数param决定。
通过配置多个CacheManager,可以根据不同的业务需求灵活地选择不同的缓存实现,提高系统的性能和可扩展性。同时,也可以根据实际情况动态地切换缓存实现,以满足不同的运行环境和性能要求。
七、Spring 缓存优化提升性能
1. 缓存预热与清除
缓存预热是一种在应用程序启动或缓存失效之后,主动将热点数据加载到缓存中的策略。其作用和目的主要有以下几点:
- 提高缓存命中率:通过预先加载热点数据,使得在实际请求到达时,缓存中已有这些数据,从而提高缓存的命中率,减少对后端数据源(如数据库)的访问,降低系统的负载。
- 保持性能稳定:在应用程序启动或缓存失效后进行预热,可以防止请求对后端数据源产生突然的压力,保持应用程序的性能稳定。
- 优化用户体验:由于热点数据已在缓存中,用户在请求这些数据时能获得更快的响应速度,提高用户体验。
缓存清除则是在数据发生变化时删除或更新缓存中的相关数据的策略。其作用和目的包括:
- 保持数据一致性:当数据发生变化时,清除缓存可以确保缓存中的数据与数据源保持一致,避免因缓存数据过期或错误而导致的应用程序错误。
- 释放缓存空间:缓存空间是有限的,清除不再需要的数据可以为新的数据访问腾出空间。
- 提高缓存利用率:通过删除过期或不常用的数据,确保缓存中的数据是最有价值的,从而提高缓存的利用率。
- 避免脏数据:在某些情况下,缓存中的数据可能因为程序错误、系统故障等原因而变得不可靠。缓存清除可以定期或根据需要清理这些脏数据,确保缓存中的数据是有效的。
2. 多级缓存
Spring 可以配置多级缓存,如本地缓存和分布式缓存的组合,使用 @CacheConfig 注解在类级别配置默认缓存配置。
在实际应用中,可以将本地缓存(如 Caffeine)作为一级缓存,远程缓存(如 Redis)作为二级缓存。这样的设计可以充分利用不同缓存的优势。Caffeine 作为本地缓存,提供极高的读写性能,能够快速响应频繁访问的数据请求。Redis 作为远程缓存,提供更大的存储容量和更好的数据共享性,适合存储需要在多个节点间共享的数据。
配置多级缓存的步骤如下:
- 添加依赖:添加 Caffeine 和 Redis 的依赖。例如:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置本地缓存 Caffeine:
@Configuration
public class CacheConfig {
@Bean
public CacheManager caffeineCache() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterAccess(10, TimeUnit.MINUTES));
return cacheManager;
}
}
- 配置远程缓存 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring.redis.host: 192.168.1.100
spring.redis.port: 6379
@Configuration
public class CacheConfig {
@Bean
public CacheManager redisCache(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory);
return redisCacheManager;
}
}
将 Caffeine 和 Redis 的 CacheManager 结合起来:
@Configuration
public class CacheConfig {...
@Bean
public CompositeCacheManager compositeCacheManager(CaffeineCacheManager caffeineCacheManager,
RedisCacheManager redisCacheManager) {
CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
compositeCacheManager.setCacheManagers(Arrays.asList(caffeineCacheManager, redisCacheManager));
compositeCacheManager.setFallbackToNoOpCache(false); // 关闭缓存未命中时自动创建的空缓存
return compositeCacheManager;
}
...
}
3. 缓存监控与优化
为了提升 Spring 缓存的性能,可以采取以下措施:
- 使用适当的缓存策略:如 LRU(Least Recently Used)、LFU(Least Frequently Used)等。虽然 Spring Cache 抽象本身不直接支持这些淘汰策略,但可以根据所选的缓存提供者使用其特定配置。例如,在使用 Redis 作为缓存时,可以通过配置 Redis 的相关参数来实现类似的淘汰策略。
- 合理设置缓存过期时间:可以在配置文件中通过spring.cache.redis.time-to-live属性设置缓存的过期时间,也可以在配置类中通过RedisCacheConfiguration.entryTtl方法设置。对于高频访问、变化不频繁的数据,可以设置较长的过期时间,提高命中率;对于低频访问、变化频繁的数据,可以设置较短的过期时间,避免数据不一致。
- 避免缓存雪崩和击穿:
- 缓存雪崩是指缓存中大量的数据同时失效,导致所有请求直接访问后端存储系统,给后端系统带来巨大压力。可以通过设置合理的过期时间,避免大量缓存同时失效。例如,为缓存设置一个随机的过期时间,Random random = new Random(); int seconds = random.nextInt(300) + 300; // 设置在 5 - 10 分钟之间过期。
- 缓存击穿是指某个特定的热点数据突然失效,且在该数据失效的时候,有大量的请求同时涌入,由于缓存中已没有该数据可供服务,这些请求会直接穿透到后端数据源进行查询,从而给后端系统带来过大的压力。可以使用分布式锁机制,保证只有一个线程能够去加载数据并设置缓存。例如:
public Object getDataWithLock(String key) {
Object data = redisTemplate.opsForValue().get(key);
if (data == null) {
// 使用分布式锁
if (redisLock.acquireLock(key)) {
try {
// 从数据库中加载数据
data = loadDataFromDatabase();
redisTemplate.opsForValue().set(key, data);
} finally {
redisLock.releaseLock(key);
}
} else {
// 获取锁失败,等待一段时间重试或返回默认值
}
}
return data;
}
- 还可以使用布隆过滤器来过滤掉一些不存在的 key,降低缓存击穿的概率。例如:
public boolean keyMayExist(String key) {
// 查询布隆过滤器
if (bloomFilter.mayContain(key)) {
// 查询缓存
return redisTemplate.hasKey(key);
}
return true; // 不存在的 key
}
八、Spring 缓存抽象配置与优化方法

1. 在 Spring Boot 中配置缓存
在 Spring Boot 中配置缓存是提升系统性能的关键步骤之一。具体来说,需要进行以下操作:
添加缓存依赖:在项目的构建文件中,如 Maven 的 pom.xml 文件,添加缓存依赖。通常情况下,需要添加spring-boot-starter-cache依赖,它为 Spring 应用程序提供透明式添加缓存的支持。如果要使用特定的缓存实现,如 Redis,还需要添加spring-boot-starter-data-redis依赖。
配置缓存管理器:缓存管理器负责管理缓存的存储和检索。在 Spring Boot 中,可以根据使用的缓存库配置不同的缓存管理器。例如,如果使用 Redis 作为缓存,需要配置RedisCacheManager;如果使用本地内存作为缓存,可以配置ConcurrentMapCacheManager。
以下是配置RedisCacheManager的示例代码:
@Configuration
public class RedisCacheConfig {
@Bean
public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(3600));
return RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
使用缓存注解控制方法的缓存行为:Spring 提供了一系列缓存注解,如@Cacheable、@CachePut、@CacheEvict等,可以在方法上使用这些注解来控制方法的缓存行为。例如,@Cacheable注解用于标记可缓存的方法,在方法执行前检查缓存中是否有现存结果,如果有则直接返回,否则执行方法并将结果存入缓存。
以下是使用@Cacheable注解的示例代码:
@Service
public class SomeService {
@Cacheable(value = "someCache", key = "#param", cacheManager = "redisCacheManager")
public Result someMethod(Param param) {
// 实际的方法逻辑
return calculateResult(param);
}
}
2. 缓存优化策略
缓存优化策略可以提高缓存的命中率,减少对后端数据源的访问,从而提升系统的性能。以下是一些常见的缓存优化策略:
缓存数据的有效期管理:缓存数据的有效期管理是确保缓存数据有效性的重要手段。可以根据业务需求和数据更新频率设置合理的缓存过期时间。例如,对于变化不频繁的数据,可以设置较长的过期时间,提高命中率;对于变化频繁的数据,可以设置较短的过期时间,避免数据不一致。
在 Spring Boot 应用程序中使用 Redis 进行缓存时,可以通过以下配置指定生存时间:spring.cache.redis.time-to-live=10m。如果缓存提供者不支持 TTL,可以使用@CacheEvict注解和调度器来实现,例如:
@CacheEvict(value = "cache1", allEntries = true)
@Scheduled(fixedRateString = "${your.config.key.for.ttl.in.milli}")
public void emptyCache1() {
// 刷新缓存,这里无需编写任何代码,除了描述性日志!
}
命中率监控:监控缓存命中率可以及时发现缓存使用不当或效率低下的问题。可以使用 Spring Boot Actuator 等监控工具来监控缓存命中率。通过监控命中率,可以调整缓存策略,提高缓存的效率。
多级缓存策略:多级缓存策略可以充分利用不同缓存的优势,提高系统的性能和稳定性。通常情况下,可以将本地缓存(如 Caffeine)作为一级缓存,远程缓存(如 Redis)作为二级缓存。这样的设计可以充分利用不同缓存的优势。Caffeine 作为本地缓存,提供极高的读写性能,能够快速响应频繁访问的数据请求。Redis 作为远程缓存,提供更大的存储容量和更好的数据共享性,适合存储需要在多个节点间共享的数据。
以下是配置多级缓存的步骤:
- 添加依赖:添加 Caffeine 和 Redis 的依赖。例如:
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
- 配置本地缓存 Caffeine:
@Configuration
public class CacheConfig {
@Bean
public CacheManager caffeineCache() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
cacheManager.setCaffeine(Caffeine.newBuilder().maximumSize(1000).expireAfterAccess(10, TimeUnit.MINUTES));
return cacheManager;
}
}
- 配置远程缓存 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring.redis.host: 192.168.1.100
spring.redis.port: 6379
@Configuration
public class CacheConfig {
@Bean
public CacheManager redisCache(RedisConnectionFactory redisConnectionFactory) {
RedisCacheManager redisCacheManager = RedisCacheManager.create(redisConnectionFactory);
return redisCacheManager;
}
}
- 将 Caffeine 和 Redis 的 CacheManager 结合起来:
@Configuration
public class CacheConfig {...
@Bean
public CompositeCacheManager compositeCacheManager(CaffeineCacheManager caffeineCacheManager,
RedisCacheManager redisCacheManager) {
CompositeCacheManager compositeCacheManager = new CompositeCacheManager();
compositeCacheManager.setCacheManagers(Arrays.asList(caffeineCacheManager, redisCacheManager));