一、介绍

文章主要内容

  1. 对SpringBoot对Redis缓存的两种使用方法编写了一套案例。第一种是基于Spring默认的缓存管理注解,第二种则是使用Redis Api实现缓存的自定义缓存管理。
  2. 对SpingBoot缓存的自动配置过程和源码,进行了探索,方便理解自动配置的流程。
  3. 针对源码自定义了 redisTemplate和RedisCacheManager ,方便修改Redis的默认序列化方式,从JDK序列化改为JSON
  4. 关于缓存注解的简单使用 可以参考这个 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 更新缓存

  1. @CachePut 适用于更新数据的方法。目标方法执行完之后生效, @CachePut被使用于修改操作比较多,哪怕缓存中已经存在目标值了,但是这个注解保证这个方法依然会执行,执行之后的结果被保存在缓存中
  2. @CachePut注解也提供了多个属性,这些属性与@Cacheable注解的属性完全相同,除了不支持sync属性。
  3. 更新操作,前端会把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中已经有数据了
在这里插入图片描述

总结:

  1. 第一次执行查询方法后,结果被缓存了,第二次执行时未调用方法,直接获取的缓存数据
  2. 数据更新后,返回的结果直接更新的对应的缓存记录
  3. 缓存对象的值,经过JDK默认序列格式化后的HEX格式存储在redis中。这种序列化方式不方便查看。后期可以优化成json格式
  4. 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;
	}
    .....
}
	
  1. 从上述RedisTemplate核心源码可以看出,在RedisTemplate内部声明了缓存数据key、value的各种序列化方式,且初始值都为空;在afterPropertiesSet()方法中,判断如果默认序列化参数defaultSerializer为空,将数据的默认序列化方式设置为JdkSerializationRedisSerializer
  2. 使用RedisTemplate进行Redis数据缓存操作时,内部默认使用的是JdkSerializationRedisSerializer序列化方式,所以进行数据缓存的实体类必须实现JDK自带的序列化接口(例如Serializable)
  3. 使用RedisTemplate进行Redis数据缓存操作时,如果自定义了缓存序列化方式defaultSerializer,那么将使用自定义的序列化方式。
  4. 可以看到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;
}
  1. 一旦自己自定义了cacheManager的Bean 那么SpringBoot的缓存配置类就不会执行,也不会读取配置文件的配置,如果要用到配置信息,需要自己开启注解 @EnableConfigurationProperties(CacheProperties.class)
  2. 因为注册 RedisCacheManager时 用到了RedisConnectionFactory 所以可以让自定义配置在 @AutoConfigureAfter(RedisAutoConfiguration.class) Redis配置之后执行。



六、总结

问题1:在上面基于注解的缓存使用中,缓存的新增、修改、删除都有了,但是既然是缓存就应该有有效期的,Redis缓存注解的有效期是多久呢?又该如何配置呢?
原因:
通过配置spring.cache.redis.time-to-live的值可以设置缓存的有效期,并不是所有缓存类型都支持配置有效期,如果不配置该属性,那么缓存到redis的数据,将不会设置过期时间。
详解:

  1. 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
  2. CacheAutoConfiguration 中引入了配置文件@EnableConfigurationProperties(CacheProperties.class) 并且import了RedisCacheConfiguration redis缓存的自动配置文件。
  3. CacheProperties除了缓存类型外,定义了不同缓存需要配置的属性 就包括了,过期时间属性的路径就对应为org.springframework.boot.autoconfigure.cache.CacheProperties.Redis#timeToLive
  4. 在Redis的自动配置类org.springframework.boot.autoconfigure.cache.RedisCacheConfiguration,自动配置RedisCacheManager的Bean时,会将CacheProperties转换为org.springframework.data.redis.cache.RedisCacheConfiguration 的Redis缓存配置对象,注意这个配置信息类和redis缓存自动配置类类名一样但是路径不一样。转换的过程中,如果没有设置timeToLive属性,那么这个RedisCacheConfiguration 配置对象中默认的值会被配置为Duration ttl=Duration._ZERO _
  5. 在RedisCache中新增缓存时,通过DefaultRedisCacheWrite的put方法来写入缓存,写入缓存时就用到了 上述的ttl属性 put方法中,判断是否使用过期的缓存判断代码如下 ttl != null && !ttl.isZero() && !ttl.isNegative();

问题2:注解缓存直接保存的序列化的结果到Redis中,不方便查看怎么办?
解答:RedisCacheManager使用了默认的JDK序列化,可以通过重写RedisCacheManager类来修改序列化的方式。

问题3:为什么我引入了Redis的依赖,就直接给我把数据存储到redis里了?
解答:

  1. Spring支持多种缓存,并且支持配置文件制定缓存类型,或者按照条件按照优先级自动配置/禁用缓存,所以引入Redis的依赖后,就会自动配置redis的缓存。
  2. 如果指定了缓存类型,那么会由指定缓存类型的配置类,来自动配置缓存,如果没指定类型,那么Spring会依次按照顺序尝试自动配置缓存,一旦有一个cacheManager成功配置,后续的自动配置类也就不满足配置条件了。
  3. 缓存配置类配置条件见该类 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"));
    }

}
Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐