一、项目核心依赖(pom文件)
<!--LettuceConnectionFactory--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>
二、核心配置文件(yaml文件)
spring: application: name: postgresql-demo datasource: url: jdbc:postgresql://localhost:5432/test username: postgres password: 123456 driver-class-name: org.postgresql.Driver jpa: show-sql: true properties: hibernate: temp: use_jdbc_metadata_defaults: false dialect: org.hibernate.dialect.PostgreSQLDialect hbm2ddl: auto: update cache: type: redis redis: timeout: 10s lettuce: pool: max-active: 8 max-wait: 30s max-idle: 8 min-idle: 0
三、Redis的配置文件(Java文件)
package com.wangyousong.redis.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheConfiguration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer; import org.springframework.data.redis.serializer.RedisSerializationContext; import org.springframework.data.redis.serializer.RedisSerializer; import org.springframework.data.redis.serializer.StringRedisSerializer; import java.time.Duration; import java.util.Arrays; @EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { private final LettuceConnectionFactory lettuceConnectionFactory; @Autowired public RedisConfig(LettuceConnectionFactory lettuceConnectionFactory) { this.lettuceConnectionFactory = lettuceConnectionFactory; } @Bean public KeyGenerator keyGenerator() { return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); Arrays.stream(params).map(Object::toString).forEach(sb::append); return sb.toString(); }; } // key键序列化方式 private RedisSerializer<String> keySerializer() { return new StringRedisSerializer(); } /** * json序列化 */ @Bean @SuppressWarnings({"rawtypes", "unchecked"}) public RedisSerializer<Object> jackson2JsonRedisSerializer() { //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值 Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper mapper = setObjectMapper(); serializer.setObjectMapper(mapper); return serializer; } private ObjectMapper setObjectMapper() { ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); return mapper; } /** * 配置缓存管理器 */ @Bean public CacheManager cacheManager() { // 生成一个默认配置,通过config对象即可对缓存进行自定义配置 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // 设置缓存的默认过期时间--10分钟,也是使用Duration设置,默认-1表示不过期 config = config.entryTtl(Duration.ofMinutes(10)) // 设置 key为string序列化 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 设置value为json序列化 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer())) // 不缓存空值 .disableCachingNullValues(); // 使用自定义的缓存配置初始化一个cacheManager return RedisCacheManager .builder(lettuceConnectionFactory) .cacheDefaults(config) .transactionAware() .build(); } /** * RedisTemplate配置 */ @Bean @SuppressWarnings({"rawtypes", "unchecked"}) public RedisTemplate<String, Object> redisTemplate(LettuceConnectionFactory lettuceConnectionFactory) { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(lettuceConnectionFactory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper objectMapper = setObjectMapper(); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); //key redisTemplate.setKeySerializer(keySerializer()); redisTemplate.setHashKeySerializer(keySerializer()); //value 值采用json序列化 redisTemplate.setValueSerializer(jackson2JsonRedisSerializer); redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer); redisTemplate.afterPropertiesSet(); return redisTemplate; } }
四、实例代码及运行效果
五、后记(代码小结)
每次写小结的时候,我都知道要认真写了,应该写代码不仅仅是为了写代码,而是为了回顾与总结在写代码时弄清楚的问题,解决问题是结果,但是我却享受过程。
(1)yaml文件中与spring.redis.timeout=10s相等的那条配置,如果写错了,比如timeout=0了,那么会遇到超时的错误;
(2)spring.redis.pool其实都有默认的值,不写也是可以的,只要要注意一下spring.redis.pool.max-wait,它的默认值是-1.
(3)关于KeyGenerator,在RedisConfig中配置,我们是如下配置:
但是实际应用的时候,我们需要根据实际情况应用,有时候还是需要自己定义。
(4)关于@Cacheable中的unless = “#result == null”,这个需要注意。如果没有写这个属性,我们缓存一个null,会报错的。显然缓存null也不是我们希望的,所以在RedisConfig中,我们是设置了不缓存空值的。
当然,这点会在程序报错的时候看到友好的提示的。比如调用UserService.findById(String id),如果id是一个不存的,那么返回的结果Optional中value便是null.
(5)关于默认缓存的时间,默认如果不配置的话TTL=-1,表示永久不过期,在上面RedisConfig中我们设置的是10分钟,通常这个值需要根据业务需求设置。
(6)最后也是看了缓存应用中的问题。UserService中findById(String id)负责查询并缓存查询结果,void update(UserDTO userDTO)负责更新数据,并清空缓存,不然 findById(String id)拿到数据就是“脏数据”,或者说是过期数据,实际的效果就是用户的体验不是很好,因为用户看不到自己实时提交(修改)的数据。所以这时候@CacheEvict注解就很重要了。当时我演示的时候,发现就是没有清空OptionalUser,我当时就很好奇,为什么?注解难道用错了?还是环境配置有问题,还是什么其它的问题?想了一番,乱搜了一番,无果。冷静下来,看看Spring Cache是如果是实现@CacheEvict机制的,换句话说,就是spring cache 是如何解析 @CacheEvict 这个注解的?后来看了一下源码,从@interface CacheEvict 看到interface CacheAnnotationParser,再看到class SpringCacheAnnotationParser,再看到abstract class CacheOperation,再追踪到class CacheEvictOperation,再一搜被调用的地方:
看到CacheAspectSupport中Aspect,就一下子想通了,肯定是通过SOP,所以结果就是在这个类中
然后就打了一下断点运行,看到update()到底做了什么,为什么没有更新findById查询出来的OptionalUser,后来一下他们的key不一样,如果我不指定key值,将采用默认的key生成策略,就是前面介绍的目标类::方法限定名+参数列表,显然查询和更新的方法名是不一样的,尽管我定义了value的值。所以我重写了@Cacheable(value = “OptionalUser”, key = “#id”, unless = “#result == null”)
@CacheEvict(value = “OptionalUser”, key = “#userDTO.id”) 这样doEvict(cache,key)就可以如期清除掉指定key的缓存。
写在最后,理解Spring Cache的缓存机制对于缓存的应用是有莫大的帮助的,不然都理解不了为什么@CacheEvict会失效。