在我们日常工作中,相信大部分人应该都用过redis做缓存,今天发现项目中用了一个注解来自动完成接口结果的缓存--@Cacheable注解(@Cacheable注解是springframework下的注解)
前言:在我们使用一个对于我们自身不熟悉的结束之前,我们需要先清楚他是干什么的,有什么优缺点,怎么使用然后才是实现原理.
现在我们按照这个流程来了解@Cacheable:
作用:
@Cacheable 是 Spring 框架中的一个注解,用于启用方法的结果缓存。
它可以应用于方法级别或类级别上。当应用在方法上时,它会将方法的返回值缓存起来,下次再调用该方法时,如果方法的参数与之前调用时的参数相同,则会直接返回缓存中的结果,而不会再执行方法体。这样可以提高方法的执行效率
优缺点:
使用 @Cacheable 注解带来以下优点:
- 提高性能:缓存可以将方法的结果存储在内存中,后续对相同参数的方法调用可以直接从缓存中获取结果,避免重复计算,提高方法的执行效率。
- 减轻数据库压力:对于需要频繁访问数据库的方法,可以将结果缓存在内存中,减轻数据库的压力,提高数据库的响应速度。
- 简化代码逻辑:通过使用缓存,可以避免编写复杂的手动缓存代码或条件判断逻辑,使代码更简洁、可读性更好。
然而,使用 @Cacheable 注解也存在一些缺点:
- 内存占用:缓存的数据存储在内存中,因此如果缓存的数据量过大,会消耗大量的内存资源。在使用缓存时需要合理安排系统的内存和缓存容量。
- 数据一致性:使用缓存后,需要注意维护数据的一致性。当数据发生变化时,需要及时更新缓存,以避免脏数据的问题。可通过合理设计缓存策略和使用缓存失效机制来解决这个问题。
- 缓存失效:缓存的有效期限需要合理设置。如果缓存的有效期太长,可能导致数据更新不及时;如果缓存的有效期太短,可能增加重复执行代码的次数。需要根据具体业务场景来选择合适的缓存有效期。
- 缓存击穿:当某个缓存条目在缓存失效时,同时有大量的并发请求到达时,可能会导致缓存击穿问题,即大量请求直接击穿到数据库。可以通过加锁或使用分布式锁等机制来避免缓存击穿。
- 适用性限制:@Cacheable 注解适用于方法级别的缓存,对于复杂的缓存需求,可能需要更高级别的缓存管理方式,如缓存集群、多级缓存等。
在了解完优缺点后如果我们仍要使用可以去尽量的避免@Cacheable的缺点:
比如内存占用问题,我们可以引入redis而不是使用系统默认的内存,缓存失效问题可以设置合理的有效时间等
使用:
为了使 @Cacheable 注解生效,需要在 Spring 的配置类中配置缓存管理器和相关的缓存策略。
首先编写配置类: 在 Spring 的配置类上添加 @EnableCaching 注解,以启用缓存功能 RedisCacheManager作为 缓存管理器
/**
* @version 1.0
* @className RedisCacheConfig
* @description 基础描述:springboot内部缓存集成redis
* @date 2022/4/24 21:26
*/
@Configuration
@EnableCaching
@Slf4j
public class RedisCacheConfig extends CachingConfigurerSupport {
//2. 从配置文件加载redis2的数据
@Value("${spring.redis_cache.host}")
private String redisCacheHost;
@Value("${spring.redis_cache.port}")
private String redisCachePort;
@Value("${spring.redis_cache.password}")
private String redisCachePassword;
//3. 连接信息
//最大空闲连接数
private static final int MAX_IDLE = 200;
//最大连接数
private static final int MAX_TOTAL = 1024;
//建立最长等待时间
private static final long MAX_WAIT_MILLIS = 10 * 1000;
//4. 连接池配置
public JedisPoolConfig jedisPoolConfig() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
//1. 配置最大空闲连接数
jedisPoolConfig.setMaxIdle(MAX_IDLE);
//2. 配置最大连接数量
jedisPoolConfig.setMaxTotal(MAX_TOTAL);
//3. 配置最长等待时间
jedisPoolConfig.setMaxWaitMillis(MAX_WAIT_MILLIS);
//4. testOnBorrow:如果为true(默认为false),当应用向连接池申请连接时,连接池会判断这条连接是否是可用的。
jedisPoolConfig.setTestOnBorrow(false);
return jedisPoolConfig;
}
//5. 配置工厂
public RedisConnectionFactory redisConnectionFactory() {
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory();
jedisConnectionFactory.setHostName(redisCacheHost);
jedisConnectionFactory.setPort(Integer.parseInt(redisCachePort));
if (StringUtils.isNotEmpty(redisCachePassword)) {
jedisConnectionFactory.setPassword(redisCachePassword);
}
//配置连接Redis的哪一个库, 默认是第一个
jedisConnectionFactory.setPoolConfig(jedisPoolConfig());
jedisConnectionFactory.afterPropertiesSet();
cacheManager(jedisConnectionFactory);
return jedisConnectionFactory;
}
@Bean(name = "againRedisTemplate")
public RedisTemplate<String, Object> aredisTemplate() {
RedisTemplate<String, Object> stringRedisTemplate = new RedisTemplate<>();
RedisSerializer<String> stringRedisSerializer = new StringRedisSerializer();
//1. 设置Json的序列化器
// Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
// ObjectMapper om = new ObjectMapper();
// om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// jackson2JsonRedisSerializer.setObjectMapper(om);
//2. 设置工厂
stringRedisTemplate.setConnectionFactory(redisConnectionFactory());
//3. 设置key的序列化器
stringRedisTemplate.setKeySerializer(stringRedisSerializer);
stringRedisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
// //4. 设置Value的序列化器(我这里设置成了String类型的)
// stringRedisTemplate.setValueSerializer(stringRedisSerializer);
//5. 设置Hash的,这里为Json类型的
// stringRedisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
return stringRedisTemplate;
}
@Bean
// @PostConstruct
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
log.info("自定义RedisCacheManager加载完成");
return new RedisCacheManager(
RedisCacheWriter.lockingRedisCacheWriter(factory),
this.getRedisCacheConfigurationWithTtl(),
this.getRedisCacheConfigurationMap()
);
}
/**
* 默认失效时间配置
*/
private RedisCacheConfiguration getRedisCacheConfigurationWithTtl() {
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);
return RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith(
RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofSeconds(10 * 60L));
}
/**
* 已知缓存名称的映射以及用于这些缓存的配置
*/
private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() {
Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>();
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.computePrefixWith(name -> name + ":")
// 设置key的序列化方式
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer()))
// 设置value的序列化方式
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer()))
// 不缓存null值
.disableCachingNullValues();
// 自定义缓存名称对应的配置
redisCacheConfigurationMap.put(Contants.RE_CACHE_OF_CENTER_DATA_CACHE_KEY, config.entryTtl(Duration.ofDays(7L)));
redisCacheConfigurationMap.put(Contants.RE_CACHE_OF_METHOD_DATA_CACHE_KEY, config.entryTtl(Duration.ofSeconds(5 * 60L)));
return redisCacheConfigurationMap;
}
// key键序列化方式
private RedisSerializer<String> keySerializer() {
return new StringRedisSerializer();
}
// value值序列化方式
private GenericJackson2JsonRedisSerializer valueSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
}
使用示例:
//引用方法的参数作为缓存的键:
@Cacheable(cacheNames = "myCache", key = "#param")
public String getCachedData(String param) {
// ...
}
//引用方法的返回值作为缓存的键:
@Cacheable(cacheNames = "myCache", key = "#result.id")
public User getUserById(Long id) {
// ...
}
// 使用 SpEL 表达式在方法参数中进行计算来定义缓存的键:
@Cacheable(cacheNames = "myCache", key = "'prefix:' + #param.toUpperCase()")
public String getCachedData(String param) {
// ...
}
// 条件判断:使用condition属性
@Cacheable(cacheNames = "myCache", condition = "#param.length() < 10")
public String getCachedData(String param) {
// ...
}
实现原理:
简化的底层源码:
CacheableOperation 类是 @Cacheable 注解的底层实现类。它继承自 AbstractCacheOperation,并实现了 execute 方法。
在 execute 方法中,首先使用缓存解析器 CacheResolver 解析缓存的 Cache 实例。然后,通过 generateKey 方法生成缓存的键。接下来,通过调用 cache.get(key) 方法尝试从缓存中获取结果。
如果缓存中存在对应的条目,则直接返回缓存中的值。如果缓存中不存在对应的条目,则通过调用 invoker.invoke() 方法执行目标方法,并将方法的结果存储到缓存中。
public class CacheableOperation extends AbstractCacheOperation {
public CacheableOperation(Cacheable cacheable, CacheOperationMetadata metadata) {
super(cacheable, metadata);
}
@Override
protected Object execute(CacheOperationInvoker invoker) {
Cache cache = getCacheResolver().resolveCache(getCacheNames(), getTargetClass());
Object key = generateKey(invoker.getArgs());
ValueWrapper valueWrapper = cache.get(key);
if (valueWrapper != null) {
return valueWrapper.get();
}
Object result = invoker.invoke();
cache.put(key, result);
return result;
}
}
@Cacheable 注解的实现原理涉及到 Spring 的缓存抽象层和底层缓存提供器的协作。
- 在 Spring 应用上下文初始化时,根据配置的缓存管理器(CacheManager)创建缓存实例(Cache)。缓存管理器可以是 Spring 内置的实现,如 ConcurrentMapCacheManager,也可以是第三方的缓存提供器,如 Ehcache 或 Redis。
- 当使用 @Cacheable 注解标记一个方法时,Spring 在方法执行前会先检查缓存中是否存在以指定的缓存名称和缓存键为索引的缓存条目。
- 如果缓存中存在对应的缓存条目,Spring 会直接从缓存中获取结果并返回,方法体不会被执行。这样可以节省方法的执行时间,提高整体性能。
- 如果缓存中不存在对应的缓存条目,则执行方法体,并将方法的返回结果存储到缓存中,以便下次同样的参数调用时可以直接使用缓存结果。
- 缓存的键(cache key)可以通过 key 属性使用 SpEL 表达式来定义,可以引用方法的参数、返回值和其他相关的对象。这样可以生成唯一的缓存键,确保不同参数的方法调用可以分别缓存结果。
- 缓存的生命周期和失效策略由底层的缓存提供器决定。不同的缓存提供器可能具有不同的缓存策略,如基于时间的失效、LRU(最近最少使用)等。
- 缓存还可以通过其他配置属性进行更精细的控制,例如 condition 属性可以使用 SpEL 表达式定义缓存的条件判断,unless 属性可以使用 SpEL 表达式定义不缓存的条件判断等。