一、介绍
文章主要内容
- 对SpringBoot对Redis缓存的两种使用方法编写了一套案例。第一种是基于Spring默认的缓存管理注解,第二种则是使用Redis Api实现缓存的自定义缓存管理。
- 对SpingBoot缓存的自动配置过程和源码,进行了探索,方便理解自动配置的流程。
- 针对源码自定义了 redisTemplate和RedisCacheManager ,方便修改Redis的默认序列化方式,从JDK序列化改为JSON
- 关于缓存注解的简单使用 可以参考这个 SpringBoot自带的内部缓存体验
二、给项目配置Redis
1、引入Spring Data redis starter
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2、配置Redis的连接信息
# Redis服务地址
spring.redis.host=127.0.0.1
# Redis服务器连接端口
spring.redis.port=6379
# Redis服务器连接密码(默认为空)
spring.redis.password=
三、使用基于注解的Redis缓存实现
1、开启Spring缓存支持
在SpringBoot启动类上,增加注解@EnableCaching 开启Spring Boot基于注解的缓存管理支持
@EnableCaching // 开启Spring Boot基于注解的缓存管理支持
@SpringBootApplication
public class Springboot04CacheApplication {
public static void main(String[] args) {
SpringApplication.run(Springboot04CacheApplication.class, args);
}
}
2、三个缓存注解轻松使用缓存 @Cacheable、@CachePut、@CacheEvict
@Service
public class TestRedisCacheService extends BaseService {
@Autowired
private SentenceMapper sentenceMapper;
/**
* 查询的句子
* 开启缓存,缓存的namespace为sentence,缓存记录的key为id(未标记key属性时,)
*
* @param id id
* @return {@link Sentence}
*/
@Cacheable(cacheNames = "sentence", unless = "#result==null")
public Sentence querySentence(Integer id) {
Sentence sentence = sentenceMapper.selectById(id);
logger.info("我是querySentence方法,我执行了");
return sentence;
}
/**
* 更新的句子
* 在更新句子对象时,会根据句子对象的id刷新他在namespace为sentence缓存对象
*
* @param sentence 句子
* @return {@link Sentence}
*/
@CachePut(cacheNames = "sentence", key = "#result.id")
public Sentence updateSentence(Sentence sentence) {
int i = sentenceMapper.updateById(sentence);
logger.info("我是updateSentence方法,我执行了");
return sentence;
}
/**
* 删除句子
* 删除句子时,一并根据id删除缓存
* @param id id
*/
@CacheEvict(cacheNames = "sentence")
public void deleteSentence(Integer id) {
logger.info("我是deleteSentence方法,我执行了");
}
}
这三个注解的功能说明
@Cacheable注解
@Cacheable注解也是由spring框架提供的,可以作用于类或方法(通常用在数据查询方法上),用于对方法结果进行缓存存储。注解的执行顺序是,先进行缓存查询,如果为空则进行方法查询,并将结果进行缓存;如果缓存中有数据,不进行方法查询,而是直接使用缓存数据。
@CachePut 更新缓存
- @CachePut 适用于更新数据的方法。目标方法执行完之后生效, @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行,执行之后的结果被保存在缓存中
- @CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同,除了不支持sync属性。
- 更新操作,前端会把id+实体传递到后端使用,我们就直接指定方法的返回值从新存进缓存时的key=“#id” , 如果前端只是给了实体,我们就使用 key=“#实体.id” 获取key. 同时,他的执行时机是目标方法结束后执行, 所以也可以使用 key=“#result.id” , 拿出返回值的id
@CacheEvict 删除缓存
@CacheEvict注解是由Spring框架提供的,可以作用于类或方法(通常用在数据删除方法上),该注解的作用是删除缓存数据。@CacheEvict注解的默认执行顺序是,先进行方法调用,然后将缓存进行清除。
3、测试结果
单元测试代码
@SpringBootTest
public class TestCache extends BaseService {
@Autowired
TestRedisCacheService service;
/**
* 测试缓存
*/
@Test
public void testCache(){
logger.info("第一次执行querySentence");
Sentence sentence1 = service.querySentence(1);
logger.info("第一次执行querySentence执行结果:"+sentence1);
logger.info("第二次执行querySentence");
Sentence sentence2 = service.querySentence(1);
logger.info("第二次执行querySentence执行结果:"+sentence2);
logger.info("**********************开始尝试更新缓存***************");
logger.info("第一次执行updateSentence");
sentence2.setcTime(LocalDateTime.now());
service.updateSentence(sentence2);
logger.info("第三次执行querySentence 观察缓存是否更新");
Sentence sentence3 = service.querySentence(1);
logger.info("第三次执行querySentence执行结果:"+sentence3);
}
/**
* 测试删除缓存
*/
@Test
public void testDeleteCache(){
logger.info("执行deleteSentence 删除缓存");
service.deleteSentence(1);
}
}
测试缓存执行的结果
此时可以看到redis中已经有数据了
总结:
- 第一次执行查询方法后,结果被缓存了,第二次执行时未调用方法,直接获取的缓存数据
- 数据更新后,返回的结果直接更新的对应的缓存记录
- 缓存对象的值,经过JDK默认序列格式化后的HEX格式存储在redis中。这种序列化方式不方便查看。后期可以优化成json格式
- redis中的namespace对应了我们制定的cacheName,键的名称空间后加两个冒号组合保存的
四、直接操作API的Redis缓存实现
在Spring Boot整合Redis缓存实现中,除了基于注解形式的Redis缓存实现外,还有一种开发中常用
的方式——基于API的Redis缓存实现。这种基于API的Redis缓存实现,需要在某种业务需求下通过
Redis提供的API调用相关方法实现数据缓存管理;同时,这种方法还可以手动管理缓存的有效期。
使用Redis的API操作缓存
@Service
public class ApiCommentService {
@Autowired
private CommentRepository commentRepository;
@Autowired
private RedisTemplate redisTemplate;
public Comment findCommentById(Integer id){
Object o = redisTemplate.opsForValue().get("comment_" + id);
if(o!=null){
return (Comment) o;
}else {
//缓存中没有,从数据库查询
Optional<Comment> byId = commentRepository.findById(id);
if(byId.isPresent()){
Comment comment = byId.get();
//将查询结果存入到缓存中,并设置有效期为1天
redisTemplate.opsForValue().set("comment_"+id,comment,1,TimeUnit.DAYS);
return comment;
}else {
return null;
}
}
}
public Comment updateComment(Comment comment) {
commentRepository.updateComment(comment.getAuthor(), comment.getaId());
//更新数据后进行缓存更新
redisTemplate.opsForValue().set("comment_"+comment.getId(),comment);
return comment;
}
public void deleteComment(int comment_id) {
commentRepository.deleteById(comment_id);
redisTemplate.delete("comment_"+comment_id);
}
基于API的Redis缓存实现的相关配置。基于API的Redis缓存实现不需要@EnableCaching注解开启
基于注解的缓存支持,所以这里可以选择将添加在项目启动类上的@EnableCaching进行删除或者
注释
五、自定义数据的序列化机制
5.1 自定义RedisTemplate来修改API保存的缓存序列化方式
5.1.1 RedisTemplate类的说明
简化Redis数据访问代码的助手类。 在给定的对象和Redis存储中的底层二进制数据之间执行自动序列化/反序列化。默认情况下,它对对象使用Java序列化(通过JdkSerializationRedisSerializer)。对于字符串密集型操作,请考虑专用的StringRedisTemplate。 中心方法是execute,支持实现RedisCallback接口的Redis访问代码。它提供了RedisConnection处理,这样RedisCallback实现和调用代码都不需要显式地关心检索/关闭Redis连接,或者处理Connection生命周期异常。对于典型的单步操作,有各种方便的方法。 一旦配置好,这个类就是线程安全的。 注意,虽然模板是泛化的,但要由序列化器/反序列化器来正确地将给定的对象与二进制数据进行转换。 这是Redis支持中的中心类。
5.1.2 观察RedisTemplate的序列化方式定义
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
....
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer<?> defaultSerializer;
private @Nullable ClassLoader classLoader;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer keySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer valueSerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashKeySerializer = null;
@SuppressWarnings("rawtypes") private @Nullable RedisSerializer hashValueSerializer = null;
private RedisSerializer<String> stringSerializer = RedisSerializer.string();
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
if (enableDefaultSerializer) {
if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}
if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<>(this);
}
initialized = true;
}
.....
}
- 从上述RedisTemplate核心源码可以看出,在RedisTemplate内部声明了缓存数据key、value的各种序列化方式,且初始值都为空;在afterPropertiesSet()方法中,判断如果默认序列化参数defaultSerializer为空,将数据的默认序列化方式设置为JdkSerializationRedisSerializer
- 使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable)
- 使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式defaultSerializer,那么将使用自定义的序列化方式。
- 可以看到Redis要求的序列化方式为RedisSerializer接口,通过该接口查看可以发现已经提供了多个实现类其中JdkSerializationRedisSerializer是JDK自带的,也是RedisTemplate内部默认的序列化实现方式,所以我们可以选择其他支持的序列化方式。例如JSON
5.1.3 如何覆盖RedisTemplate
RedisTemplate这个Bean原来是由RedisAutoConfiguration类自动配置代码
@AutoConfiguration
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(redisConnectionFactory);
return template;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
return new StringRedisTemplate(redisConnectionFactory);
}
}
可以看到直接使用RedisConnectionFactory生成了一个默认的RedisTemplate,并且是在没有定义Bean名称为redisTemplate的情况下才进行自定义配置。这样我们只要自定一个类型为RedisTemplate并且名称为redisTemplate的Bean就可以覆盖这里的默认配置了
5.1.4 自定义RedisTemplate修改序列化配置
定义一个配置类,来自己定义RedisTemplate 并且Bean名称需要为redisTemplate,这样就能覆盖原有的配置。
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 使用JSON格式序列化对象,对缓存数据key和value进行转换
Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 设置RedisTemplate模板API的序列化方式为JSON
template.setDefaultSerializer(jacksonSeial);
return template;
}
}
5.2 自定义RedisCacheManager修改缓存注解保存的数据序列化方式
5.2.1 RedisCacheConfiguration设置的序列化方式
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisConnectionFactory.class)
@AutoConfigureAfter(RedisAutoConfiguration.class)
@ConditionalOnBean(RedisConnectionFactory.class)
@ConditionalOnMissingBean(CacheManager.class)
@Conditional(CacheCondition.class)
class RedisCacheConfiguration {
@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
List<String> cacheNames = cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
}
if (cacheProperties.getRedis().isEnableStatistics()) {
builder.enableStatistics();
}
redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return cacheManagerCustomizers.customize(builder.build());
}
private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
CacheProperties cacheProperties,
ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
ClassLoader classLoader) {
return redisCacheConfiguration.getIfAvailable(() -> createConfiguration(cacheProperties, classLoader));
}
//这里根据 cacheProperties 创建redis的Configuration 使用了defaultCacheConfig进行初始化
private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
CacheProperties cacheProperties, ClassLoader classLoader) {
Redis redisProperties = cacheProperties.getRedis();
//默认初始化
org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
.defaultCacheConfig();
config = config.serializeValuesWith(
SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}
public static RedisCacheConfiguration defaultCacheConfig(@Nullable ClassLoader classLoader) {
DefaultFormattingConversionService conversionService = new DefaultFormattingConversionService();
registerDefaultConverters(conversionService);
return new RedisCacheConfiguration(Duration.ZERO, true, true, CacheKeyPrefix.simple(),
SerializationPair.fromSerializer(RedisSerializer.string()),
SerializationPair.fromSerializer(RedisSerializer.java(classLoader)), conversionService);
}
可以看到,RedisCacheConfiguration同样通过RedisConnectionFactory定义了一个缓存管理器,RedisCacheManager;同时在制定序列化政策时,给key使用了RedisSerializer.string() 而value使用了JDK默认序列化机制。
5.2.2 自定义RedisCacheManager
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 分别创建String和JSON格式序列化对象,对缓存数据key和value进行转换
RedisSerializer<String> strSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jacksonSeial =new Jackson2JsonRedisSerializer(Object.class);
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jacksonSeial.setObjectMapper(om);
// 定制缓存数据序列化方式及时效
RedisCacheConfiguration config =
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(1))
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(strSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair
.fromSerializer(jacksonSeial))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager
.builder(redisConnectionFactory).cacheDefaults(config).build();
return cacheManager;
}
- 一旦自己自定义了cacheManager的Bean 那么SpringBoot的缓存配置类就不会执行,也不会读取配置文件的配置,如果要用到配置信息,需要自己开启注解
@EnableConfigurationProperties(CacheProperties.class)
- 因为注册 RedisCacheManager时 用到了RedisConnectionFactory 所以可以让自定义配置在
@AutoConfigureAfter(RedisAutoConfiguration.class)
Redis配置之后执行。
六、总结
问题1:在上面基于注解的缓存使用中,缓存的新增、修改、删除都有了,但是既然是缓存就应该有有效期的,Redis缓存注解的有效期是多久呢?又该如何配置呢?
原因:
通过配置spring.cache.redis.time-to-live的值可以设置缓存的有效期,并不是所有缓存类型都支持配置有效期,如果不配置该属性,那么缓存到redis的数据,将不会设置过期时间。
详解:
- SpringBoot缓存的自动配置类,在Spring-Boot-autoconfigure包中,根据自动配置的配置文件
org.springframework.boot.autoconfigure.AutoConfiguration.imports
(这里使用的SpringBoot是2.7.4 spring从2.7开始,将自动配置类的类路径配置文件放在了META-INF/spring/%s.imports 文件中,而不是再从META-INF/spring.factories 中读取) 发现缓存自动配置的入口配置类为org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
CacheAutoConfiguration
中引入了配置文件@EnableConfigurationProperties(CacheProperties.class)
并且import了RedisCacheConfiguration redis缓存的自动配置文件。- CacheProperties除了缓存类型外,定义了不同缓存需要配置的属性 就包括了,过期时间属性的路径就对应为
org.springframework.boot.autoconfigure.cache.CacheProperties.Redis#timeToLive
- 在Redis的自动配置类org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration,自动配置RedisCacheManager的Bean时,会将CacheProperties转换为org.springframework.data.redis.cache.RedisCacheConfiguration 的Redis缓存配置对象,注意这个配置信息类和redis缓存自动配置类类名一样但是路径不一样。转换的过程中,如果没有设置
timeToLive
属性,那么这个RedisCacheConfiguration 配置对象中默认的值会被配置为Duration ttl=Duration._ZERO _ - 在RedisCache中新增缓存时,通过DefaultRedisCacheWrite的put方法来写入缓存,写入缓存时就用到了 上述的ttl属性 put方法中,判断是否使用过期的缓存判断代码如下
ttl != null && !ttl.isZero() && !ttl.isNegative();
问题2:注解缓存直接保存的序列化的结果到Redis中,不方便查看怎么办?
解答:RedisCacheManager使用了默认的JDK序列化,可以通过重写RedisCacheManager类来修改序列化的方式。
问题3:为什么我引入了Redis的依赖,就直接给我把数据存储到redis里了?
解答:
- Spring支持多种缓存,并且支持配置文件制定缓存类型,或者按照条件按照优先级自动配置/禁用缓存,所以引入Redis的依赖后,就会自动配置redis的缓存。
- 如果指定了缓存类型,那么会由指定缓存类型的配置类,来自动配置缓存,如果没指定类型,那么Spring会依次按照顺序尝试自动配置缓存,一旦有一个cacheManager成功配置,后续的自动配置类也就不满足配置条件了。
- 缓存配置类配置条件见该类 org.springframework.boot.autoconfigure.cache.CacheCondition
class CacheCondition extends SpringBootCondition {
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context, AnnotatedTypeMetadata metadata) {
String sourceClass = "";
if (metadata instanceof ClassMetadata) {
sourceClass = ((ClassMetadata) metadata).getClassName();
}
ConditionMessage.Builder message = ConditionMessage.forCondition("Cache", sourceClass);
Environment environment = context.getEnvironment();
try {
BindResult<CacheType> specified = Binder.get(environment).bind("spring.cache.type", CacheType.class);
if (!specified.isBound()) {
return ConditionOutcome.match(message.because("automatic cache type"));
}
CacheType required = CacheConfigurations.getType(((AnnotationMetadata) metadata).getClassName());
if (specified.get() == required) {
return ConditionOutcome.match(message.because(specified.get() + " cache type"));
}
}
catch (BindException ex) {
}
return ConditionOutcome.noMatch(message.because("unknown cache type"));
}
}