SpringBoot(六) 整合数据访问

一.整合JDBC

1.引入相关依赖

(1) mysql的驱动依赖

       “mysql-connector-java"是MySQL官方提供的JDBC驱动包,其提供了最基本的数据库驱动、数据连接、数据操作等功能以及一些接口,是JDBC连接MySQL数据库时必须依赖的jar包。换句话说,mysql驱动给jdbc提供了操作的数据源。



   mysql
   mysql-connector-java
   5.1.47

注意:由于SpringBoot版本仲裁的mysql驱动版本为8.0,而本地mysql数据库为5.6,二者版本不匹配。因此根据依赖的就近原则,我们在version中直接指定版本为5.1.47即可。

(2)JDBC依赖

        JDBC代表Java数据库连接,它是用于Java编程语言和数据库(可以是mysql、oracle、redis等等,取决于JDBC使用的数据源驱动)之间的数据库无关连接的标准Java API,封装了大量的数据操作。换句话说:JDBC是用于在Java语言编程中与数据库连接的API。



    org.springframework.boot
    spring-boot-starter-jdbc

SpringBoot(六) 整合数据访问_第1张图片

 2. 相关配置说明

1.1 自动配置类原理

(1)DataSourceAutoConfiguration:数据源的自动配置

  • 自定义修改数据源的相关配置:spring.datasource
  • 数据库连接池的相关配置:当没有自己配置数据库连接池时,系统会帮我们自动配置Hikari数据源

SpringBoot(六) 整合数据访问_第2张图片(2)DataSourceTransactionManagerAutoConfiguration:事务管理器的自动配置

(3)JdbcTemplateAutoConfiguration:JdbcTemplate的自动配置,封装了CURD,操作相应的数据源

1.2 修改配置项

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/education?serverTimezone=UTC&userUnicode=true&characterEncoding=utf-8
    username: root
    password: my060321
    driver-class-name: com.mysql.jdbc.Driver

3. 使用JDBC

@SpringBootTest
class Boot04ApplicationTests {

    @Autowired
    JdbcTemplate jdbcTemplate;

    @Test
    void contextLoads() {
        //List ids = jdbcTemplate.queryForList("select sno from student",Integer.class);
        //System.out.println(ids);

        List> list_maps = jdbcTemplate.queryForList("select * from student");
        System.out.println(list_maps);
    }

}

注意:直接使用queryForList查询对象列表会报错,原因是该方法不支持自定义bean,只是支持Integer,String这些基本类型,它只返回一列的List(某属性的List)

二.整合Druid数据源

        Druid是Java语言中最好的数据库连接池。Druid能够提供强大的可视化监控和扩展功能。其数据吞吐量大、支持流式数据摄入和实时、查询灵活且快速,具有优秀的性能。

1.引入第三方数据源的starter



   com.alibaba
   druid-spring-boot-starter
   1.1.17

2. 自动配置分析

        引入starter后,启动器帮我们导入了druid数据源、日志、相关的自动配置类,接下来我们分析一下自动配置类帮我们干了什么。

SpringBoot(六) 整合数据访问_第3张图片

  •  DruidSpringAopConfiguration:配置容器的SpringBean监控,监控容器内组件,配置项为spring.datasource.druid.aop-patterns
  • DruidStatViewServletConfiguration:druid监控页面配置,提供可视化,默认开启,配置项为spring.datasource.druid.stat-view-servlet
  • DruidWebStatFilterConfiguration:web访问监控,统计web请求/交互数据,默认开启,配置项为spring.datasource.druid.web-stat-filter
  • DruidFilterConfiguration:所有druid自己的filter配置项(比如防火墙、编码过滤),配置项为spring.datasource.druid.filter

3.修改配置项

3.1 配置项说明

(1)JDBC配置

spring.datasource.druid.url= # 或spring.datasource.url=
spring.datasource.druid.username= # 或spring.datasource.username=
spring.datasource.druid.password= # 或spring.datasource.password=
spring.datasource.druid.driver-class-name= #或 spring.datasource.driver-class-name=

(2)连接池配置

spring.datasource.druid.initial-size=
spring.datasource.druid.max-active=
spring.datasource.druid.min-idle=
spring.datasource.druid.max-wait=
spring.datasource.druid.pool-prepared-statements=
spring.datasource.druid.max-pool-prepared-statement-per-connection-size=
spring.datasource.druid.max-open-prepared-statements= #和上面的等价
spring.datasource.druid.validation-query=
spring.datasource.druid.validation-query-timeout=
spring.datasource.druid.test-on-borrow=
spring.datasource.druid.test-on-return=
spring.datasource.druid.test-while-idle=
spring.datasource.druid.time-between-eviction-runs-millis=
spring.datasource.druid.min-evictable-idle-time-millis=
spring.datasource.druid.max-evictable-idle-time-millis=
spring.datasource.druid.filters= #配置多个英文逗号分隔

(3)监控配置

# WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
spring.datasource.druid.web-stat-filter.enabled= #是否启用StatFilter默认值false
spring.datasource.druid.web-stat-filter.url-pattern=
spring.datasource.druid.web-stat-filter.exclusions=

#StatViewServlet配置,说明请参考Druid Wiki,配置_StatViewServlet配置
spring.datasource.druid.stat-view-servlet.enabled= #是否启用StatViewServlet(监控页面)默认值为false(考虑到安全问题默认并未启动,如需启用建议设置密码或白名单以保障安全)
spring.datasource.druid.stat-view-servlet.url-pattern=
spring.datasource.druid.stat-view-servlet.reset-enable=
spring.datasource.druid.stat-view-servlet.login-username=
spring.datasource.druid.stat-view-servlet.login-password=

3.2 自定义配置实例

spring:
  datasource:
    #基本连接信息
    url: jdbc:mysql://localhost:3306/education?serverTimezone=UTC&userUnicode=true&characterEncoding=utf-8
    username: root
    password: my060321
    driver-class-name: com.mysql.jdbc.Driver

    druid:
      # 1.连接池基本配置
      # 初始化初始化时建立物理连接的个数。
      initial-size: 5
      #最小连接池数量
      min-idle: 5
      #最大连接池数量
      maxActive: 20
      # 配置获取连接等待超时的时间,单位毫秒
      maxWait: 60000
      # 配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
      timeBetweenEvictionRunsMillis: 60000
      # 配置有一个连接在连接池中的最小生存时间,单位是毫秒
      minEvictableIdleTimeMillis: 300000
      #用来检测连接是否有效的sql,要求是一个查询语句
      validationQuery: SELECT 1 FROM DUAL
      #建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
      testWhileIdle: true
      #申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
      testOnBorrow: false
      #归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
      testOnReturn: false
      # 打开PSCache,并且指定每个连接上PSCache的大小
      poolPreparedStatements: true
      maxPoolPreparedStatementPerConnectionSize: 20

      #2.Spring容器监控
      aop-patterns: com.sdust.boot04.*

      #3.可视化监控页面配置
      stat-view-servlet:
        enabled: true
        reset-enable: false
        login-username: admin
        login-password: my060321

      #4.web访问监控
      web-stat-filter:
        enabled: true
        url-pattern: /*
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'

      #5.filter配置
      #filter: stat,wall  该方式来启用相应的内置Filter,不过这些Filter都是默认配置。如想自定义详细配置,请参考以下:
      filter:
        stat:
          enabled: true
        wall:
          enabled: true

4.测试效果

        可以通过localhost:8080/druid来访问数据监控页,对数据库操作、网站访问、Spring容器等进行监控和可视化。

SpringBoot(六) 整合数据访问_第4张图片

 三.整合MyBatis

        MyBatis相比于原生的JdbcTemplate提供了更为强大和灵活的数据等操作,通过其XML配置方式也可以很好的实现sql语句与代码解耦,因此我们主要介绍MyBatis框架的XML配置整合。

 1.引入MyBatis-Satrter



    org.mybatis.spring.boot
    mybatis-spring-boot-starter
    2.2.0

mybatis的场景启动器也帮我们引入了很多配置类,自动配置了熟悉的SqlSessionFactory 、SqlSessionTemplate、MapperScanner扫描器等等,因此我们只需要准备好相应的配置文件即可使用。

2.MyBatis全局文件配置

(1)XML方式配置

# 1. ymal mybatis配置
mybatis:
  #mybatis 全局配置文件位置
  config-location: classpath:mybatis/mybatis-config.xml
  #mybatis sql映射文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml




    
    
        
    

    
        
        
        
    

(2)yaml方式配置(完全接管全局配置文件)

# 1. 全面接管式 mybatis配置
mybatis:
  #mybatis 全局配置文件位置
  #config-location: classpath:mybatis/mybatis-config.xml
  #mybatis sql映射文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml
  #配置别名 
  type-aliases-package: com.sdust.boot04.bean
  #mybatis配置项 
  configuration:
    #
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

 3.MyBatis-SQL映射文件配置






    
        
        
        
        
        
    

    

4. Dao层接口编码

(1)使用@Mapper注解扫描单个接口

@Mapper
public interface IStudentDao {

    public List getStudentsByAll();
}

(2)使用@MapperScan进行包扫描

@SpringBootApplication
//扫描dao接口,创建代理对象
@MapperScan("com.sdust.boot04.dao")
public class Boot04Application {

    public static void main(String[] args) {
        SpringApplication.run(Boot04Application.class, args);
    }

}

        然后通过@Autowired来注入接口,即可完成对mybatis的整合和操作。对于纯注解方式整合mybatis的方法这里不再介绍,可以自行查阅。

四.整合缓存

1.MyBatis的缓存分析

        为了提高数据查询效率,MyBatis提供了原生的缓存机制,分别是一级缓存和二级缓存,我们分别对两种缓存进行分析。为了搞明白缓存,我们先清楚以下两点:

  • SpringBoot中的MyBatis与原生MyBatis基本原理相同,只是SpringBoot对MyBatis的细节作了封装和自动配置,使得我们可以只关注与数据访问开发。
  • MyBatis每次调用Mapper层进行数据操作,都会从线程池中获取一个新的sqlsession连接,操作完成后提交事务并关闭连接。但是,如果多个Mapper属于一个事务,则会共享一个sqlSession。
  • sqlSession从开启到关闭属于一次数据库连接会话,在会话期间,每一次commit都是一组事务提交,一次会话期间可以操作多组事务,共享一个一级缓存。

(1)MyBatis一级缓存

1.定义:MyBatis一级缓存是单个session级别的,其作用域是一次SqlSession会话(开启到关闭期间生效)
2.特点:
    (1)一级缓存是本地(局部)缓存,默认开启且不能被关闭,只能配置缓存范围:SESSION(一次Session会话,默认)或STATEMENT(一次查询语句)
    (2)当SqlSession执行commit操作(包括插入、更新、删除)时(不管是否提交),会清空SqlSession的一级缓存,主要目的是确保缓存中的数据是最新的,避免脏读。
3.缓存流程:在同一个session会话中,当查询数据时会先去一级缓存查询,若存在语句、参数都相同的结果则直接返回,否则再去数据库查询。其本质是一个map实现
    @Test
    void CacheOfLevelOne(){
        SqlSession session =  sqlSessionFactory.openSession(false);
        SqlSession session2 = sqlSessionFactory.openSession();

        IAdminDao adminDao = session.getMapper(IAdminDao.class);
        IAdminDao adminDao2 = session2.getMapper(IAdminDao.class);
        //1.测试同一个session下的数据缓存(生效)
        Admin admin = adminDao.getAdminByName("admin");
        System.out.println(admin);
        Admin admin2 = adminDao.getAdminByName("admin");
        System.out.println(admin2);
        //2.测试不同session下的数据缓存(独立)-可能会产生脏数据(另一个session修改了本session缓存的数据库数据)
        admin = adminDao.getAdminByName("shaxin");
        System.out.println(admin);
        admin2 = adminDao2.getAdminByName("shaxin");
        System.out.println(admin2);
        //3.测试增删改对一级缓存的清空
        admin = adminDao.getAdminByName("shaxin");//查询缓存
        System.out.println(admin);
        Admin admin_insert = new Admin();//插入数据
        admin_insert.setAdminName("lisa");
        admin_insert.setAdminPassword("123123");
        adminDao.insertAdmin(admin_insert);
        //session.commit();//不管是否提交
        admin = adminDao.getAdminByName("shaxin");//再次查询
        System.out.println(admin);

        session.close();
        session2.close();
    }

SpringBoot(六) 整合数据访问_第5张图片

 (2)MyBatis二级缓存

1.定义:二级缓存的作用域是namespace,同一个Mapper.xml下的所有session共享缓存
2.生效条件:
   (1)开启MyBatis的二级缓存支持:configuration.cache-enabled=true(默认为开启)
   (2)指定namespace下的二级缓存生效:在需要启动二级缓存的mapper.xml下添加
3.特点:
   (1)同一个namespace下的所有session操作共享一个二级缓存,但是不同namespace下的缓存互相独立(不同域下的同一个对象操作可能带来脏读)
   (2)二级缓存操作涉及的实体类,必须实现序列化接口(Serializable )
   (3)可以使用 useCache="false" 标签属性指明某个查询不使用二级缓存
   (4)当开启缓存后,数据的查询执行的流程就是 二级缓存 -> 一级缓存 -> 数据库。任何一个地方命中都会直接返回
   (5)同一个域下的任何插入、删除、更新操作,在commit提交后都会清空刷新二级缓存(但是一级缓存就算不提交也会清空)
4.原理:
   (1)每一个Mapper.xml,如果在其中使用了 节点,则MyBatis会为这个Mapper创建一个默认的Cache缓存对象,该对象会将Mapper的 namespace作为它们的ID;
   (2)在存入数据时:采用了类似于Map的结构,将数据对象序列化后存入(key-value)
   (3)在查询时:mybatis会先去找对应ID的Cache缓存类,任何调用get方法获取数据,如果命中直接返回,否则再去数据库。
   (4)mybatis自带的二级缓存,具有一定的容量限制,默认采用LRU:(Least Recently Used),最近最少使用算法来替换数据
   (5)二级缓存的实现原理,大量运用了装饰者模式,如果想自定义二级缓存可以实现Cache类,并显式指定缓存类
    @Test
    void CacheOfLevelTwo(){
        Admin admin = adminDao.getAdminByName("admin");
        System.out.println(admin);
        Admin admin2 = adminDao.getAdminByName("admin");
        System.out.println(admin2);
        //插入数据
        Admin admin_insert = new Admin();
        admin_insert.setAdminName("lisa");
        admin_insert.setAdminPassword("123123");
        adminDao.insertAdmin(admin_insert);

        admin = adminDao.getAdminByName("admin");
        System.out.println(admin);
    }

SpringBoot(六) 整合数据访问_第6张图片

2.SpringBoot整合Redis工具

(1)Redis概述

1.Redis简介:Redis是一种key-value形式的NoSQL数据库,它的工作策略是将数据直接缓存于内存中,并通过默认的“快照”方式将数据不定时的刷新到计算机硬盘中进行持久化。因此,Redis相对于MySQL来说,更加轻便、高效、快速,适用于各种缓存、热点数据查询等场景。

2.为什么MyBatis有原生的缓存,还要整合Redis:

       (1)mybatis一级缓存作用域是session,session在commit之后缓存就消失了。

  (2)mybatis二级缓存作用域是sessionfactory,该缓存是以namespace为单位的(也就是一个Mapper.xml文件),不同namespace下操作互不影响。    

  (3)所有对数据表的改变操作都会刷新缓存,但是一般不要用二级缓存,例如,在UserMapper.xml中有大多数针对User表的操作,但是在另一个XXXManpper.xml中,还有针对user单表的操作,这会导致user在两个命名空间下的数据不一致。

  (4)如果在UserMapper.xml中做了刷新缓存的操作,在XXXMapper.xml中缓存仍然有效,如果有针对user的单标查询,使用缓存的结果可能会不正确,读到脏数据。

  (5)redis很好的解决了这个问题,而且比之一、二级缓存的更好,redis可以搭建在其它服务器上,缓存容量可扩展,redis可以灵活的使用在需要的缓存的数据上,比如一些热点数据,统计点赞啊。

 (2)SpringBoot封装Redis工具类

1. 引入SpringBoot-Redis的依赖启动器: spring-boot-starter-data-redis,后续通过其提供的redisTemplate来操作redis。



    org.springframework.boot
    spring-boot-starter-data-redis

 2. 在yml文件中配置redis连接信息,开启线程池。注意:

(1)redis具有两种客户端连接模式,两种连接模式区别:
     - Jedis客户端:是直连的同步模式,在多个线程间共享一个Jedis实例时是线程不安全的,可以通过创建多个Jedis实例来解决,但当连接数量增多时,物理连接成本就较高同时会影响性能,因此较好的解决方法是使用JedisPool。
     - Lettuce客户端(默认使用):连接是基于Netty的,连接实例可以在多个线程间异步共享,Netty可以使多线程的应用使用同一个连接实例,而不用担心并发线程的数量。通过异步的方式可以让我们更好地利用系统资源。同时Lettuce也支持连接池。
(2)lettuce连接池开启方法:SpringBoot2默认使用的是lettuce作为redis客户端,默认是不使用连接池的。我们只有配置 redis.lettuce.pool下的属性的时候才会开启redis连接池
(3)lettuce连接池要求:lettuce要使用连接池,要依赖commons-pool2提供的连接池类,因此需要导入commons-pool2依赖
 

   org.apache.commons
   commons-pool2
#配置redis信息,开启lettuce连接池
spring:
  redis:
    database: 0
    host: 127.0.0.1
    port: 6379
    #配置lettuce使用连接池
    lettuce:
      pool:
        # 连接池中的最小空闲连接
        min-idle: 0
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池的最大数据库连接数
        max-active: 30
        # #连接池最大阻塞等待时间 ms(使用负值表示没有限制)
        max-wait: -1
    #连接超时时间(ms)
    timeout: 2000

 3. 自定义Redis序列化配置:Redis默认使用JdkSerializationRedisSerializer来进行序列化存储,显示为二进制格式乱码,可读性差。因此,我们需要自定义配置使用fastJson提供的序列化器将数据进行json格式序列化存储和反序列化。

  • 源码:springboot默认提供redisTemplate形式的键值对存储,并默认使用Jdk二进制序列化。由于源码使用了@ConditionalOnMissingBean,因此我们可以自定义redisTemplate代替默认的对象,满足开发需求。
  • 自定义配置:首先我们不再使用形式的键值对存储,而是使用形式的存储方式。其次,我们使用fastJson提供的序列化器代替Jdk序列化来对对象进行Json格式的序列化存储,增强可读性。

SpringBoot(六) 整合数据访问_第7张图片

//Redis自定义配置类
@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        FastJsonRedisSerializer jsonSerializer = new FastJsonRedisSerializer(Object.class);
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(jsonSerializer);
        template.setHashValueSerializer(jsonSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        return template;
    }
}

4. 封装Redis操作工具类:包括存储、取值、设置过期时间、删除、清空等等。

//Redis封装工具类(注入为spring组件)
@Component
public class RedisUtils {
    @Autowired
    RedisTemplate redisTemplate;

    /**
     * 指定目标缓存失效时间(秒),默认永久有效
     * @param key
     * @param time (time<=0不改变过期时间)
     * @return
     */
    public boolean expire(String key,long time){
        try{
            if(time > 0){
                redisTemplate.expire(key,time, TimeUnit.SECONDS);
            }
            return true;
        }catch(Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 根据key 获取过期时间(秒)
     * @param key
     * @return 时间(秒)
     *      1.The command returns -2 if the key does not exist.
     *      2.The command returns -1 if the key exists but has no associated expire.
     *      3.The command returns -3 if exception is occured
     */
    public long getExpire(String key){
        try{
            return redisTemplate.getExpire(key,TimeUnit.SECONDS);
        }catch (Exception e){
            e.printStackTrace();
            return -3;
        }
    }

    /**
     * 判断key是否存在
     * @param key
     * @return
     */
    public boolean hasKey(String key){
        try{
            return redisTemplate.hasKey(key);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 设置缓存数据
     * @param key
     * @param value
     */
    public boolean put(String key,Object value){
        try{
            redisTemplate.opsForValue().set(key,value);
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 获取缓存数据
     * @param key
     * @return
     */
    public Object get(String key){
        try{
            return redisTemplate.opsForValue().get(key);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 换取缓存数据,并转换对象类型
     * @param key
     * @param clazz
     * @param 
     * @return
     */
    public  T get(String key,Class clazz){
        try{
            return JSON.toJavaObject((JSON) get(key), clazz);
        }catch (Exception e){
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 设置缓存数据,并设置过期时间
     * @param key
     * @param value
     * @param time 时间(秒) 注意若time<=0,则设置无期限
     * @return
     */
    public boolean put(String key,Object value,long time){
        try{
            if(time > 0){
                redisTemplate.opsForValue().set(key,value,time,TimeUnit.SECONDS);
            }else{
                redisTemplate.opsForValue().set(key,value);
            }
            return true;
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 删除目标缓存
     * @param key
     * @return
     */
    public boolean del(String key){
        try{
            return redisTemplate.delete(key);
        }catch (Exception e){
            e.printStackTrace();
            return false;
        }
    }

    /**
     * 清空redis缓存
     * @return The number of keys that were removed.
     */
    public long flushDB(){
        try{
            Set keys = redisTemplate.keys("*");
            return redisTemplate.delete(keys);
        }catch(Exception e){
            e.printStackTrace();
            return 0;
        }
    }
}

 5. 测试:序列化与反序列化、期限是否生效等

    @Test
    void redisUtilsTest(){
        //测试过期时间
        long time = redisUtils.getExpire("lisa");
        System.out.println(time);
        //修改过期时间
        redisUtils.expire("lisa",60);
        long time2 = redisUtils.getExpire("lisa");
        System.out.println(time2);
        //插入数据
        Admin admin_insert = new Admin();
        admin_insert.setAdminId(2);
        admin_insert.setAdminName("wangxin");
        admin_insert.setAdminPassword("my060321");
        redisUtils.put("wangxin",admin_insert,60);//有期限
        redisUtils.put("admin",admin_insert);//无期限
        //获取数据
        Admin admin_get = redisUtils.get("admin",Admin.class);
        System.out.println(admin_get);
        //清空数据库
        long len = redisUtils.flushDB();
        System.out.println(len);
    }

 3.SpringBoot整合Redis做缓存

(1)Redis封装工具类手动缓存

        该方式通过上一节封装的Redis工具类,在每次数据操作之前都手动进行缓存判断和操作,来实现数据库与缓存的结合。

    @Test
    public void redisUtilsCacheTest(){
        //1.获取数据
        String key = "admin";
        //先查询缓存
        Admin admin = redisUtils.get(key,Admin.class);
        //若缓存没有则查询数据库
        if(admin==null){
            admin = adminDao.getAdminByName("admin");
            //查询数据存入缓存
            if(admin!=null)redisUtils.put(admin.getAdminName(),admin);
        }
        System.out.println(admin);
        //2.更新数据
        Admin admin_insert = new Admin();
        admin_insert.setAdminName("wangxin");
        admin_insert.setAdminPassword("my060321");
        adminDao.insertAdmin(admin_insert);
        //清空缓存
        redisUtils.flushDB();
    }

(2)MyBatis二级缓存整合Redis

1. 自定义获取RedisTemplate的工具类:我们要自定义缓存类来进行redis缓存,进而代替mybtais原生的二级缓存。自定义缓存类是一个不属于spring管理的普通类,因此要在普通类中使用spring管理的redisTemplate,就要通过ApplicationContext来获取。

/**
 * 实现了ApplicationContextAware这个接口的bean,当spring容器初始化的时候
 * 会自动的调用setApplicationContext方法将当前ApplicationContext注入进来。
 */
@Component
public class ApplicationContextHolder implements ApplicationContextAware {

    private static ApplicationContext applicationContext = null;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }

    //获取applicationContext实例
    public static ApplicationContext getApplicationContext(){
        return applicationContext;
    }

    // 从容器中获取RedisTemplate
    public static RedisTemplate getRedisTemplate() {
        return applicationContext.getBean("redisTemplate", RedisTemplate.class);
    }
}

 2. 实现RedisCache二级缓存类:代替MyBatis二级缓存的对象必须实现Cache接口,并重写其中的方法。MyBatis会自动为每一个namespace创建一个Cache对象,在执行每一个查询操作时,自动先调用Cache的get方法查询缓存;在执行每一个更新方法后,会自动调用Cache的clear方法清空缓存;因此,我们只需要将redis缓存的相关操作集成到Cache方法中即可。

public class RedisCache implements Cache {

    // 这个id是namespace 这个类是mybatis 调用的所以这个构造方法的参数也是mybatis传递的
    private String id;
    //缓存过期时间(days)
    private int Cache_EffectiveTime = 7;
    //redisTemplate操作对象
    private RedisTemplate redisTemplate = null;
    //读写锁
    private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();

    public RedisCache(String id) {
        if (id == null) {
            throw new IllegalArgumentException("Cache instances require an ID");
        }
        this.id = id;
        System.out.println("cache Id is" + id);
    }

    @Override
    public String getId() {
        return this.id;
    }

    @Override
    public void putObject(Object o, Object o1) {
        try{
            System.out.println("put Object method is execute");
            System.out.println("key is " + o.toString());
            RedisTemplate redisTemplate = getRedisTemplate();
            redisTemplate.opsForValue().set(o.toString(),o1,Cache_EffectiveTime, TimeUnit.DAYS);
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("ERROR : 数据缓存异常");
        }
    }

    @Override
    public Object getObject(Object o) {
        try{
            System.out.println("get Object method is execute");
            System.out.println("key is " + o.toString());
            RedisTemplate redisTemplate = getRedisTemplate();
            if(o!=null) return redisTemplate.opsForValue().get(o.toString());
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("ERROR : 缓存获取异常");
        }
        return null;
    }

    @Override
    public Object removeObject(Object o) {
        try{
            RedisTemplate redisTemplate = getRedisTemplate();
            if(o!=null) redisTemplate.delete(o.toString());
        }catch (Exception e){
            e.printStackTrace();
            System.out.println("ERROR : 缓存对象移除异常");
        }
        return null;
    }

    @Override
    public void clear() {
        System.out.println("Cache is clear...");
        RedisTemplate redisTemplate = getRedisTemplate();
        //回调函数
        redisTemplate.execute(new RedisCallback() {
            @Override
            public Object doInRedis(RedisConnection redisConnection) throws DataAccessException {
                redisConnection.flushDb();
                return null;
            }
        });
    }

    @Override
    public int getSize() {
        RedisTemplate redisTemplate = getRedisTemplate();
        //回调函数
        Long size = (Long) redisTemplate.execute(new RedisCallback() {
            @Override
            public Long doInRedis(RedisConnection redisConnection) throws DataAccessException {
                return redisConnection.dbSize();
            }
        });
        return size.intValue();
    }

    @Override
    public ReadWriteLock getReadWriteLock() {
        return reentrantReadWriteLock;
    }

    //从容器中获取redisTemplate对象
    public RedisTemplate getRedisTemplate(){
        if(redisTemplate==null){
            redisTemplate = ApplicationContextHolder.getRedisTemplate();
        }
        return redisTemplate;
    }
}

 3. 开启并指定二级缓存:在namespace中,使用标签开启二级缓存生效,并指定二级缓存类为我们自定义的Redis缓存。


    
    

    
        
        
        
    

    

4. 修改RedisTemplate配置文件:修改序列化器为GenericJackson2JsonRedisSerializer,因为之前的fastJson序列化器序列化对象为JSONObject,不能直接强制转化为Object,在使用中会报错。

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        //FastJsonRedisSerializer jsonRedisSerializer = new FastJsonRedisSerializer(Object.class);
        //修改Json序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // value值的序列化采用JsonRedisSerializer
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        return template;
    }
}

 5. 测试:二级缓存使用Redis生效,可以观察MyBatis默认设置的缓存key值是什么特点。

    @Test
    public void redisCacheOfTwoTest(){
        //查询单个对象1
        Admin admin1 = adminDao.getAdminByName("admin");
        System.out.println(admin1);
        //查询列表1
        List admin_list1 = adminDao.getAdminByAll();
        System.out.println(admin_list1);
        //查询单个对象2
        Admin admin2 = adminDao.getAdminByName("admin");
        System.out.println(admin2);
        //查询列表2
        List admin_list2 = adminDao.getAdminByAll();
        System.out.println(admin_list2);
        //更新
        Admin admin_insert = new Admin();
        admin_insert.setAdminName("test");
        admin_insert.setAdminPassword("666666");
        adminDao.insertAdmin(admin_insert);
        //查询单个对象3
        Admin admin3 = adminDao.getAdminByName("admin");
        System.out.println(admin3);
        //查询列表1
        List admin_list3 = adminDao.getAdminByAll();
        System.out.println(admin_list3);
    }

SpringBoot(六) 整合数据访问_第8张图片

(3)SpringBoot注解方式整合Redis缓存

        除了以上的方式,SpringBoot还提供了缓存注解,由SpringBoot在执行方法的时候来控制缓存,而不是由MyBatis。常见的缓存注解有@Cacheable、@CachePut、@CacheEvict、@CacheConfig等。

注意:pom和yml整合配置redis后,springBoot会自动将redis作为缓存中间件,因此我们只需要开启缓存注解,并进行一些自定义配置即可。

1. 缓存注解配置:SpringBoot提供了很多注解缓存处理类,在注解的地方对缓存进行处理,比如SimpleCacheManager、RedisCacheManager、EhCacheManager等。但是默认的缓存处理类的序列化方式采用Jdk二进制序列化,并且默认缓存键key的生成方式采用简单的参数,这些都不满足我们的需求。因此在自定义缓存注解配置中,我们要使用自定义的cacheManager,来代替SpringBoot默认的cacheManager。并且自定义键key的生成器。

@Configuration
public class RedisConfig {
    /**
     * SpringBoot提供了很多注解缓存处理类:SimpleCacheManager、RedisCacheManager、EhCacheManager等。
     * 在自定义缓存注解配置中,我们要使用自定义的cacheManager,来代替SpringBoot的cacheManager
     * @param factory
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 配置序列化
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration = config
                // 键序列化方式 redis字符串序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                // 值序列化方式 简单json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                //不缓存Null值
                .disableCachingNullValues()
                //缓存失效 3天
                .entryTtl(Duration.ofDays(3));
        return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
    }

    /**
     * 重写缓存key的生成方式: 类名.方法名字&[参数列表]
     * @return
     */
    @Bean
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName()).append(".");//执行类名
                sb.append(method.getName()).append("&");//方法名
                sb.append(Arrays.toString(params));//参数
                return sb.toString();
            }
        };
    }
}

SpringBoot(六) 整合数据访问_第9张图片

 2. 开启注解缓存支持:@EnableCaching

@SpringBootApplication
@MapperScan("com.sdust.example.dao")
@EnableCaching //开启缓存注解支持
public class BootRedisApplication {

    public static void main(String[] args) {
        SpringApplication.run(BootRedisApplication.class, args);
    }

}

 3. 使用缓存注解:@Cacheable、@CachePut、@CacheEvict、@CacheConfig等

@Service
/**
 * @CacheConfig:缓存配置注解,用于类上,抽取类内缓存注解的公共部分,比如缓存区名称cacheNames
 */
@CacheConfig(cacheNames = "adminCache")
public class AdminServiceImpl implements IAdminService {
    @Autowired
    IAdminDao adminDao;

    /**
     * 一.@Cacheable可用于类或方法上;在目标方法执行前,会根据key先去缓存中查询看是否有数据。
     *      1.有就直接返回缓存中的key对应的value值。不再执行目标方法;
     *      2.无则执行目标方法,并将[方法的返回值]作为value,并以键值对的形式存入缓存
     *      3.注意:这一切都是由springBoot来控制执行的,脱离了MyBatis,与之前的缓存方法都不同
     * 参数:
     *      1.cacheNames/value : 指定缓存区的名字,将方法的返回值存放在哪个缓存区中,是数组的方式,可以指定多个缓存.(redis缓存是以文件的形式折叠的,同名的缓存区折叠在一个文件下)
     *      2.key : 缓存数据时使用的key,可以使用sqEL表达式。如果写了keyGenerator则不用声明
     *      3.condition : 在符合该条件的情况下才缓存数据.如:condition = "#id>0"代表参数id必须大于0才缓存
     *      4.keyGenerator:指定key的生成策略生成器。
     * 场景:一般用在查询方法上
     * @param name
     * @return
     */
    @Override
    @Cacheable(keyGenerator = "keyGenerator")
    public Admin selectAdminByName(String name) {
        return adminDao.getAdminByName(name);
    }

    @Override
    @Cacheable(keyGenerator = "keyGenerator")
    public List selectAllAdmins() {
        return adminDao.getAdminByAll();
    }

    /**
     * 二.@Cacheput:可用于类或方法上;在执行完目标方法后,并将方法的返回值作为value,并以键值对的形式存入缓存中(若已存在则执行更新)。
     * 参数:
     *      1.cacheNames/value : 指定缓存区的名字,将方法的返回值存放在哪个缓存区中,是数组的方式,可以指定多个缓存.(redis缓存是以文件的形式折叠的,同名的缓存区折叠在一个文件下)
     *      2.key : 缓存数据时使用的key,可以使用sqEL表达式。如果写了keyGenerator则不用声明
     *      3.condition : 在符合该条件的情况下才缓存数据.如:condition = "#id>0"代表参数id必须大于0才缓存
     *      4.keyGenerator:指定key的生成策略生成器。
     * 场景:一般用在更新方法上,更新数据库后顺便更新缓存
     */
    @Override
    @CachePut(keyGenerator = "keyGenerator")
    public Admin insertAdmin(Admin admin){
        adminDao.insertAdmin(admin);
        //1.必须要有返回值:但是插入操作中有些字段是数据库生成的,这里不好界定。是不是还要重新查一边数据库?不如直接清空得了。
        //2.执行后接着查询但是还是查询了数据库:原因是插入时的key和查询时的key生成不同!如下(实际应用如何解决看自己把,要不就替换key生成器):
        //  -插入:adminCache::com.sdust.example.service.impl.AdminServiceImpl.insertAdmin&[Admin{adminId=null, adminName='cacheTest', adminPassword='666666'}]
        //  -查询:adminCache::com.sdust.example.service.impl.AdminServiceImpl.selectAdminByName&[cacheTest]
        return admin;
    }


    /**
     * 三.@CacheEvict:可用于类或方法上;在执行完目标方法后,清除缓存中对应key的数据(如果缓存中有对应key的数据缓存的话)
     * 参数:
     *      1.cacheNames/value : 指定缓存区空间的名字
     *      2.key : 清除目标缓存数据的key,可以使用sqEL表达式。
     *      3.allEntries:是否清空缓存区下(注意是[指定缓存区]!而不是整个redisIndex库)的所有缓存,默认为false。如果为true 则删除整个缓存区的所有值,此时指定的key无效。
     *      4.beforeInvocation :默认false,表示调用方法之后删除缓存数据;true时,在调用之前删除缓存数据(如方法出现异常就不会删除缓存数据)
     *  场景:一般用在删除方法上,更新数据库后清空缓存
     */
    @Override
    @CacheEvict(allEntries = true)
    public void deleteAdmin(String name){
        adminDao.deleteAdmin(name);
    }

}

 4. 测试:测试缓存注解是否生效

    @Test
    public void cacheAnnoTest(){
        //查询单个对象
        Admin admin = adminService.selectAdminByName("admin");
        System.out.println(admin);
        Admin admin2 = adminService.selectAdminByName("admin");
        System.out.println(admin2);
        //查询数组列表
        List admin_list = adminService.selectAllAdmins();
        System.out.println(admin_list);
        List admin_list2 = adminService.selectAllAdmins();
        System.out.println(admin_list2);
        //更新数据,更新缓存
        Admin admin_insert = new Admin();
        admin_insert.setAdminName("cacheTest");
        admin_insert.setAdminPassword("666666");
        adminService.insertAdmin(admin_insert);
        Admin admin3 = adminService.selectAdminByName("admin");//更新后查询原始数据
        System.out.println(admin3);
        Admin admin4 = adminService.selectAdminByName("cacheTest");//更新后查询新增数据
        System.out.println(admin4);
        //删除数据,清空缓存
        adminService.deleteAdmin("test");
        Admin admin5 = adminService.selectAdminByName("admin");//删除后查询原始数据
        System.out.println(admin5);
    }

SpringBoot(六) 整合数据访问_第10张图片

五.SpringBoot 复杂缓存场景处理方案

1.SpringBoot 缓存注解细节

(1)复杂缓存逻辑注解 @Caching

        如果我们在缓存处理中需要同时对数据进行多种缓存操作或者需要同时对多个缓存区进行操作,那么使用 @Caching 就很符合我们的需求。@Caching是@Cacheable、@CachePut、@CacheEvict三个注解的复合注解,其源码如下所示。

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Caching {
    Cacheable[] cacheable() default {};

    CachePut[] put() default {};

    CacheEvict[] evict() default {};
}

        同时清除test1和test2多个缓存区数据可以如下这么写,当然该注解也支持多种复合缓存操作。

// - 同时清除test1和test2多个缓存区数据
@Caching(
        evict={
                @CacheEvict(cacheNames = "test1",allEntries = true),
                @CacheEvict(cacheNames = "test2",allEntries = true)
        }
)
public void cacheEvict(String key) {

}


// - 等价于 cacheNames 数组形式
@CacheEvict(cacheNames = {"test1","test2"},allEntries = true)
public void cacheEvict(String key) {

}

(2)缓存注解配置覆盖

        在类上使用@CacheConfig()全局配置时,我们仍可在某个方法上使用@Cacheable、@CachePut、@CacheEvict相关注解指定其他的cacheName等属性来覆盖@CacheConfig,如果不指定则默认使用@CacheConfig配置属性。

@Service
@CacheConfig(cacheNames = "typeCache",keyGenerator = "keyGenerator")
public class TypeServiceImpl implements ITypeService {

    @Autowired
    ITypeDao typeDao;

    @Autowired
    IBlogDao blogDao;

    //1.默认使用@CacheConfig配置
    @Cacheable
    @Override
    public List getTypeByAll() {
        return typeDao.selectTypeByAll();
    }

    //2.覆盖@CacheConfig配置
    @CacheEvict(cacheNames = "blogCache",allEntries = true)
    @Override
    public boolean addType(Type type) {
        typeDao.insertType(type);
        return true;
    }
}

 (3)冷门注解属性解析

  • beforeInvocation属性:该属性出现在@CacheEvict中,缓存清除操作默认是在对应方法成功执行之后触发的(默认为false),如果方法抛出异常而未能成功返回时不会触发清除操作。使用beforeInvocation 可以改变触发清除操作的时间,当指定该属性值为 true 时,Spring 会在调用该方法之前清除缓存中的指定元素。
  • allEntries属性:该属性出现在@CacheEvict中,allEntries 是 boolean 类型,表示是否需要清除对应缓存区中的所有元素。默认为 false,表示不需要。当指定了 allEntries 为 true 时,Spring Cache 将忽略指定的 key。有的时候需要 Cache 一下清除所有的元素,这比一个一个清除元素更有效率。

  • condition属性:condition属性表示缓存注解操作触发的条件,默认为空。其支持使用spEL表达式作为判断条件,表达式条件为true时才触发清除缓存,否则不执行缓存操作。spEL表达式元数据如下(key属性也支持spEL表达式)

SpringBoot(六) 整合数据访问_第11张图片

(4)条件缓存注解配置 

        缓存注解结合condition属性或unless属性和spEL表达式可以实现复杂的缓存逻辑,在满足条件时才进行相应的缓存操作,此处例子:返回值为true时才执行缓存操作,否则不执行。

    @Caching(
            evict = {
                    @CacheEvict(cacheNames = "typeCache",allEntries = true,condition = "#result == true"),
                    @CacheEvict(cacheNames = "blogCache",allEntries = true,condition = "#result == true")
            }
    )
    @Transactional(rollbackFor = Exception.class)
    @Override
    public boolean addType(Type type) {
        if(StringUtil.isNullOrEmpty(type.getTypeName())){
            return false;
        }
        Type sameType = typeDao.selectTypeByName(type.getTypeName());
        if(sameType == null){
            typeDao.insertType(type);
            return true;
        }
        return false;
    }

 注意:若要使用condition属性判断方法返回值条件,必须设置beforeInvocation 属性为false(默认值),即在方法执行完成后再进行缓存清除;否则在方法执行前就清除了缓存,condition属性判断就无效了!

2.SpringBoot 多Redis Index库操作解决方案 

  • 原因:很多场景下,在缓存的使用中缓存数据可能涉及不同的方面,我们对他们的缓存要求可能不一样,也或者是我们想避免缓存数据混合到一起造成可读性差/混乱等,我们需要将缓存数据进行分库处理。而Redis的整合默认都是只能使用其中一个库,所以我们需要自己配置自定义的Redis操作方案!
  • 原理:观察源码发现,Redis配置访问库的操作是在connectionFactory创建时指定的redisIndex,如果我们需要进行多Redis库访问有这么两种方式:一是分别配置多个不同redisIndex绑定下的connectionFactory;另一个是在需要时动态地进行访问库切换

 (1)redisIndex分别独立绑定RedisTemplate实现分库操作

        该方式的原理就是:分别配置多个不同redisIndex绑定的connectionFactory,然后注册注入每个connectionFactory下的RedisTemplate进行不同库的访问操作。

@Configuration
public class RedisConfig {
    //1.读取redis配置
    @Value("${redis.host}")
    private String host;
    @Value("${redis.port}")
    private int port;
    @Value("${redis.password}")
    private String password;
    @Value("${redis.pool.max-total}")
    private int maxTotal;
    @Value("${redis.pool.max-wait}")
    private int maxWait;
    @Value("${redis.pool.max-idle}")
    private int maxIdle;
    @Value("${redis.pool.min-idle}")
    private int minIdle;
    //2.db0和db1对应的操作redisTemplate
    @Bean(name = "db0RedisTemplate")
    public RedisTemplate db0RedisTemplate() {
        return getRedisTemplate(db0Factory());
    }
    @Bean(name = "db1RedisTemplate")
    public RedisTemplate db1RedisTemplate() {
        return getRedisTemplate(db1Factory());
    }
    //3.db0和db1对应的ConnectionFactory(默认使用Lettuce客户端)
    //    RedisConfig:需要redis的配置(redis模式、host、port、dbIndex等)
    //    ClientConfig:需要客户端的配置(连接池配置等)
    @Bean
    @Primary
    public LettuceConnectionFactory db0Factory(){
        return new LettuceConnectionFactory(getRedisConfig(0), getClientConfig());
    }

    @Bean
    public LettuceConnectionFactory db1Factory(){
        return new LettuceConnectionFactory(getRedisConfig(1), getClientConfig());
    }
    //4.redis单机模式配置和Lettuce客户端连接池配置
    private RedisStandaloneConfiguration getRedisConfig(int dbNo) {
        RedisStandaloneConfiguration config = new RedisStandaloneConfiguration();
        config.setHostName(host);
        config.setPort(port);
        config.setPassword(password);
        config.setDatabase(dbNo);
        return config;
    }
    private LettuceClientConfiguration getClientConfig() {
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxTotal(maxTotal);
        poolConfig.setMaxIdle(maxIdle);
        poolConfig.setMinIdle(minIdle);
        poolConfig.setMaxWaitMillis(maxWait);
        return LettucePoolingClientConfiguration.builder().poolConfig(poolConfig).build();
    }
    //5.绑定redisTemplate,设置序列化
    public RedisTemplate getRedisTemplate(LettuceConnectionFactory factory) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(String.class);
        RedisTemplate redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

    public StringRedisTemplate getStringRedisTemplate(LettuceConnectionFactory factory) {
        StringRedisTemplate redisTemplate = new StringRedisTemplate();
        redisTemplate.setConnectionFactory(factory);
        return redisTemplate;
    }

}

 (2)使用map动态映射独立绑定RedisTemplate实现分库操作

        该方式是(1)的进阶版,通过map存储和映射可以动态的获取相应redisIndex的redisTemplate,减少了代码冗余。

@Configuration
public class RedisConfig {
    //redis配置类
    @Resource
    private RedisProperties redisProperties;
    //静态属性注入(16个库的redisTemplate全部绑定到map)
    static Map> redisTemplateMap = new HashMap<>();

    @PostConstruct
    public void initRedisTemp() {
        for (int i = 0; i <= 15; i++) {
            redisTemplateMap.put(i, getRedisTemplate(i));
        }
    }

    public RedisTemplate setDataBase(int num) {
        return redisTemplateMap.getOrDefault(num, redisTemplateMap.get(0));
    }

    /**
     * 获取redisTemplate实例
     *
     * @param db
     * @return
     */
    private RedisTemplate getRedisTemplate(int db) {
        final RedisTemplate redisTemplate = new RedisTemplate<>();
        LettuceConnectionFactory factory = factory();
        factory.setDatabase(db);
        redisTemplate.setConnectionFactory(factory);
        return serializer(redisTemplate);
    }

    /**
     * redis单机配置
     *
     * @return
     */
    private RedisStandaloneConfiguration redisConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        //设置密码
        if (redisProperties.getPassword() != null) {
            redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        }
        return redisStandaloneConfiguration;
    }

    /**
     * redis哨兵配置
     *
     * @return
     */
    private RedisSentinelConfiguration getSentinelConfiguration() {
        RedisProperties.Sentinel sentinel = redisProperties.getSentinel();
        if (sentinel != null) {
            RedisSentinelConfiguration config = new RedisSentinelConfiguration();
            config.setMaster(sentinel.getMaster());
            if (!StringUtils.isEmpty(redisProperties.getPassword())) {
                config.setPassword(RedisPassword.of(redisProperties.getPassword()));
            }
            config.setSentinels(createSentinels(sentinel));
            return config;
        }
        return null;
    }

    /**
     * 获取哨兵节点
     *
     * @param sentinel
     * @return
     */
    private List createSentinels(RedisProperties.Sentinel sentinel) {
        List nodes = new ArrayList<>();
        for (String node : sentinel.getNodes()) {
            String[] parts = StringUtils.split(node, ":");
            Assert.state(parts.length == 2, "redis哨兵地址配置不合法!");
            nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
        }
        return nodes;
    }

    /**
     * redis集群配置
     *
     * @return
     */
    private RedisClusterConfiguration getRedisClusterConfiguration() {
        RedisProperties.Cluster cluster = redisProperties.getCluster();
        if (cluster != null) {
            RedisClusterConfiguration config = new RedisClusterConfiguration();
            config.setClusterNodes(createCluster(cluster));
            if (!StringUtils.isEmpty(redisProperties.getPassword())) {
                config.setPassword(RedisPassword.of(redisProperties.getPassword()));
            }
            config.setMaxRedirects(redisProperties.getCluster().getMaxRedirects());
            return config;
        }
        return null;
    }

    /**
     * 获取集群节点
     *
     * @param cluster
     * @return
     */
    private List createCluster(RedisProperties.Cluster cluster) {
        List nodes = new ArrayList<>();
        for (String node : cluster.getNodes()) {
            String[] parts = StringUtils.split(node, ":");
            Assert.state(parts.length == 2, "redis哨兵地址配置不合法!");
            nodes.add(new RedisNode(parts[0], Integer.valueOf(parts[1])));
        }
        return nodes;
    }


    /**
     * 连接池配置
     *
     * @return
     */
    private GenericObjectPoolConfig redisPool() {
        GenericObjectPoolConfig genericObjectPoolConfig =
                new GenericObjectPoolConfig();
        genericObjectPoolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        genericObjectPoolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        genericObjectPoolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
        genericObjectPoolConfig.setTestOnBorrow(true);
        genericObjectPoolConfig.setTestWhileIdle(true);
        genericObjectPoolConfig.setTestOnReturn(false);
        genericObjectPoolConfig.setMaxWaitMillis(5000);
        return genericObjectPoolConfig;
    }

    /**
     * redis客户端配置
     *
     * @return
     */
    private LettuceClientConfiguration clientConfiguration() {
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
        builder.commandTimeout(redisProperties.getLettuce().getShutdownTimeout());
        builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());
        builder.poolConfig(redisPool());
        LettuceClientConfiguration lettuceClientConfiguration = builder.build();
        return lettuceClientConfiguration;
    }

    /**
     * redis获取连接工厂
     *
     * @return
     */
    @Scope(scopeName = "prototype")
    private LettuceConnectionFactory factory() {
        //根据配置和客户端配置创建连接
        LettuceConnectionFactory lettuceConnectionFactory = null;
        if (redisProperties.getSentinel() == null && redisProperties.getCluster() == null) {  //单机模式
            lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration(), clientConfiguration());
            lettuceConnectionFactory.afterPropertiesSet();
        } else if (redisProperties.getCluster() == null) {                                      //哨兵模式
            lettuceConnectionFactory = new LettuceConnectionFactory(getSentinelConfiguration(), clientConfiguration());
            lettuceConnectionFactory.afterPropertiesSet();
        } else {                                                                                 //集群模式
            lettuceConnectionFactory = new LettuceConnectionFactory(getRedisClusterConfiguration(), clientConfiguration());
            lettuceConnectionFactory.afterPropertiesSet();
        }
        return lettuceConnectionFactory;
    }

    /**
     * 序列化
     *
     * @param redisTemplate
     * @return
     */
    private RedisTemplate serializer(RedisTemplate redisTemplate) {
        Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper om = new ObjectMapper()
                // 序列化 java8的时间Instant、LocalDateTime、LocalDate,(存储实体类的时间也可以序列化)
                .registerModule(new ParameterNamesModule())
                .registerModule(new Jdk8Module())
                .registerModule(new JavaTimeModule());
        om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(om);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        // hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        // value序列化方式采用jackson
        redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
        // hash的value序列化方式采用jackson
        redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
        redisTemplate.afterPropertiesSet();
        return redisTemplate;
    }
}

 (3)RedisTemplate+@Cache缓存注解分库操作

  • RedisTemplate 处理业务一的缓存,存储于缓存库 REDIS_INDEX_1
  • @Cache缓存注解 + chacheManager处理业务二缓存,存储于缓存库 REDIS_INDEX_2
/**
 * 配置 Redis 多 dbIndex 操作
 *  1.RedisTemplate处理RefreshToken缓存,存储与缓存库 REDIS_INDEX_TOKEN
 *  2.@Cache + chacheManager处理业务缓存,存储与缓存库 REDIS_INDEX_SERVICE
 */
@Configuration
public class RedisConfig {
    //redis配置类
    @Resource
    private RedisProperties redisProperties;

    /**
     * redis单机配置(默认)
     *  1.配置基本的redis连接属性(host,port等)
     *  1.哨兵模式和集群模式我们暂时用不到,不再配置(不需要数据备份和高并发)
     */
    private RedisStandaloneConfiguration redisConfiguration() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        //设置密码
        if (redisProperties.getPassword() != null) {
            redisStandaloneConfiguration.setPassword(RedisPassword.of(redisProperties.getPassword()));
        }
        return redisStandaloneConfiguration;
    }

    /**
     * redis Lettuce客户端连接池配置
     */
    private LettuceClientConfiguration clientConfiguration() {
        //配置连接池
        GenericObjectPoolConfig poolConfig = new GenericObjectPoolConfig();
        poolConfig.setMaxIdle(redisProperties.getLettuce().getPool().getMaxIdle());
        poolConfig.setMinIdle(redisProperties.getLettuce().getPool().getMinIdle());
        poolConfig.setMaxTotal(redisProperties.getLettuce().getPool().getMaxActive());
        poolConfig.setMaxWait(redisProperties.getLettuce().getPool().getMaxWait());
        //配置客户端
        LettucePoolingClientConfiguration.LettucePoolingClientConfigurationBuilder builder = LettucePoolingClientConfiguration.builder();
        //设置超时时间,原setTimeout已弃用
        builder.shutdownTimeout(redisProperties.getLettuce().getShutdownTimeout());
        builder.commandTimeout(redisProperties.getLettuce().getShutdownTimeout());
        return builder.poolConfig(poolConfig).build();
    }

    /**
     * 配置 业务逻辑缓存的redisConnectionFactory1
     */
    @Bean("redisServiceFactory")
    public LettuceConnectionFactory redisServiceFactory(){
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration(),clientConfiguration());
        lettuceConnectionFactory.setDatabase(ConstantUtil.REDIS_INDEX_1);
        return lettuceConnectionFactory;
    }

    /**
     * 配置 Token缓存的redisConnectionFactory2
     */
    @Bean("redisTokenFactory")
    public LettuceConnectionFactory redisTokenFactory(){
        LettuceConnectionFactory lettuceConnectionFactory = new LettuceConnectionFactory(redisConfiguration(),clientConfiguration());
        lettuceConnectionFactory.setDatabase(ConstantUtil.REDIS_INDEX_2);
        return lettuceConnectionFactory;
    }

    //RedisTemplate配置 RedisTemplate与@Cacheable独立,需要重新设置序列化方式
    @Bean
    public RedisTemplate redisTemplate(@Qualifier("redisTokenFactory") RedisConnectionFactory redisConnectionFactory) {
        RedisTemplate template = new RedisTemplate();
        template.setConnectionFactory(redisConnectionFactory);
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // value值的序列化采用fastJsonRedisSerializer
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // key的序列化采用StringRedisSerializer
        template.setKeySerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        return template;
    }

    /**
     * 缓存注解@Cache 配置
     */
    @Bean
    public CacheManager cacheManager(@Qualifier("redisServiceFactory") RedisConnectionFactory factory) {
        GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        // 配置序列化
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        RedisCacheConfiguration redisCacheConfiguration = config
                // 键序列化方式 redis字符串序列化
                .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
                // 值序列化方式 简单json序列化
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericJackson2JsonRedisSerializer))
                //不缓存Null值
                .disableCachingNullValues()
                //默认缓存失效 3天
                .entryTtl(Duration.ofDays(3));
        return RedisCacheManager.builder(factory).cacheDefaults(redisCacheConfiguration).build();
    }

    /**
     * 重写缓存key的生成方式: 类名.方法名字&[参数列表]
     */
    @Bean
    public KeyGenerator keyGenerator(){
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                StringBuilder sb = new StringBuilder();
                sb.append(target.getClass().getName()).append(".");//执行类名
                sb.append(method.getName()).append("&");//方法名
                sb.append(Arrays.toString(params));//参数
                return sb.toString();
            }
        };
    }


}

(4)RedisTemplate动态切换 dbIndex 实现分库操作

         实际上一个redisTemplate就可以完成分库操作,其本质就是通过一行代码在业务需要时通过connectionFactory.setDatabase(num)来完成动态库切换。但这样不是特别合适,因为我们需要操作连接工厂,通过最上面实现的代码可以看到,连接工厂factory是在redisTemplate配置中,通过template.setConnectionFactory(factory)去指定的,如果我们采用一个redisTemplate的方式,那么我们就需要不停的进行resetConnection的方式刷新连接配置,切换频繁,导致效率太低。


	@Autowired
    private StringRedisTemplate redisTemplate;
    //动态切换dbIndex
    public void setDataBase(int num) {
        LettuceConnectionFactory connectionFactory = (LettuceConnectionFactory) redisTemplate.getConnectionFactory();
        if (connectionFactory != null && num != connectionFactory.getDatabase()) {
        	//切换dbIndex
            connectionFactory.setDatabase(num);
            //是否允许多个线程操作共用同一个缓存连接,默认 true,false 时每个操作都将开辟新的连接
            connectionFactory.setShareNativeConnection(false);
            this.redisTemplate.setConnectionFactory(connectionFactory);
            //释放Connection对象,但RedisClient中的redisUrl对象的属性并未刷新,导致重新生成的Connection对象的DB值仍未改变
            connectionFactory.resetConnection();
            //刷新RedisClient,刷新时会自动取lettuceConnectionFactory内部属性的最新DB值填充,DB切换成功
            connectionFactory.afterPropertiesSet()
        }
    }

 

你可能感兴趣的:(mysql,数据库,java)