Redis (three)

One, SpringBoot and Redis integration

1. Introduce dependencies


    <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. Create a configuration file

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. Create a startup class

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. Unit testing

Important: Must be the same package as the startup class

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);
    }

}

Insert picture description here

Five, two template classes are provided in the springboot reids starter

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;
    }

The difference between RedisTemplate and StringRedisTemplate

  • RedisTemplate:
    RedisTemplate is the most basic operation class. Its default serialization method is JdkSerializationRedisSerializer. When storing the value, the key value will be serialized into a byte array, which is poorly readable. The value is also the same when it is stored in redis. The value is in the form of a string, and null will be returned when the value is taken.
  • StringRedisTemplate:
    StringRedisTemplate inherits from RedisTemplate<String, String>, the default serialization method is StringRedisSerializer, and the stored values ​​are all in the form of strings
 @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"));
    }
Insert picture description here


Solve RedistTemplate garbled code

@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;
    }
}

Two, Mybatis uses Redis as the second-level cache

Single-machine mybatis turns on the second-level cache
1. Turn on the global switch in the configuration file
2. Annotate @CacheNamespace in xml// Open the second-level cache
Common features of mybatis level-1 and level-2 cache :
​Commit to generate cache when querying, as long as there are additions, deletions, changes, and clearing the cache.
Mybatis queries go to level-2 cache first, level-2 then level-1, level-1 does not go to database. In
mybatis-spring application, Every time the corresponding method of the dao interface is called, a new sqlSession will be generated, and there is no first-level cache

1. The advantages of mybatis using redis as a secondary cache

Advantages :
1. Submit query efficiency
2. Save the jvm memory space of each application to facilitate garbage collection
Insert picture description here

2. Turn on the mybatis secondary cache in the configuration file

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

3. Create a custom cache class

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. Use custom cache in 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. Test

@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);
    }


}

Three, Spring uses Redis as a cache

Spring itself also supports caching, you can also use redis as a cache
Cache the results of method calls
Spring Cache uses SimpleCacheConfiguration for caching by default. If Springboot-redis starter is configured, Redis caching is used
Spring Boot cache annotations are @EnableCaching @Cacheable, @CacheEvict, @CachePut @Caching
Prerequisites turn off the second level cache
# 开启mybatis 二级缓存
mybatis.configuration.cache-enabled=false

1. Turn on spring cache

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

    public static void main(String[] args) {

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

}

Use cache

  • @ Cacheable
    acts on the method and caches the result corresponding to the method
  • @ CachePut (value = "student",key = "#student.id")// The spring cache will be updated when updating, and written to mysql
    // First update the cache (the cached result is the returned data) Modify the database
  • @ CacheEvict (value = "student",key = "#id",allEntries = true,beforeInvocation=true)// Clear all caches when deleting (otherwise data inconsistency will occur)
    // All entries starting with student will be deleted The key, allEntries = true
    // Before calling this method, clear the cache and delete it in the database (to prevent dirty data from appearing)
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 +"";
    }
}

Fourth, the common problems of redis caching

1. Cache penetration problem

Penetration: Go through (redis) to mysql query, query data that does not exist in the database, and go through reddis to query the database every time
If the result of our current query id=1000 student is not in the database, it will not be cached in redis. Next time we will query id=1000 student (after redis, redis did not go to the database to get it, no,)
Insert picture description here

2. Cache breakdown problem

Cache breakdown means that the data in the cache becomes invalid. When the user accesses it, he has to go to the database to query
Avalanche: Because the response time of mysql is too long, the tomcat request response accumulates, which may cause the memory of the tomcat connection number to be insufficient. The tomcat hangs up the entire system and collapses.
Insert picture description here

3. Cache tilt

Insert picture description here