Redis是一个缓存,消息中间件及具有丰富特性的键值存储系统。Spring Boot为Redis的客户端Jedis提供了自动配置实现,Spring Data Redis提供了在它之上的抽象,spring-boot-starter-redis'Starter'为我们提供了必要的依赖。
我们按照之前的经验,创建一个web程序,并将之改造成Spring Boot项目,具体过程略。
<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <!--使用默认的Lettuce时,若配置spring.redis.lettuce.pool则必须配置该依赖--> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-pool2</artifactId> </dependency> <dependency> <groupId>redis.clients</groupId> <artifactId>jedis</artifactId> <version>2.7.3</version> </dependency> <!--解决jdk1.8中新时间API的序列化时出现com.fasterxml.jackson.databind.exc.InvalidDefinitionException的问题--> <dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.1.10</version> </dependency> </dependencies>
SpringBoot集成Redis主要是使用RedisTemplate类进行操作,但是在SpringBoot2.0以后,底层默认访问的不再是Jedis而是lettuce。
spring: datasource: username: root password: syc url: jdbc:mysql://localhost:3306/db4?useUnicode=true&characterEncoding=utf-8&useSSL=true&serverTimezone=UTC driver-class-name: com.mysql.jdbc.Driver jpa: show-sql: true hibernate: ddl-auto: update redis: host: 127.0.0.1 port: 6379 password: timeout: 3600ms #超时时间 lettuce: #若是在配置中配置了lettuce的pool属性,那么必须在pom.xml文件中加入commons-pool2的依赖。 pool: max-active: 8 #最大连接数 max-idle: 8 #最大空闲连接 默认8 max-wait: -1ms #默认-1 最大连接阻塞等待时间 min-idle: 0 #最小空闲连接 # jedis: # pool: # max-active: 8 #最大连接数 # max-idle: 8 #最大空闲连接 默认8 # max-wait: -1ms #默认-1 最大连接阻塞等待时间 # min-idle: 0 #最小空闲连接
在SpringBoot中,已经自动帮我们在容器中生成了一个RedisTemplate和一个StringRedisTemplate。
下面是SpringBoot中关于RedisTemplate自动装配的源码:
@Configuration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> 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; } }
从源码中可以看出,我们开发时会存在2个问题:
那么如何解决上述两个问题呢?
@ConditionalOnMissing注解:如果Spring容器中已经定义了id为redisTemplate的Bean, 那么自动装配的RedisTemplate不会实例化。 因此我们可以写一个配置类,配置Redisemplate。 若未自定义RedisTemplate,默认会对key进行jdk序列化。
当我们利用StringRedisSerializer,Jackson2JsonRedisSerializer和JdkSerializationRedisSerializer进行序列化时,对同一个数据进行序列化前后的结果如下表:
本案例中,我们对于Key采用stringRedisSerializer;而对于Value我们采用jackson2JsonRedisSerializer的序列化方式。
ObjectMapper是Jackson操作的核心,Jackson所有的json操作都是在ObjectMapper中实现的。 om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); 设置所有访问权限以及所有的实际类型都可序列化和反序列化 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); Jackson的ObjectMapper.DefaultTyping.NON_FINAL属性的作用。
在JDK1.8中的时间类,采用了一套了新的API,但是在反序列化中,会出现异常。
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of java.time.LocalDate (no Creators, like default construct, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
在SpringBoot中的解决方案:
在MAVEN中加入jackson-datatype-jsr310依赖。
<dependency> <groupId>com.fasterxml.jackson.datatype</groupId> <artifactId>jackson-datatype-jsr310</artifactId> </dependency>
配置Configuration中的ObjectMapper。
@Bean public ObjectMapper serializingObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); objectMapper.registerModule(new JavaTimeModule()); return objectMapper; }
package com.yyg.boot.config; import com.fasterxml.jackson.annotation.JsonAutoDetect; import com.fasterxml.jackson.annotation.PropertyAccessor; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.springframework.beans.factory.annotation.Value; import org.springframework.cache.CacheManager; 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; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ @Configuration public class RedisConfig { @Value("${spring.redis.timeout}") private Duration timeToLive = Duration.ZERO; /** * 由于原生的redis自动装配,在存储key和value时,没有设置序列化方式,故自己创建redisTemplate实例 */ @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); // 配置连接工厂 template.setConnectionFactory(factory); //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式) Jackson2JsonRedisSerializer jacksonSeial = new Jackson2JsonRedisSerializer(Object.class); ObjectMapper om = new ObjectMapper(); // 指定要序列化的域,field,get和set,以及修饰符范围,ANY是都有包括private和public om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); // 指定序列化输入的类型,类必须是非final修饰的,final修饰的类,比如String,Integer等会跑出异常 om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jacksonSeial.setObjectMapper(om); // 值采用json序列化 template.setValueSerializer(jacksonSeial); //使用StringRedisSerializer来序列化和反序列化redis的key值 template.setKeySerializer(new StringRedisSerializer()); // 设置hash key 和value序列化模式 template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(jacksonSeial); template.afterPropertiesSet(); return template; } /** * 解决jdk1.8中新时间API的序列化时出现com.fasterxml.jackson.databind.exc.InvalidDefinitionException的问题 */ @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<String> redisSerializer = new StringRedisSerializer(); Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class); //解决查询缓QQ号码转让地图存转换异常的问题 ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); // 配置序列化(解决乱码的问题) RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() .entryTtl(timeToLive) .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer)) .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer)) .disableCachingNullValues(); return RedisCacheManager.builder(factory) .cacheDefaults(config) .build(); } }
package com.yyg.boot.util; import org.springframework.beans.BeansException; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextAware; import org.springframework.stereotype.Service; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ @Service public class SpringContextUtil implements ApplicationContextAware { private static ApplicationContext context; @Override public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { SpringContextUtil.context = applicationContext; } public static <T> T getBean(String name, Class<T> requiredType) { return context.getBean(name, requiredType); } }
RedisTemplate模板类可以对Redis进行添加,删除,设置缓存过期时间等设置。
RedisTemplate中主要的API是:
opsForValue()集合使用说明
package com.yyg.boot.util; import lombok.extern.slf4j.Slf4j; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.util.CollectionUtils; import java.util.concurrent.TimeUnit; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ @Slf4j public class RedisUtil { //@Autowired //private static RedisTemplate<String, Object> redisTemplate; private static final RedisTemplate<String, Object> redisTemplate = SpringContextUtil.getBean("redisTemplate", RedisTemplate.class); /********************************************************************************** * redis-公共操作 **********************************************************************************/ /** * 指定缓存失效时间 * * @param key 键 * @param time 时间(秒) * @return */ public static boolean expire(String key, long time) { try { if (time > 0) { redisTemplate.expire(key, time, TimeUnit.SECONDS); } return true; } catch (Exception e) { log.error("【redis:指定缓存失效时间-异常】", e); return false; } } /** * 根据key 获取过期时间 * * @param key 键 不能为null * @return 时间(秒) 返回0代表为永久有效;如果该key已经过期,将返回"-2"; */ public static long getExpire(String key) { return redisTemplate.getExpire(key, TimeUnit.SECONDS); } /** * 判断key是否存在 * * @param key 键 * @return true 存在 false不存在 */ public static boolean exists(String key) { try { return redisTemplate.hasKey(key); } catch (Exception e) { log.error("【redis:判断{}是否存在-异常】", key, e); return false; } } /********************************************************************************** * redis-String类型的操作 **********************************************************************************/ /** * 普通缓存放入 * * @param key 键 * @param value 值 * @return true成功 false失败 */ public static boolean set(String key, Object value) { try { redisTemplate.opsForValue().set(key, value); return true; } catch (Exception e) { log.error("【redis:普通缓存放入-异常】", e); return false; } } /** * 普通缓存放入并设置时间 * * @param key 键 * @param value 值 * @param time 时间(秒) time要大于0 如果time小于等于0 将设置无限期 * @return true成功 false 失败 */ public static boolean set(String key, Object value, long time) { try { if (time > 0) { redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS); } else { set(key, value); } return true; } catch (Exception e) { log.error("【redis:普通缓存放入并设置时间-异常】", e); return false; } } /** * 递增 * * @param key 键 * @param delta 要增加几(大于0) * @return */ public static long incr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递增因子必须大于0"); } return redisTemplate.opsForValue().increment(key, delta); } /** * 递减 * * @param key 键 * @param delta 要减少几(小于0) * @return */ public static long decr(String key, long delta) { if (delta < 0) { throw new RuntimeException("递减因子必须大于0"); } return redisTemplate.opsForValue().increment(key, -delta); } /** * 删除缓存 * * @param key 可以传一个值 或多个 */ @SuppressWarnings("unchecked") public static void del(String... key) { if (key != null && key.length > 0) { if (key.length == 1) { redisTemplate.delete(key[0]); } else { redisTemplate.delete(CollectionUtils.arrayToList(key)); } } } /** * 获取缓存 * * @param key redis的key * @param clazz value的class类型 * @param <T> * @return value的实际对象 */ public static <T> T get(String key, Class<T> clazz) { Object obj = key == null ? null : redisTemplate.opsForValue().get(key); if (!obj.getClass().isAssignableFrom(clazz)) { throw new ClassCastException("类转化异常"); } return (T) obj; } /** * 获取泛型 * * @param key 键 * @return 值 */ public static Object get(String key) { return key == null ? null : redisTemplate.opsForValue().get(key); } }
RedisService类
package com.yyg.boot.service; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ public interface RedisService { void setObj(String key, Object obj, long timeout); Object getObj(String key); }
RedisServiceImpl类
package com.yyg.boot.service.impl; import com.yyg.boot.service.RedisService; import com.yyg.boot.util.RedisUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ @Service public class RedisServiceImpl implements RedisService { @Autowired private RedisUtil redisUtil; @Override public void setObj(String key, Object obj, long timeout) { redisUtil.set(key,obj,timeout); } @Override public Object getObj(String key) { return redisUtil.get(key); } }
package com.yyg.boot.web; import com.yyg.boot.domain.User; import com.yyg.boot.repository.UserRepository; import com.yyg.boot.service.RedisService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; /** * @Author 一一哥Sun * @Date Created in 2020/3/31 * @Description Description */ @RestController @RequestMapping("/user") public class UserController { @Autowired private UserRepository userRepository; @Autowired private RedisService redisService; @GetMapping("/{id}") public User findUserById(@PathVariable("id") Long id) { User user = (User) redisService.getObj("user" + id); if (user == null) { user = userRepository.findById(id).get(); redisService.setObj("user" + id, user, 1000 * 60 * 2); return user; } return user; } }
package com.yyg.boot; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @Author 一一哥Sun * @Date Created in 2020/4/8 * @Description Description */ @SpringBootApplication public class RedisApplication { public static void main(String[] args){ SpringApplication.run(RedisApplication.class,args); } }
在我的数据库中,有这样的数据。
Redis数据库中默认没有缓存数据。
在浏览器中输入地址,进行查询。
此时再次去Redis DesktopManager中查看,会发现已经有了一条缓存的数据。