一、项目核心依赖(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会失效。