# spring-cache-redis-demo **Repository Path**: cckevincyh/spring-cache-redis-demo ## Basic Information - **Project Name**: spring-cache-redis-demo - **Description**: No description available - **Primary Language**: Java - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2020-11-11 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README [TOC] ## Spring Data Redis ### Spring Cache 简介 > 当Spring Boot 结合Redis来作为缓存使用时,最简单的方式就是使用Spring Cache了,使用它我们无需知道Spring中对Redis的各种操作,仅仅通过它提供的@Cacheable 、@CachePut 、@CacheEvict 、@EnableCaching等注解就可以实现缓存功能 ### 在spring boot项目中加入依赖和配置 ```xml 4.0.0 org.springframework.boot spring-boot-starter-parent 2.2.2.RELEASE org.example spring-cache-redis-demo 1.0-SNAPSHOT org.springframework.boot spring-boot-starter-data-redis org.springframework.boot spring-boot-starter-test test com.fasterxml.jackson.datatype jackson-datatype-jsr310 org.apache.commons commons-pool2 ``` 注意:在 springboot 1.5.x版本的默认的Redis客户端是 Jedis实现的,springboot 2.x版本中默认客户端是用 lettuce实现的。 区别: - Jedis在实现上是直连Redis服务,多线程环境下非线程安全,除非使用连接池,为每个 RedisConnection 实例增加物理连接。 - Lettuce是一种可伸缩,线程安全,完全非阻塞的Redis客户端,多个线程可以共享一个RedisConnection,它利用Netty NIO框架来高效地管理多个连接,从而提供了异步和同步数据访问方式,用于构建非阻塞的反应性应用程序。 我们这里推荐使用Lettuce。 加入配置 ```yaml server: port: 8081 spring: application: name: redis-demo redis: host: localhost password: dpmcp_redis database: 0 lettuce: pool: max-active: 8 # 连接池最大连接数 max-idle: 8 # 连接池最大空闲连接数 min-idle: 0 # 连接池最小空闲连接数 max-wait: -1ms # 连接池最大阻塞等待时间,负值表示没有限制 ``` 注意:由于SpringBoot 2.x中默认并没有使用Redis连接池,所以需要在pom.xml中添加commons-pool2的依赖 ```xml org.apache.commons commons-pool2 ``` #### RedisTemplate自动装配 我们可以看到在`spring.factories`文件中,配有redis的自动装配的类`RedisAutoConfiguration` 我们可以看看`RedisAutoConfiguration.class`中配置了什么。 ```java @Configuration( proxyBeanMethods = false ) @ConditionalOnClass({RedisOperations.class}) @EnableConfigurationProperties({RedisProperties.class}) @Import({LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class}) public class RedisAutoConfiguration { public RedisAutoConfiguration() { } @Bean @ConditionalOnMissingBean( name = {"redisTemplate"} ) public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate template = new RedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } } ``` 从上面的源码可以看到,在SpringBoot中,已经自动帮我们在容器中生成了一个`RedisTemplate`和一个`StringRedisTemplate`。 从源码中可以看出,我们开发时会存在2个问题: - `RedisTemplate`的泛型是,我们在进行缓存时写代码不是很方便,因为一般我们的key是String类型,所以我们需要一个的泛型。 - `RedisTemplate`没有设置数据存储在Redis时,Key和Value的序列化方式(采用默认的JDK序列化方式)。 如何解决上面这两个问题呢? - 上面源码中的`@ConditionalOnMissing`注解表示:如果Spring容器中已经定义了id为`redisTemplate`的Bean,那么自动装配的`RedisTemplate`不会实例化。因此我们可以写一个配置类,配置`RedisTemplate`. #### RedisSerializer序列化器 当我们利用`StringRedisSerializer`,`Jackson2JsonRedisSerializer`和`JdkSerializationRedisSerializer`进行序列化时,对同一个数据进行序列化前后的结果如下表: | 数据结构 | 序列化类 | 序列化前 | 序列化后 | | --------- | --------------------------------- | ---------- | ------------ | | key/value | `StringRedisSerializer` | test_value | test_value | | key/value | `Jackson2JsonRedisSerializer` | test_value | "test_value" | | key/value | `JdkSerializationRedisSerializer` | test_value | 乱码 | 所以我们需要对key用`StringRedisSerializer`进行序列化,对value用`Jackson2JsonRedisSerializer`进行序列化 #### 配置类 ```java package org.example.demo.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.JsonTypeInfo; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.CacheErrorHandler; 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.RedisConnectionFactory; 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; @EnableCaching @Configuration public class RedisConfig extends CachingConfigurerSupport { private static final Logger log = LoggerFactory.getLogger(RedisConfig.class); @Bean public RedisTemplate redisTemplate(RedisConnectionFactory factory) { RedisTemplate template = new RedisTemplate<>(); template.setConnectionFactory(factory); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = getObjectJackson2JsonRedisSerializer(); // String 的序列化 StringRedisSerializer stringRedisSerializer = new StringRedisSerializer(); // key采用String的序列化方式 template.setKeySerializer(stringRedisSerializer); // hash的key也采用String的序列化方式 template.setHashKeySerializer(stringRedisSerializer); // value序列化方式采用jackson template.setValueSerializer(jackson2JsonRedisSerializer); // hash的value序列化方式采用jackson template.setHashValueSerializer(jackson2JsonRedisSerializer); // 设置值(value)的序列化采用Jackson2JsonRedisSerializer。 // 设置键(key)的序列化采用StringRedisSerializer。 template.afterPropertiesSet(); return template; } private Jackson2JsonRedisSerializer getObjectJackson2JsonRedisSerializer() { Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper objectMapper = serializingObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 该方法过时 // om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); // 上面 enableDefaultTyping 方法过时,使用 activateDefaultTyping // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY); jackson2JsonRedisSerializer.setObjectMapper(objectMapper); return jackson2JsonRedisSerializer; } /** * 解决jdk1.8中新时间API的序列化时出现com.fasterxml.jackson.databind.exc.InvalidTypeIdException:的问题 */ @Bean public ObjectMapper serializingObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.registerModule(new JavaTimeModule()); return objectMapper; } @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { RedisSerializer redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = getObjectJackson2JsonRedisSerializer(); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(Duration.ofSeconds(10)) //有效期设置为10s .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } @Bean @Override public KeyGenerator keyGenerator() { // 设置自动key的生成规则,配置spring boot的注解,进行方法级别的缓存 // 使用:进行分割,可以很多显示出层级关系 // 这里其实就是new了一个KeyGenerator对象, // 这是lambda表达式的写法 return (target, method, params) -> { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(":"); sb.append(method.getName()); for (Object obj : params) { sb.append(":").append(obj); } String rsToUse = String.valueOf(sb); log.info("自动生成Redis Key -> [{}]", rsToUse); return rsToUse; }; } @Override @Bean public CacheErrorHandler errorHandler() { // 异常处理,当Redis发生异常时,打印日志,但是程序正常走 log.info("init -> [{}]", "Redis CacheErrorHandler"); return new CacheErrorHandler() { @Override public void handleCacheGetError(RuntimeException e, Cache cache, Object key) { log.error("Redis occur handleCacheGetError: key -> [{}]", key, e); } @Override public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) { log.error("Redis occur handleCachePutError: key -> [{}];value -> [{}]", key, value, e); } @Override public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) { log.error("Redis occur handleCacheEvictError: key -> [{}]", key, e); } @Override public void handleCacheClearError(RuntimeException e, Cache cache) { log.error("Redis occur handleCacheClearError: ", e); } }; } } ``` ### @Cacheable 使用该注解的方法当缓存存在时,会从缓存中获取数据而不执行方法,当缓存不存在时,会执行方法并把返回结果存入缓存中。`一般使用在查询方法上`,可以设置如下属性: - value:缓存名称(必填),指定缓存的命名空间; - key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义; - unless:条件符合则不缓存; - condition:条件符合则缓存。 ### @CachePut 使用该注解的方法每次执行时都会把返回结果存入缓存中。`一般使用在新增方法上`,可以设置如下属性: - value:缓存名称(必填),指定缓存的命名空间; - key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义; - unless:条件符合则不缓存; - condition:条件符合则缓存 ### @CacheEvict 使用该注解的方法执行时会清空指定的缓存。`一般使用在更新或删除方法上`,可以设置如下属性: - value:缓存名称(必填),指定缓存的命名空间; - key:用于设置在命名空间中的缓存key值,可以使用SpEL表达式定义; - allEntries: 是否清除指定命名空间下的所有key的缓存,默认为false。当其为true时,将忽略key属性配置指定key - condition:条件符合则缓存。 - beforeInvocation : 是否在调用方法前去清除指定缓存。当其为true时,将会在方法执行前清除缓存;当其为false时,将会在方法成功执行后清除缓存,如果方法未成功执行(抛出异常)将无法清除缓存 ## 参考 [SpringBoot2.x教程——NoSQL之SpringBoot整合Redis](https://blog.csdn.net/qianfeng_dashuju/article/details/105969558) [springboot2.0 集成redis服务详解,以及 (Lettuce & Jedis)](https://blog.csdn.net/zzhongcy/article/details/102584028) [Spring Data Redis 最佳实践!](https://zhuanlan.zhihu.com/p/164608337) [Redis在SpringBoot中的实践](https://zhuanlan.zhihu.com/p/134143113) [SpringBoot 应用 Redis 声明式缓存](https://zhuanlan.zhihu.com/p/60664482) [ SpringBoot + Redis 整合 + 配置类+工具类 + 实例, 修改过时方法 enableDefaultTyping,修复LocalDateTime反序列化问题](https://blog.csdn.net/qq_38534107/article/details/105316788) [SpringBoot 2.x 整合redis](https://segmentfault.com/a/1190000020314044?utm_source=tag-newest) [SpringBoot2.X整合Redis缓存](https://www.jianshu.com/p/0d4aea41a70c) [SpringBoot(2.1.1)集成redis及缓存操作失败的异常处理](https://blog.csdn.net/u014401141/article/details/79024483) [3 Springboot中使用redis,redis自动缓存异常处理](https://tianyalei.blog.csdn.net/article/details/70670329) [sprintboot redis异常处理CacheErrorHandler详解以及性能问题分析](https://blog.csdn.net/zzhongcy/article/details/102553614) [Spring之注解式使用Redis缓存当Redis故障或不可用时仍然执行方法服务可用](https://blog.csdn.net/hjtlovelife/article/details/94404802?utm_medium=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.pc_relevant_is_cache&depth_1-utm_source=distribute.pc_relevant_t0.none-task-blog-BlogCommendFromMachineLearnPai2-1.pc_relevant_is_cache) [使用Spring Cache时 优雅的处理缓存系统无法连接](https://my.oschina.net/JasonZhang/blog/994028)