美文网首页
SAAS-HRM-day6(Redis缓存+无限级树)

SAAS-HRM-day6(Redis缓存+无限级树)

作者: 程序员Darker | 来源:发表于2019-09-30 08:25 被阅读0次

    1. client和controller冲突解决(代码生成模板处理)

    1.1 问题发现

    突然发现以前的client包下client那里的@RequestMapping注解的地址都有一个user的前缀。如下所示:

    @FeignClient(value = "ZUUL-GATEWAY", configuration = FeignClientsConfiguration.class,
            fallbackFactory = CourseClientHystrixFallbackFactory.class)
    @RequestMapping("/user/course" )
    public interface CourseClient {
    

    这时候我需要删除user,使client的@RequestMapping里面的参数与controller里面的@RequestMapping里面的参数相同。

    这时候启动项目就会报错。但是错误信息很不明显,很难发现错误!这时候想要看到详细的日志错误信息有两种办法:第一种,把自己的日志配置文件取消掉,这时候启动项目报的错误就会比较详细!第二种:修改日志配置文件的日志等级,把等级调低,错误信息记录的就会更加详细!

    修改日志以后,再启动项目就会看到详细的错误,如下所示:

    Caused by: java.lang.IllegalStateException: Ambiguous mapping. Cannot map 'cn.wangningbo.hrm.client.CourseClient' method 
    public abstract cn.wangningbo.hrm.util.AjaxResult cn.wangningbo.hrm.client.CourseClient.save(cn.wangningbo.hrm.domain.Course)
    to {[/course/save],methods=[POST]}: There is already 'courseController' bean method
    public cn.wangningbo.hrm.util.AjaxResult cn.wangningbo.hrm.web.controller.CourseController.save(cn.wangningbo.hrm.domain.Course) mapped.
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.assertUniqueMethodMapping(AbstractHandlerMethodMapping.java:581) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping$MappingRegistry.register(AbstractHandlerMethodMapping.java:545) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.registerHandlerMethod(AbstractHandlerMethodMapping.java:267) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.lambda$detectHandlerMethods$1(AbstractHandlerMethodMapping.java:252) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at java.util.LinkedHashMap.forEach(LinkedHashMap.java:684) ~[na:1.8.0_111]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.detectHandlerMethods(AbstractHandlerMethodMapping.java:250) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.initHandlerMethods(AbstractHandlerMethodMapping.java:219) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.handler.AbstractHandlerMethodMapping.afterPropertiesSet(AbstractHandlerMethodMapping.java:189) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping.afterPropertiesSet(RequestMappingHandlerMapping.java:136) ~[spring-webmvc-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1758) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1695) ~[spring-beans-5.0.9.RELEASE.jar:5.0.9.RELEASE]
        ... 50 common frames omitted
    

    这个错误产生的原因:由于修改了模块hrm_course_interface下的client包的@RequestMapping("/course" )与hrm_course_service模块下controller的@RequestMapping("/course" )相同,启动项目就会在client端产生一个本地代理对象,而产生本地代理对象的地址就是/course,这样最终就会和引用进来的controller的地址/course产生冲突。

    1.2 问题解决

    在二级子模块course下新建一个三级子模块,取名client,这里面就是专用于存放client的。

    这时候二级子模块course下就有了三个子模块,分别是client、interface、service。把原来interface模块里面的client包整个就全部剪切掉放到client模块里!这时候会报错,要导包!也要依赖于interface!

            <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_basic_util</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--不能直接依赖starter,有自动配置,而消费者是不需要额。-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!--不能全部引入mybatis-plus,这是要做数据库操作,这里是不需要的,只需引入核心包解决错误而已-->
            <dependency>
                <groupId>com.baomidou</groupId>
                <artifactId>mybatis-plus</artifactId>
                <version>2.2.0</version>
            </dependency>
            <!--客户端feign支持-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
            <!--依赖于interface 公共代码-->
            <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_course_interface</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    

    简单分析这样做的目的:我自己不可能调用自己的服务,我如果需要调用自己直接调用自己的service调用自己的Mapper即可,所以service不需要依赖于client模块,service只需要依赖于interface即可!client是对外暴露服务给其他模块用的!

    最终模块存放情况:

    1. client模块:client
    2. interface模块:domain、query
    3. service模块:config、mapper、service、util、web\controller

    最终依赖情况:client依赖于interface,service依赖于interface

    2. 无限级树(课程类型树)

    2.1 场景分析

    1. 后台管理页面类型树
    2. 前台用户高级搜索时候类型树的选择

    2.2 后台实现--类型树查询

    2.2.1 方案分析

    1. 递归-(不采纳-->因为发送很多条sql效率低)
    2. 循环-(采纳)

    2.2.2 方案实现(两种方案都有代码)

    简单逻辑分析:前端发送一个请求到后台,需要获取到一个类型树!

    1. controller
        //类型树
        @RequestMapping(value = "/treeData",method = RequestMethod.GET)
        public List<CourseType> treeData(){
            //数据库中0就是顶级
            return courseTypeService.queryTypeTree(0L);
        }
    
    1. IService
        List<CourseType> queryTypeTree(Long pid);
    
    1. CourseType.java里面新建属性用来存放儿子
        @TableField(exist = false) //用来存放儿子
        private List<CourseType> children = new ArrayList<>();
    
    1. ServiceImpl
        @Override
        public List<CourseType> queryTypeTree(Long pid) {
            //递归
    //        return getCourseTypesRecursion(pid);
            // 循环
            return getCourseTypesLoop(pid);
        }
    
        /**
         * 循环
         * @param pid
         * @return
         */
        private List<CourseType> getCourseTypesLoop(Long pid) {
            List<CourseType> result = new ArrayList<>();
            //1 查询所有类型
            List<CourseType> allTypes = courseTypeMapper.selectList(null);
            //建立id和CourseType的关联关系
            Map<Long, CourseType> allTypesDto = new HashMap<>();
            for (CourseType allType : allTypes) {
                allTypesDto.put(allType.getId(), allType);
            }
            //2 遍历判断是否是第一级  pid为传入id,
            for (CourseType type : allTypes) {
                Long pidTmp = type.getPid();
                //2.1 是。直接加入返回列表
                if (pidTmp.longValue() == pid.longValue()) {
                    result.add(type);
                } else {
                    //2.2 不是。要把自己作为父亲儿子
                    //方案:通过pid获取父亲。通过map获取
                    CourseType parent = allTypesDto.get(pidTmp);
                    //获取父亲儿子集合,把自己加进去
                    parent.getChildren().add(type);
                }
            }
            return result;
        }
    
        /**
         * 递归
         * @param pid
         * @return
         */
        private List<CourseType> getCourseTypesRecursion(Long pid) {
            // 方案1:递归-自己调用自己,要有出口
            List<CourseType> children = courseTypeMapper.selectList(new EntityWrapper<CourseType>().eq("pid", pid));
            // 出口
            if (children == null || children.size() < 1)
                return null;
            for (CourseType child : children) {
                // 自己调用自己
                List<CourseType> courseTypes = queryTypeTree(child.getId());
                child.setChildren(courseTypes);
            }
            return children;
        }
    

    3. 课程类型树优化方案

    3.1 为什么要优化?

    每次使用都要从数据库查询一次,这样就会存在一些问题

    使用的地方和问题:

    1. 后台管理管理员的页面

      课程类型树,在后面添加课程时会反复使用。就算每个人使用时只查询一次,如果人比较多.也要对数据库进行频繁操作

    2. 前台用户使用的页面

      缓存还不够优化,如果一亿并发,就会访问redis一亿次.对缓存服务器也是一种压力.

    3.2 优化方案

    1. 后台管理员

      缓存:用内存查询替换数据库磁盘查询.(应用场景:经常查询,很少修改的数据)

    2. 前端用户

      页面静态化:以不发请求静态页面代替要发请求静态页面或者动态页面.没有对后台数据获取.

      不能用缓存,还是会高并发访问缓存中数据.

    4. 课程类型后台缓存优化

    4.1 常见缓存实现方案

    1. jpa,mybatis二级缓存,默认情况下,不支持集群环境使用.
    2. 中央缓存:redis/memcached

    4.2 交互图

    [图片上传失败...(image-65fe02-1569803130923)]

    4.3 数据存储

    1. 数据存放:List<CourseType>
      • 把对象转换为json字符串,以json字符串方式进行存储.
      • jedis.set(“courseTypes”,jsonStr)
    2. 数据获取:json字符串
      • jedis.get(“courseTypes”)
      • 把json字符串转换为java对象进行返回

    java对象和json字符串之间相互转换?常见json框架-json-lib,jackson,gson,fastjson(阿里巴巴)。相互对比以后发现阿里巴巴的fastjson最好!

    4.4 实现

    4.4.1 redis项目搭建步骤分析

    1. 创建项目
    2. 导包
    3. 配置
    4. 入口类
    5. 日志
    6. 网关
    7. swagger
    8. 测试swagger页面

    4.4.2 redis项目搭建步骤实现

    1. 创建项目
      项目结构:
    • hrm_parent
      • hrm_basic_parent
        • hrm_basic_redis_client
        • hrm_basic_redis_common
        • hrm_basic_redis_service
    1. 导包

    common

    <!--不能直接依赖starter,有自动配置,而消费者是不需要额。-->
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
    </dependency>
    

    client

            <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_basic_util</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_basic_redis_common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
            <!--不能直接依赖starter,有自动配置,而消费者是不需要额。-->
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
    
            <!--客户端feign支持-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-openfeign</artifactId>
            </dependency>
    
            <!-- https://mvnrepository.com/artifact/com.alibaba/fastjson  调用者需要转换-->
            <dependency>
                <groupId>com.alibaba</groupId>
                <artifactId>fastjson</artifactId>
                <version>1.2.58</version>
            </dependency>
    

    service

    <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_basic_redis_common</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-web</artifactId>
            </dependency>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-starter-test</artifactId>
                <scope>test</scope>
            </dependency>
            <!-- Eureka 客户端依赖 -->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
            </dependency>
    
            <!--引入swagger支持-->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger2</artifactId>
                <version>2.9.2</version>
            </dependency>
            <!-- https://mvnrepository.com/artifact/io.springfox/springfox-swagger-ui -->
            <dependency>
                <groupId>io.springfox</groupId>
                <artifactId>springfox-swagger-ui</artifactId>
                <version>2.9.2</version>
            </dependency>
    
            <!--配置中心支持-->
            <dependency>
                <groupId>org.springframework.cloud</groupId>
                <artifactId>spring-cloud-starter-config</artifactId>
            </dependency>
    
            <!--redis客户端-jedis-->
            <!-- https://mvnrepository.com/artifact/redis.clients/jedis -->
            <dependency>
                <groupId>redis.clients</groupId>
                <artifactId>jedis</artifactId>
                <version>2.9.0</version>
            </dependency>
    
    1. 配置

    service那里application.yml

    server:
      port: 9005
    spring:
      application:
        name: hrm-redis
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:7001/eureka
      instance:
        prefer-ip-address: true
    
    1. 入口类

    service那里的入口类

    package cn.wangningbo.hrm;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    
    @SpringBootApplication
    @EnableEurekaClient
    public class Redis9005Application {
        public static void main(String[] args) {
            SpringApplication.run(Redis9005Application.class, args);
        }
    }
    
    1. 日志

    resources下存放一个logback日志的配置文件就行,名字固定logback-spring.xml

    <?xml version="1.0" encoding="UTF-8"?>
    <!--
    scan:当此属性设置为true时,配置文件如果发生改变,将会被重新加载,默认值为true。
    scanPeriod:设置监测配置文件是否有修改的时间间隔,如果没有给出时间单位,默认单位是毫秒当scan为true时,此属性生效。默认的时间间隔为1分钟。
    debug:当此属性设置为true时,将打印出logback内部日志信息,实时查看logback运行状态。默认值为false。
    -->
    <configuration scan="false" scanPeriod="60 seconds" debug="false">
        <!-- 定义日志的根目录 -->
        <property name="LOG_HOME" value="/hrm/" />
        <!-- 定义日志文件名称 -->
        <property name="appName" value="hrm-redis"></property>
        <!-- ch.qos.logback.core.ConsoleAppender 表示控制台输出 -->
        <appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
            <!--
            日志输出格式:
                %d表示日期时间,
                %thread表示线程名,
                %-5level:级别从左显示5个字符宽度
                %logger{50} 表示logger名字最长50个字符,否则按照句点分割。 
                %msg:日志消息,
                %n是换行符
            -->
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n</pattern>
            </layout>
        </appender>
    
        <!-- 滚动记录文件,先将日志记录到指定文件,当符合某个条件时,将日志记录到其他文件 -->  
        <appender name="appLogAppender" class="ch.qos.logback.core.rolling.RollingFileAppender">
            <!-- 指定日志文件的名称
               /hrm/hrm-course/hrm-course.log
            -->
            <file>${LOG_HOME}/${appName}/${appName}.log</file>
            <!--
            当发生滚动时,决定 RollingFileAppender 的行为,涉及文件移动和重命名
            TimeBasedRollingPolicy: 最常用的滚动策略,它根据时间来制定滚动策略,既负责滚动也负责出发滚动。
            -->
            <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
                <!--
                滚动时产生的文件的存放位置及文件名称 %d{yyyy-MM-dd}:按天进行日志滚动 
                %i:当文件大小超过maxFileSize时,按照i进行文件滚动
                -->
                <fileNamePattern>${LOG_HOME}/${appName}/${appName}-%d{yyyy-MM-dd}-%i.log</fileNamePattern>
                <!-- 
                可选节点,控制保留的归档文件的最大数量,超出数量就删除旧文件。假设设置每天滚动,
                且maxHistory是365,则只保存最近365天的文件,删除之前的旧文件。注意,删除旧文件是,
                那些为了归档而创建的目录也会被删除。
                -->
                <MaxHistory>365</MaxHistory>
                <!-- 
                当日志文件超过maxFileSize指定的大小是,根据上面提到的%i进行日志文件滚动 注意此处配置SizeBasedTriggeringPolicy是无法实现按文件大小进行滚动的,必须配置timeBasedFileNamingAndTriggeringPolicy
                -->
                <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                    <maxFileSize>100MB</maxFileSize>
                </timeBasedFileNamingAndTriggeringPolicy>
            </rollingPolicy>
            <!-- 日志输出格式: -->     
            <layout class="ch.qos.logback.classic.PatternLayout">
                <pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} [ %thread ] - [ %-5level ] [ %logger{50} : %line ] - %msg%n</pattern>
            </layout>
        </appender>
    
        <!-- 
            logger主要用于存放日志对象,也可以定义日志类型、级别
            name:表示匹配的logger类型前缀,也就是包的前半部分
            level:要记录的日志级别,包括 TRACE < DEBUG < INFO < WARN < ERROR
            additivity:作用在于children-logger是否使用 rootLogger配置的appender进行输出,
            false:表示只用当前logger的appender-ref,true:
            表示当前logger的appender-ref和rootLogger的appender-ref都有效
        -->
        <!-- hibernate logger -->
        <logger name="cn.wangningbo" level="debug" />
        <!-- Spring framework logger -->
        <logger name="org.springframework" level="debug" additivity="false"></logger>
    
    
    
        <!-- 
        root与logger是父子关系,没有特别定义则默认为root,任何一个类只会和一个logger对应,
        要么是定义的logger,要么是root,判断的关键在于找到这个logger,然后判断这个logger的appender和level。 
        -->
        <root level="info">
            <appender-ref ref="stdout" />
            <appender-ref ref="appLogAppender" />
        </root>
    </configuration> 
    
    1. 网关

    在网关的配置文件里面配置一下

    zuul:
      routes: 
        redis.serviceId: hrm-redis # 服务名
        redis.path: /redis/** # 把redis打头的所有请求都转发给hrm-redis
    
    1. swagger.服务端配置
    package cn.wangningbo.hrm.config;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import springfox.documentation.builders.ApiInfoBuilder;
    import springfox.documentation.builders.PathSelectors;
    import springfox.documentation.builders.RequestHandlerSelectors;
    import springfox.documentation.service.ApiInfo;
    import springfox.documentation.service.Contact;
    import springfox.documentation.spi.DocumentationType;
    import springfox.documentation.spring.web.plugins.Docket;
    import springfox.documentation.swagger2.annotations.EnableSwagger2;
    
    @Configuration
    @EnableSwagger2
    public class Swagger2 {
    
        @Bean
        public Docket createRestApi() {
            return new Docket(DocumentationType.SWAGGER_2)
                    .apiInfo(apiInfo())
                    .select()
                    //对外暴露服务的包,以controller的方式暴露,所以就是controller的包.
                    .apis(RequestHandlerSelectors.basePackage("cn.wangningbo.hrm.controller"))
                    .paths(PathSelectors.any())
                    .build();
        }
    
    
        private ApiInfo apiInfo() {
            return new ApiInfoBuilder()
                    .title("中央缓存api")
                    .description("中央缓存接口文档说明")
                    .contact(new Contact("wangningbo", "", "wang_ning_bo163@163.com"))
                    .version("1.0")
                    .build();
        }
    
    }
    
    1. 测试swagger页面

      http://localhost:9005/swagger-ui.html
      http://localhost:9527/swagger-ui.html

    4.4.3 缓存服务实现

    4.4.3.1 redis那里接口实现

    4.4.3.1.1 步骤分析
    1. client模块
    2. service模块
    4.4.3.1.2 步骤实现
    1. client模块
    package cn.wangningbo.hrm.client;
    
    import org.springframework.cloud.openfeign.FeignClient;
    import org.springframework.cloud.openfeign.FeignClientsConfiguration;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    
    @FeignClient(value = "HRM-REDIS", configuration = FeignClientsConfiguration.class,
            fallbackFactory = RedisClientFallbackFactory.class)//服务提供者的名字
    @RequestMapping("/cache")
    public interface RedisClient {
        @PostMapping
        void set(@RequestParam("key") String key, @RequestParam("value") String value);
    
        @GetMapping
        String get(@RequestParam("key") String key);
    }
    
    package cn.wangningbo.hrm.client;
    
    import feign.hystrix.FallbackFactory;
    import org.springframework.stereotype.Component;
    
    @Component
    public class RedisClientFallbackFactory implements FallbackFactory<RedisClient> {
        @Override
        public RedisClient create(Throwable throwable) {
            return new RedisClient() {
                @Override
                public void set(String key, String value) {
    
                }
    
                @Override
                public String get(String key) {
                    return null;
                }
            };
        }
    }
    
    1. service模块

    这里是使用jedis操作redis!jedis的包已经导入过了!

    redis配置文件:redis.properties

    redis.host=127.0.0.1
    redis.port=6379
    redis.password=root
    redis.timeout=3000
    

    操作redis的工具类

    package cn.wangningbo.hrm.util;
    
    import redis.clients.jedis.Jedis;
    import redis.clients.jedis.JedisPool;
    import redis.clients.jedis.JedisPoolConfig;
    
    import java.io.IOException;
    import java.util.Properties;
    
    /**
     * 获取连接池对象
     */
    public enum RedisUtils {
        INSTANCE;
        static JedisPool jedisPool = null;
    
        static {
            //1 创建连接池配置对象
            JedisPoolConfig config = new JedisPoolConfig();
            //2 进行配置-四个配置
            config.setMaxIdle(1);//最小连接数
            config.setMaxTotal(11);//最大连接数
            config.setMaxWaitMillis(10 * 1000L);//最长等待时间
            config.setTestOnBorrow(true);//测试连接时是否畅通
            //3 通过配置对象创建连接池对象
            Properties properties = null;
            try {
                properties = new Properties();
                properties.load(RedisUtils.class.getClassLoader().getResourceAsStream("redis.properties"));
            } catch (IOException e) {
                e.printStackTrace();
            }
            String host = properties.getProperty("redis.host");
            String port = properties.getProperty("redis.port");
            String password = properties.getProperty("redis.password");
            String timeout = properties.getProperty("redis.timeout");
            jedisPool = new JedisPool(config, host, Integer.valueOf(port), Integer.valueOf(timeout), password);
        }
    
        //获取连接
        public Jedis getSource() {
            return jedisPool.getResource();
        }
    
        //关闭资源
        public void closeSource(Jedis jedis) {
            if (jedis != null) {
                jedis.close();
            }
    
        }
    
        /**
         * 设置字符值
         *
         * @param key
         * @param value
         */
        public void set(String key, String value) {
            Jedis jedis = getSource();
            jedis.set(key, value);
            closeSource(jedis);
        }
    
        /**
         * 设置
         *
         * @param key
         * @param value
         */
        public void set(byte[] key, byte[] value) {
            Jedis jedis = getSource();
            jedis.set(key, value);
            closeSource(jedis);
        }
    
        /**
         * @param key
         * @return
         */
        public byte[] get(byte[] key) {
            Jedis jedis = getSource();
            try {
                return jedis.get(key);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                closeSource(jedis);
            }
            return null;
    
        }
    
        /**
         * 设置字符值
         *
         * @param key
         */
        public String get(String key) {
            Jedis jedis = getSource();
            try {
                return jedis.get(key);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                closeSource(jedis);
            }
    
            return null;
    
        }
    }
    

    controller

    package cn.wangningbo.hrm.controller;
    
    import cn.wangningbo.hrm.util.RedisUtils;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/cache")
    public class RedisController {
        @PostMapping
        public void set(@RequestParam("key") String key, @RequestParam("value") String value) {
            RedisUtils.INSTANCE.set(key, value);
        }
    
        @GetMapping
        public String get(@RequestParam("key") String key) {
            return RedisUtils.INSTANCE.get(key);
        }
    }
    
    1. 测试

      启动redis服务端命令:D:\soft\redis>redis-server.exe redis.windows.conf

      swagger测试:自身和网关
      postman测试接口

    4.4.3.2 其他项目接口调用

    4.4.3.2.1 步骤分析
    1. 导包
    2. 入口扫描
    3. service
    4. cache
    4.4.3.2.2 步骤实现
    1. 导包
            <!--内部调用redis模块接口-->
            <dependency>
                <groupId>cn.wangningbo.hrm</groupId>
                <artifactId>hrm_basic_redis_client</artifactId>
                <version>1.0-SNAPSHOT</version>
            </dependency>
    
    1. 入口扫描

      入口类那里配置一下,内部调用要打注解@EnableFeignClients和@MapperScan("cn.wangningbo.hrm.mapper")

    package cn.wangningbo.hrm;
    
    import org.mybatis.spring.annotation.MapperScan;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.cloud.netflix.eureka.EnableEurekaClient;
    import org.springframework.cloud.openfeign.EnableFeignClients;
    
    @SpringBootApplication
    @EnableEurekaClient
    @MapperScan("cn.wangningbo.hrm.mapper")
    @EnableFeignClients
    public class Course9002Application {
        public static void main(String[] args) {
            SpringApplication.run(Course9002Application.class, args);
        }
    }
    
    1. service

    简单业务逻辑分析:由于课程类型树是很常用的但又很少修改的数据,所以要把课程类型树放入redis缓存之中!当查询课程类型树的时候先去redis缓存中获取,如果缓存中有,就直接拿走使用,如果缓存中没有,就去数据库中查询,查询完数据以后先放入缓存中,再返回给查询者!对课程类型表进行增删改的时候,也要同步到redis缓存中!

    改造获取课程类型树的方法

        @Autowired
        private CourseTypeCache courseTypeCache;
    
        @Override
        public List<CourseType> queryTypeTree(Long pid) {
            // 从redis缓存中获取课程类型树
            List<CourseType> courseTypes = courseTypeCache.getCourseTypes();
            // 判断redis中有没有获取到课程类型树
            if (courseTypes==null||courseTypes.size()<1){
                // 进入到了if里面就说明缓存中没有,要从db库中获取课程类型树
                return getCourseTypesLoop(pid);//调用循环的方式获取课程类型树
            }
            // 返回redis缓存中的课程类型树
            return courseTypes;
        }
    

    改造对课程类型表的添加、修改、删除方法!操作这些的时候也要同步操作redis缓存

        @Override
        public boolean insert(CourseType entity) {
            courseTypeMapper.insert(entity);
            // 重新查询一下课程类型树,更新到redis缓存
            List<CourseType> courseTypes = queryTypeTree(0L);
            courseTypeCache.setCourseTypes(courseTypes);
            return true;
        }
    
        @Override
        public boolean deleteById(Serializable id) {
            courseTypeMapper.deleteById(id);
            // 重新查询一下课程类型树,更新到redis缓存
            List<CourseType> courseTypes = queryTypeTree(0L);
            courseTypeCache.setCourseTypes(courseTypes);
            return true;
        }
    
        @Override
        public boolean updateById(CourseType entity) {
            courseTypeMapper.updateById(entity);
            // 重新查询一下课程类型树,更新到redis缓存
            List<CourseType> courseTypes = queryTypeTree(0L);
            courseTypeCache.setCourseTypes(courseTypes);
            return true;
        }
    
    1. cache
    package cn.wangningbo.hrm.cache;
    
    import cn.wangningbo.hrm.client.RedisClient;
    import cn.wangningbo.hrm.domain.CourseType;
    import com.alibaba.fastjson.JSONArray;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    
    import java.util.List;
    
    @Component
    public class CourseTypeCache {
        @Autowired
        private RedisClient redisClient;
    
        private static final String TYPETREEDATA_IN_REDIS = "typetreedata_in_redis";
    
        /**
         * 从redis获取数据
         *
         * @return
         */
        public List<CourseType> getCourseTypes() {
            String redisData = redisClient.get(TYPETREEDATA_IN_REDIS);
            return JSONArray.parseArray(redisData, CourseType.class);
        }
    
        /**
         * 设置数据到redis
         *
         * @param courseTypesDb
         */
        public void setCourseTypes(List<CourseType> courseTypesDb) {
            String jsonStr = JSONArray.toJSONString(courseTypesDb);
            redisClient.set(TYPETREEDATA_IN_REDIS, jsonStr);
        }
    }
    

    5. 高级&面试题

    1. 使用缓存好处?
      • 减轻数据库压力
      • 提高访问速度,增强用户体验
    2. 我们缓存数据很多的时候怎么办? 使用redis集群
      • (集群方式1)主从复制-解决单个主故障
      • (集群方式2)哨兵模式-每个节点数据都是一样
      • (集群方式3)redis-cluster: 单点故障,高并发,大量数据
    3. 缓存穿透怎么解决?
      • 产生原因:高并发访问数据库中不存在数据,放入缓存的数据也没有,击穿缓存每次都要查询数据库.
      • 解决办法有很多种,我列举一下两种
        • (1)最常见的则是采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的bitmap中,一个一定不存在的数据会被这个bitmap拦截掉,从而避免了对底层数据库的查询压力。
        • (2)另外也有一个更为简单粗暴的方法,如果一个查询返回的数据为空(不管是数据不存在,还是系统故障),仍然把这个空结果进行缓存,但它的过期时间会很短,最长不超过五分钟。

    我上面的那个课程类型树就存在缓存穿透的问题!下面进行解决!

    改造获取课程类型树的方法

        @Override
        public List<CourseType> queryTypeTree(Long pid) {
            // 从redis缓存中获取课程类型树
            List<CourseType> courseTypes = courseTypeCache.getCourseTypes();
            // 判断redis中有没有获取到课程类型树
            if (courseTypes == null || courseTypes.size() < 1) {
                // 进入到了if里面就说明缓存中没有,要从db库中获取课程类型树 // 调用循环的方式获取课程类型树
                List<CourseType> courseTypesDb = getCourseTypesLoop(pid);
                // 判断数据库中是否查到了数据
                if (courseTypesDb == null || courseTypesDb.size() < 1)
                    // 如果数据库中没有查到,就返回一个空回去 // 并设置一个很短的过期时间,我这里过期时间为5分钟
                    courseTypesDb = new ArrayList<>();
                // 把查询的结果放入缓存中
                courseTypeCache.setCourseTypes(courseTypesDb);
                return courseTypesDb;
            }
            // 返回redis缓存中的课程类型树
            return courseTypes;
        }
    

    改造redis服务的controller层的set方法,把那些为"[]""的value设置为5分钟后过期,否则就设置永不过期

    package cn.wangningbo.hrm.controller;
    
    import cn.wangningbo.hrm.util.RedisUtils;
    import org.springframework.web.bind.annotation.*;
    
    @RestController
    @RequestMapping("/cache")
    public class RedisController {
        @PostMapping
        public void set(@RequestParam("key") String key, @RequestParam("value") String value) {
            if (value.equals("[]"))
                RedisUtils.INSTANCE.getSource().setex(key, 5 * 60, value);
            else
                RedisUtils.INSTANCE.set(key, value);
        }
    
        @GetMapping
        public String get(@RequestParam("key") String key) {
            return RedisUtils.INSTANCE.get(key);
        }
    }
    
    1. 缓存击穿怎么解决?
      • 产生原因:一些key同时过期,又来高并发访问. 直接高并发访问数据库
      • 解决办法:让热点数据永远不过期
    2. 缓存雪崩怎么解决?
      • 产生原因:一堆key同时过期
      • 解决办法有很多种,我列举以下两种:
        • (1)设置过期时间不一致
        • (2)热点数据永远不过期

    相关文章

      网友评论

          本文标题:SAAS-HRM-day6(Redis缓存+无限级树)

          本文链接:https://www.haomeiwen.com/subject/vxlcpctx.html