Redis(三)

一、SpringBoot与Redis集成

1、引入依赖


    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-parent</artifactId>
        <version>2.1.5.RELEASE</version>
    </parent>

    <dependencies>

        <!--引入spring mvc 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>

        <!--
            spirng boot 单元测试
        -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>


        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
            <version>3.1.0</version>
        </dependency>



        <!-- redis 配置 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <version>2.1.1.RELEASE</version>
        </dependency>

    </dependencies>

2、创建配置文件

server.port=8080

# redis 单机配置
spring.redis.host=192.168.12.130
spring.redis.port=6379


#redis 集群配置
#spring.redis.cluster.nodes=192.168.12.130:7001,192.168.12.130:7002,192.168.12.130:7003,192.168.12.130:7004,192.168.12.130:7005,192.168.12.130:7006
#spring.activemq.close-timeout=5000

3、创建启动类

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MyApplication {
     

    public static void main(String[] args) {
     

        SpringApplication.run(MyApplication.class,args);
    }

}

4、单元测试类

重点:必须和启动类同一包

package com.qfedu;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

/**
 * 当前单元测试类 必须和启动类 在同一包下
 */

@RunWith(SpringRunner.class)
@SpringBootTest // 表明当前类是  springboot  单元测试类 必须和启动类 在同一包下
public class RedisTest {
     

    /**
     * StringRedisTemplate  所有存储数据都是string 字符串
     *    Template  模板  模板设计模式
     *              作用为使用方式 提供统一的 方法调用
     *
     */
    @Autowired // 从容器中获取stringRedisTemplate   key String  value 也是string (hash 除外)
    private StringRedisTemplate stringRedisTemplate;

    @Test
    public void  stringTest(){
     

        // ValueOperations 就是 springboot 对应 redis 提供的 key String (key value) 类型的的操作类
        ValueOperations<String, String> valueOperations = stringRedisTemplate.opsForValue();


        valueOperations.set("name", "xiaoing ");
        String name = valueOperations.get("name");

        System.out.println("name = " + name);


    }

    @Test
    public void hastTest(){
     

        // 操作 值为 hash 接口
        HashOperations<String, Object, Object> hashOperations = stringRedisTemplate.opsForHash();

        // 设置数据
        hashOperations.put("user1", "name", "xiaoxue");


        String name = (String) hashOperations.get("user1", "name");

        System.out.println("name = " + name);

    }

    /**
     * 测试 list
     */
    @Test
    public void listTest(){
     

        ListOperations<String, String> listOperations = stringRedisTemplate.opsForList();


        listOperations.leftPush("list1", "a");
        listOperations.leftPush("list1", "b");
        listOperations.leftPush("list1", "c");


        List<String> list1 = listOperations.range("list1", 0, -1);
        System.out.println("list1 = " + list1);

    }

    @Test
    public void boundValueTest(){
     


        // 在获取时 直接绑定了key   省去了每次操作制定 key 的流程
        BoundValueOperations<String, String> valueOps = stringRedisTemplate.boundValueOps("name");

        valueOps.set("xiaowang");

        String name = valueOps.get();

        System.out.println("name = " + name);

    }

    /**
     * 绑定 key  value Hash
     */
    @Test
    public void  boundHashTest(){
     

        BoundHashOperations<String, Object, Object> hashOps = stringRedisTemplate.boundHashOps("user1");

        hashOps.put("name", "xiaocui");

        Object name = hashOps.get("name");

        System.out.println("name = " + name);
    }

}

Redis(三)_第1张图片

五、在springboot reids启动器中提供了两个模板类

StringRedisTemplate

public class StringRedisTemplate extends RedisTemplate<String, String> {
     
    public StringRedisTemplate() {
     
        this.setKeySerializer(RedisSerializer.string());
        this.setValueSerializer(RedisSerializer.string());
        this.setHashKeySerializer(RedisSerializer.string());
        this.setHashValueSerializer(RedisSerializer.string());
    }

RedisTemplate

 public void afterPropertiesSet() {
     
        super.afterPropertiesSet();
        boolean defaultUsed = false;
        if (this.defaultSerializer == null) {
     
            this.defaultSerializer = new JdkSerializationRedisSerializer(this.classLoader != null ? this.classLoader : this.getClass().getClassLoader());
        }

        if (this.enableDefaultSerializer) {
     
            if (this.keySerializer == null) {
     
                this.keySerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.valueSerializer == null) {
     
                this.valueSerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.hashKeySerializer == null) {
     
                this.hashKeySerializer = this.defaultSerializer;
                defaultUsed = true;
            }

            if (this.hashValueSerializer == null) {
     
                this.hashValueSerializer = this.defaultSerializer;
                defaultUsed = true;
            }
        }

        if (this.enableDefaultSerializer && defaultUsed) {
     
            Assert.notNull(this.defaultSerializer, "default serializer null and not all serializers initialized");
        }

        if (this.scriptExecutor == null) {
     
            this.scriptExecutor = new DefaultScriptExecutor(this);
        }

        this.initialized = true;
    }

RedisTemplate 与 StringRedisTemplate区别

  • RedisTemplate:
    RedisTemplate是最基本的操作类,它默认的序列化方式是JdkSerializationRedisSerializer,在存值时,键值会被序列化为字节数组,可读性差,取值时也是一样,如果redis中存的值正常的字符串形式,取值时将返回null
  • StringRedisTemplate:
    StringRedisTemplate继承于 RedisTemplate,默认的序列化方式是StringRedisSerializer,存值取值都是按照字符串的形式
 @Autowired// 序列化 jdk  序列化
    private RedisTemplate redisTemplate;

    @Test
    public void  redisTemplateTest1(){
     


        ValueOperations valueOperations = redisTemplate.opsForValue();

        valueOperations.set("str1", "hallo world");
        System.out.println("str1:"+valueOperations.get("str1"));
    }

Redis(三)_第2张图片
解决RedistTemplate乱码

@Configuration
public class RedisConfig {
     

    
    @Bean("redisTemplate")
    public RedisTemplate redisTemplate(RedisConnectionFactory factory, Jackson2JsonRedisSerializer redisJsonSerializer) {
     
        RedisTemplate<Object, Object> template = new RedisTemplate<Object, Object>();
        //redis连接工厂
        template.setConnectionFactory(factory);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
        //redis.key序列化器
        template.setKeySerializer(stringRedisSerializer);
        //redis.value序列化器
        template.setValueSerializer(redisJsonSerializer);
        //redis.hash.key序列化器
        template.setHashKeySerializer(stringRedisSerializer);
        //redis.hash.value序列化器
        template.setHashValueSerializer(redisJsonSerializer);
        //调用其他初始化逻辑
        template.afterPropertiesSet();
        //这里设置redis事务一致
        template.setEnableTransactionSupport(true);
        return template;
    }

    /**
     * 配置redis Json序列化器
     *
     * @return
     */
    @Bean
    public Jackson2JsonRedisSerializer redisJsonSerializer() {
     
        //使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用JDK的序列化方式)
        Jackson2JsonRedisSerializer serializer = new Jackson2JsonRedisSerializer(Object.class);
        ObjectMapper mapper = new ObjectMapper();
        mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        serializer.setObjectMapper(mapper);
        return serializer;
    }
}

二、Mybatis 使用Redis 作为二级缓存

单机 mybatis 开启二级缓存
1、在配置文件开启全局开关
2、在xml 注解 @CacheNamespace// 开启器二级缓存

mybatis 一级二级 缓存共同点
​在查询时 commit 产生缓存,只要发生增删改 清空缓存
mybatis 查询先去二级缓存,二级再去一级 ,一级没有去数据库差
mybatis-spring 应用中,每调用一次dao接口对应的方法,都会产生一个新的sqlSession,不存在一级缓存

1、mybatis 使用redis 作为二级缓存的优点

优点
1.提交查询效率
2.节约每个应用的jvm 内存空间 便于垃圾回收

Redis(三)_第3张图片

2、在配置文件开启mybatis二级缓存

# 开启mybatis 二级缓存
mybatis.configuration.cache-enabled=true

3、创建 自定缓存类

package com.qfedu.cache;

import org.apache.ibatis.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;

import java.util.Set;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 * 自定义 mybatis 二级缓存  底层使用 redis 存储
 *
 * 当前 cache 没有加入到容器
 *
 */

public class RedisCache implements Cache {
     

    private  String id;

    // 读写锁  可以多个人同时读,只要有一个人写 其他人都不能读
    private  ReadWriteLock readWriteLock = new ReentrantReadWriteLock();



    public RedisCache() {
     
    }

    public RedisCache(String id) {
     
        System.out.println("命名空间-----id:"+id);
        this.id = id;
    }

    // 获取的id 就是 当前mapper 的命名空间
    public String getId() {
     
        return id;
    }


    /**
     * 向缓存中存放数据   key(类似sql) value (查询结果)
     * @param o
     * @param o1
     */
    public void putObject(Object key, Object value) {
     
        System.out.println("存储  key = " + key);
            getRedisTemplate().opsForValue().set(key.toString(), value, 10, TimeUnit.MINUTES);
    }

    /**
     * 获取缓存是 通过 key
     * @param key
     * @return
     */
    public Object getObject(Object key) {
     
        System.out.println("获取key 对应的值  key = " + key);

        return getRedisTemplate().opsForValue().get(key.toString());
    }

    /**
     * 清除key 对应的缓存
     * @param key
     * @return
     */
    public Object removeObject(Object key) {
     
        System.out.println("删除  key = " + key);
        return getRedisTemplate().delete(key.toString());
    }

    /**
     * 清空当前命名空间二级缓存
     *          就是删除所有 对应namespace(id) 的 key
     */
    public void clear() {
     
        // 获取到所有包含 namespace(id) 的 key
        Set<String> keys = getRedisTemplate().keys("*" + id + "*");

        for (String key : keys) {
     
            System.out.println("遍历清空所有的namespace 下 的key = " + key);
            getRedisTemplate().delete(key);
        }

    }

    /**
     * 获取当前命名空间对应的条数
     * @return
     */
    public int getSize() {
     

        System.out.println("获取当前命名空间下 的条数");
        // 获取到所有包含 namespace(id) 的 key
        Set<String> keys = getRedisTemplate().keys("*" + id + "*");
        return keys.size();
    }

    public ReadWriteLock getReadWriteLock() {
     
        return readWriteLock;
    }


    /**
     * 获取容器中的 redisTemplate
     * @return
     */
    public  RedisTemplate getRedisTemplate(){
     

        // 获取到容器
        ApplicationContext applicationContext = ApplicationHolder.getApplicationContext();

        return (RedisTemplate) applicationContext.getBean("redisTemplate");
    }
}

package com.qfedu.cache;

import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Component;

/**
 *
 * ApplicationContextAware  作用 就是 在生成 对应的实例是调用,将容器ApplicationContext 传进来
 */
@Component// 将当前类加入到容器中  ,如果当前类实现ApplicationContextAware 还会在创建bean调用改接口对应的setApplicationContext
public class ApplicationHolder implements ApplicationContextAware {
     

    private static ApplicationContext applicationContext;

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
     
        // 持有上下文对象
        this.applicationContext  = applicationContext;
    }


    /**
     * 获取核心容器
     * @return
     */
    public static ApplicationContext getApplicationContext() {
     
        return applicationContext;
    }
}

4、在xml 中使用自定义缓存

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.qfedu.dao.StudentDao">

    <!--  开启二级缓存

        type="" mybatis  会使用 用户自定的二级缓存 存储 如果没有配置就是用 jvm

        mybatis 创建 RedisCache ,没有加入到容器中
    -->
    <cache type="com.qfedu.cache.RedisCache" eviction="LRU" size="1024" flushInterval="60"></cache>

    <!--
            在springboot中使用别名有 警告 误报没有影响
            可以使用全限定名解决  com.qfedu.entity.Student

    -->
    <select id="findAllStudent" resultType="com.qfedu.entity.Student">
        select * from student_tb
    </select>


    <update id="updateStudent">
        update student_tb  set name = #{
     name},age =  #{
     age},sex = #{
     sex},height=#{
     height} where id =#{
     id}
    </update>

</mapper>

5、测试

@RunWith(SpringRunner.class)
@SpringBootTest
public class MyBatisTest {
     

    @Autowired
    private StudentService studentService;

    @Test
    public void findAllStudentTest(){
     
        // 第一次查询
        List<Student> allStudent = studentService.findAllStudent();

        System.out.println("allStudent = " + allStudent);

        System.out.println(" xiaocui!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!*********************");
        // 第二次查询
        List<Student> allStudent2 = studentService.findAllStudent();

        System.out.println("allStudent2 = " + allStudent2);

    }


    @Test
    public void updateStudentTest(){
     

        Student student = new Student();
        student.setId(2);
        student.setName("崔老师");
        student.setAge(20);
        student.setSex("M");
        student.setHeight(190);

        // 只要发生增删改 清空二级缓存
        int num =  studentService.updateStudent(student);

        System.out.println("num = " + num);
    }


}

三、Spring使用Redis作为缓存

Spring 本身也支持缓存,也可以使用使用redis 作为缓存

缓存方法调用的结果

Spring Cache 默认使用SimpleCacheConfiguration 来进行缓存的,如果配置了Springboot-redis启动器则使用Redis缓存

Spring Boot缓存注解有 @EnableCaching @Cacheable、@CacheEvict、@CachePut @Caching

前提条件 关闭二级缓存

# 开启mybatis 二级缓存
mybatis.configuration.cache-enabled=false

1、开启spring缓存

@MapperScan("com.qfedu.dao")
@SpringBootApplication
@EnableCaching// 开启 spring 缓存
public class MyApplication {
     

    public static void main(String[] args) {
     

        SpringApplication.run(MyApplication.class,args);
    }

}

使用缓存

  • @Cacheable
    作用在方法上 ,将该方法对应的结果进行缓存
  • @CachePut(value = “student”,key = “#student.id”)// 在更新时 会更新 spring 缓存 ,并且写入mysql
    // 先更新缓存 (缓存结果为 返回的数据) 在 修改数据库
  • @CacheEvict(value = “student”,key = “#id”,allEntries = true,beforeInvocation=true)// 在删除的时候清空所有的缓存(否则会出现数据不一致问题)
    // 会删除所有以 student 开头的 key ,allEntries = true
    // 调用在这个 方法之前 先清空缓存 在去数据库删除(防止脏数据出现)
package com.qfedu.controller;

import com.qfedu.entity.Student;
import com.qfedu.service.StudentService;
import org.junit.After;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.cache.annotation.Caching;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

/**
 * Student 相关
 */
@RestController
public class StudentController {
     

    @Autowired
    private StudentService studentService;


    @RequestMapping("/findAllStudent")
    @Cacheable(value = "student") // 将结果进行缓存   student 对应缓存key 的前缀
    public List<Student> findAllStudent(){
     
        System.out.println("查询所有学生");

        return studentService.findAllStudent();
    }


    @RequestMapping("/findStudentById")
    @Cacheable(value = "student",key = "#id",condition = "#id>2")// key 制定缓存key  condition 制定缓存条件,不满足条件不会缓存
    public Student findStudentById(int id) {
     

        System.out.println("查询所有学生id"+ id);

        return studentService.findStudentById(id);
    }



    @RequestMapping("/updateStudent")
    @CachePut(value = "student",key = "#student.id")// 在更新时 会更新 spring 缓存 ,并且写入mysql
    //  先更新缓存 (缓存结果为 返回的数据) 在 修改数据库
    //  更新也尽量用 @CacheEvict 有可能发生脏数据
    public Student  updateStudent(Student student){
     


        int num =   studentService.updateStudent(student);


        return student;
    }

    @RequestMapping("/deleteStudentById")
    @CacheEvict(value = "student",key = "#id",allEntries = true,beforeInvocation=true)// 在删除的时候清空所有的缓存(否则会出现数据不一致问题)
    // 会删除所有以 student 开头的 key  ,allEntries = true
    // 调用在这个 方法之前 先清空缓存 在去数据库删除(防止脏数据出现)   beforeInvocation=true

    public String deleteStudentById(int id){
     

        int i = studentService.deleteStudentById(id);

        return i +"";
    }
}

四、redis缓存的常见问题

1、缓存穿透问题

穿透:穿过去(redis)去mysql 查询 ,查询数据库中不存在的数据,每次都要穿过reddis 去数据库查询一下

当前我们查询的结果id=1000 student 在数据库中没有时,不会在redis中缓存,下一次再去查询id=1000 student (经过redis ,redis 没有 去数据库拿,没有,)

Redis(三)_第4张图片

2、缓存击穿问题

缓存击穿,就是缓存中的数据失效,用户访问时,不得已去数据库查询

雪崩:就是因为 mysql 响应时间过长,造成 tomcat 请求响应堆积,可能造成tomcat 连接数 内存不够用 tomcat挂掉 整个系统崩塌

Redis(三)_第5张图片

3、缓存倾斜

Redis(三)_第6张图片

你可能感兴趣的:(JavaEE,java,redis,mybatis)