阅读 87

SpringBoot2.x系列教程28--SpringBoot中利用Redis实现缓存功能详细教程

前言

随着互联网中Web2.0网站项目的兴起,传统的关系数据库在应付Web2.0网站,特别是超大规模和高并发的SNS社交类型的网站时,已经显得力不从心,例如:

1️⃣. High performance - 对数据库高并发读写的需求。

2️⃣. Huge Storage - 对海量数据的高效存储和访问的需求。

3️⃣. High Scalability && High Availability- 对数据库的高可扩展性和高可用性的需求。

传统的关系型数据库,例如Oracle、MySQL等都很难满足高并发、实时海量存储的需求,所以这时候就得需要有NoSQL类型的数据库,来解决高并发、实时海量存储带来的挑战,尤其是大数据应用的难题。

所以今天 壹哥 带各位学习如何在SpringBoot中关联Redis,利用Redis实现数据库查询的缓存功能,以此来提高我们项目的运行效率。

一. Redis简介

Redis的内容有很多,如果你没有Redis的基础,是需要专门学习的。本文不是专门的Redis教程,所以这里只对Redis进行简单介绍,各位可以阅读我其他的Redis教程。

1. Redis概念

Redis:REmote DIctionary Server(远程字典服务器),它是一个完全开源免费且遵守BSD协议,用C语言开发的,高性能的key-value型的分布式内存数据库,而且它还是一种基于内存运行并支持持久化的NoSQL数据库,可用于缓存、事件发布或订阅、高速队列等场景。该数据库使用ANSI C语言编写,支持网络,提供字符串、哈希、列表、队列、集合结构直接存取,基于内存,并且可持久化。Redis是当前最热门的NoSql数据库之一,也被人们称为数据结构服务器。

2. 环境配置要求

要想实现本文效果,各位需要配置如下开发环境。

  • Springboot 2.2.5;

  • Redis 3.2.x;

  • Redis可视化工具Redis Desktop Manager

二. SpringBoot整合Redis实现步骤

接下来我就带大家在SpringBoot中实现Redis的整合。

1. 创建Web项目

我们按照之前的经验,创建一个SpringBoot的Web程序,具体过程略。

2. 添加依赖

接下来在pom.xml文件中添加核心依赖包,如下所示:

 <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> 复制代码

3. 添加yml配置文件

SpringBoot集成Redis主要是使用RedisTemplate类进行操作,但是在SpringBoot2.0以后,底层默认访问的不再是Jedis而是lettuce。

3.1 jedis客户端和lettuce客户端的区别

  • jedis采用的是直连redis server,在多线程之间公用一个jedis实例,是线程不安全的。想要避免线程不安全,可以使用连接池pool,这样每个线程单独使用一个jedis实例。但是线程过多时,带来的是redis server的负载较大,有点类似BIO模式。

  • lettuce采用netty连接redis server,实例在多个线程间共享,不存在线程不安全的情况,这样可以减少线程数量。当然在特殊情况下,lettuce也可以使用多个实例,有点类似NIO模式。

3.2 yml配置文件

创建一个application.yml文件,在这里配置MySQL与Redis数据库的信息。

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 #最小空闲连接 复制代码

4. RedisConfig配置类简介

4.1 RedisTemplate自动装配简介

在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个问题:

  • RdisTemplate的泛型是<Object,Object>,我们在进行缓存时写代码不是很方便,因为一般我们的key是String类型,所以我们需要一个<String,Object>的泛型。

  • RedisTemplate没有设置数据存储在Redis时,Key和Value的序列化方式(采用默认的JDK序列化方式)。

4.2 问题解决思路

那么如何解决上述两个问题呢?我们可以考虑采用下面的思路来实现:

结合@ConditionalOnMissing注解,检查Spring容器中是否已经定义了id为redisTemplate的Bean,

否则自动装配的RedisTemplate不会实例化。

因此我们可以写一个配置类,配置Redisemplate对象,若未自定义RedisTemplate,默认会对key进行jdk序列化。

所以接下来我们就要创建一个RedisTemplate对象。

4.3 RedisSerializer序列化器简介

创建Redis对象时,我们还需要利用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属性的作用。 复制代码

4.4 Jackson序列化特性简介

在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) 复制代码

5. 创建RedisConfig配置类

我们需要创建一个RedisConfig,在这个类中对Redis进行配置,创建一个RedisTemplate对象,并对Redis的缓存进行设置。

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);         //解决查询缓存转换异常的问题         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();     } } 复制代码

6. 创建SpringContextUtil工具类

为了方便获取ApplicationContext对象,这里我封装了一个SpringContextUtil工具类。

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);     } } 复制代码

7. 创建RedisUtil工具类

RedisTemplate模板类可以对Redis进行添加,删除,设置缓存过期时间等设置。
RedisTemplate中主要的API是:
opsForValue()集合使用说明

  • set(K key,V value) 新建缓存
    redisTemplate.opsForValue().set("key","value");

  • get(Object key) 获取缓存
    redisTemplate.opsForValue().get("key");

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);     } } 复制代码

8. 创建RedisService及其实现类

为了操作方便,我封装一个RedisService接口类,在这里封装存值和取值方法。

8.1 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); } 复制代码

8.2 RedisServiceImpl类

然后对RedisService接口进行实现。

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);     } } 复制代码

9. 创建UserController测试接口

接着我创建一个UserController,编写几个URL接口进行测试。

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;     } } 复制代码

10. 创建入口类

最后我们再编写一个程序入口类,启动项目进行测试。

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);     } } 复制代码

11. 项目结构

这里是完整的项目代码结构,各位可以参考创建。

12. 启动项目进行测试

12.1 数据库初始状态

在我的MySQL数据库中,存有这样的数据。

Redis数据库中默认是没有缓存数据的。

12.2 测试缓存接口

接下来我们在浏览器中输入接口地址,进行查询。

这时候我们再次去Redis DesktopManager中查看,会发现Redis中已经有了一条缓存的数据,就是我们刚才从MySQL中查询过来的用户信息。

结语

至此,壹哥 就带各位实现了SpringBoot中对Redis的整合。

今日小作业:

在学生信息系统中,新增一个登陆功能,请在登录的业务逻辑中,利用Redis对登录的用户进行存储,实现Session共享效果。


作者:一一哥Sun
链接:https://juejin.cn/post/7168977656392876039

文章分类
代码人生
版权声明:本站是系统测试站点,无实际运营。本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 XXXXXXo@163.com 举报,一经查实,本站将立刻删除。
相关推荐