SpringBoot项目中使用Redis作为数据缓存,Redis的缓存机制,数据一致性、缓存穿透和缓存雪崩等问题的处理(非关系型数据库技术课程 第十一周作业)

一、要求

创建一个 SpringBoot 项目,对 myschool 数据库做操作,要求:

  • 项目名 ” week11_redis_ 学号 ” ;
  • 整合 Mybatis 框架, 或使用 MybatisPlus ;
  • 整合 s SpringDataRedis 框架;
  • 对 student 表进行操作 ,在学生表中插入自己的数据 ,实现根据 id 查询 、更新和删除学生的功能 ;
  • 创建相应的Pojo 层、Mapper 层、Service 层、Controller 层;
  • 在各层中,创建针对user 表操作的类或接口;
  • 使用 redis 作为数据缓存,并考虑数据一致性、缓存穿透和缓存雪崩等问题的处理
  • 启动服务器, 测试控制器中的方法, 可以使用 Postman 进行测试或自己模拟数据进行测试 ;
  • 将运行结果截图,保存在实验报告 中;
  • 将项目和实验报告一起提交。

二、知识总结

1.Redis 缓存

缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用 DRAM 技术,而使用昂贵但较快速的 SRAM 技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一(摘自百度百科)。

Redis 因读写性能较高,它非常适合作为存贮数据的临时地方、成为数据交换的缓冲区,因此在一些大型互联网应用中,Redis 常用来进行数据缓存处理。

下图是项目中使用 Redis 作为 MySQL 缓存的一般流程。
在这里插入图片描述
Redis 作为缓存,给系统带来了一些好处:

  • 降低后端负载
  • 提高读写效率,降低响应时间

但同时,在项目中使用缓存也会带来一些问题:

  • 数据一致性问题:redis 和 mysql 如何保存数据一致性
  • 代码维护成本:处理缓存穿透、缓存雪崩等问题
  • 运维成本:redis 集群的维护

这些问题是 Redis 作为缓存时必须要考虑的。

Redis作为MySQL缓存基本代码逻辑实现

//根据id查询学生信息
    public Student findStudentById(Long id){
        //1.查看Redis缓存中是否有数据
        Student student =getStudentByRedis(id);

        //2.如果Redis中有该学生,则返回
        if (student !=null){
            System.out.println("Redis缓存中查询到此学生");
            return student;
        }

        // 3.Redis中没有,则到mysql中查询,
        // 缓存穿透处理:如果mysql中也没有,则将空对象写入redis
        System.out.println("Redis缓存中没有此学生");
        student = studentMapper.findStudentById(id);
        if(student==null){
            System.out.println("Mysql中也没有此学生");
            Student s = new Student();
            s.setId(id);
            saveToRedis(s);
        }
        else{
            System.out.println("Mysql中查询到此学生");
            saveToRedis(student);
        }
        return student;
    }

2. 数据一致性问题

2.1 Redis 缓存更新策略

从 Redis 的角度来说,它的缓存更新策略一般有 3 种,如下表:
在这里插入图片描述
在实际的应用中,根据不同的需缓存处理的数据性质,数据的一致性需求存在以下两种情况:

  • 弱一致性需求:如一些商品类型、客户类型、店铺类型等表示某种类型的数据,
    一般变动比较少,对这些数据进行缓存处理时,可以使用内存淘汰机制;
  • 强一致性需求:如商品库存、店铺主页、促销、优惠券等数据,经常发生变化,对这些数据进行缓存处理时,可以使用主动更新策略,并以超时剔除作为兜底方案。

2.2 主动更新策略

主动更新策略即更新数据库的同时更新 Redis 缓存,具体实现时,一般使用以下方案:

  • 删除缓存更新数据库时,删除缓存,让缓存失效,查询时再更新缓存;
  • 保证数据的原子性:单体系统,将缓存与数据库操作放在一个事务,分布式系统,则利用 TCC 等分布式事务方案;
  • 减少并发操作带来的数据错误先操作数据库,再删除缓存
  • 使用超时剔除机制:当数据写入缓存时,一定设定超时时间

数据一致性问题处理基本代码逻辑:

数据一致性处理:

  • 1.数据写入redis时,设置key的超时时间
  • 2.修改数据时,==先修改mysql,再删除redis缓存 ==
  • 3.开启事务:保证正确事务的提交
//根据id修改用户信息
    @Transactional   //1:开启事务
    public String updateStudentById(Student student) {
        Long id = student.getId();
        if (id == null) {
            return "学生id不能为空";
        }
        //2. 先更新mysql数据库
        studentMapper.updateStudentById(student);
        //3. 后删除缓存
        String key="student:"+id;
        redisTemplate.delete(key);
        return "更新成功";
    }

在将数据存入Redis缓存时设置超时时间如:

//修改 1:设置key的过期时间为6分钟
redisTemplate.expire(key,360, TimeUnit.SECONDS);

3. 缓存穿透

3.1 什么是缓存穿透

缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,缓存永远不会生效。这样,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。具体见下图。
在这里插入图片描述

3.2 解决方案

常用的缓存穿透的解决方案包括:

  • 对空值进行缓存:即使一个查询返回的数据为空,仍然把这个空结果(null)进行缓存,同时还可以对空结果设置较短的过期时间。这种方法实现简单,维护方便,但是会额外的内存消耗。具体见下图;
    在这里插入图片描述
  • 采用布隆过滤器:(布隆过滤器(Bloom Filter)是 1970 年由布隆提出的。它实际上是一个很长的二进制向量(位图)和一系列随机映射函数(哈希函数)。布隆过滤器可以用于检索一个元素是否在一个集合中。它的优点是空间效率和查询时间都远远超过一般的算法,缺点是有一定的误识别率和删除困难。
  • 进行实时监控:当发现 Redis 的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务
  • 增强 id 的复杂度,避免被猜测 id 规律
  • 做好数据的基础格式校验
  • 加强用户权限校验

缓存穿透问题处理(对空值进行缓存)相关代码逻辑:

if(student==null){
            System.out.println("Mysql中也没有此学生");
            Student s = new Student();
            s.setId(id);
            saveToRedis(s);
        }

4. 缓存雪崩

4.1 什么是缓存雪崩

缓存雪崩是指在同一时段大量的缓存 key 同时失效,或者 Redis 服务宕机,导致大量请求到达数据库,带来巨大压力。具体如下图所示:
在这里插入图片描述

4.2 解决方案

常用的缓存雪崩的解决方案包括:

  • 给不同的 Key 的 TTL 添加随机值
  • 利用 Redis 集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存雪崩的解决方案(给不同的 Key 的 TTL 添加随机值)相关代码逻辑:

//保存Student信息到Redis,使用hash类型
    public void saveToRedis(Student student) {
        //设置key: student:ID
        String key="student:"+student.getId();
        //各字段的值都存入Redis
        redisTemplate.opsForHash().put(key,"sname",student.getSname()+"");
        redisTemplate.opsForHash().put(key,"dept",student.getDept()+"");
        redisTemplate.opsForHash().put(key,"age",student.getAge()); //!!! Age为Int类型不用+“”

        //设置key的过期时间为6分钟
//        redisTemplate.expire(key,360, TimeUnit.SECONDS);

        //缓存雪崩处理:创建一个随机的KEY 的有效期
        int expiredTime=360+new Random().nextInt(100);
        System.out.println("过期时间: "+expiredTime);
        redisTemplate.expire(key,expiredTime, TimeUnit.SECONDS);
    }

5. 项目创建

创建的SpringBoot项目需要整合Mybatis 框架、SpringDataRedis 框架;同时需要Mysql的驱动等;
因此创建SpringBoot项目时需要勾选以下依赖:
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
具体的创建步骤、注意事项及可能的报错处理可参考:软件工程综合实践课程第十一周作业( SpringBoot整合Mybatis完成CRUD操作并使用接口调试工具对接口进行测试)中“创建SpringBoot项目”部分

6、接口调试工具的基本使用

可参考:软件工程综合实践课程第十一周作业( SpringBoot整合Mybatis完成CRUD操作并使用接口调试工具对接口进行测试)中“接口测试工具的基本使用方法”部分

三、项目结构

在这里插入图片描述

四、完整参考代码

com.example.config包

com.example.config.RedisConfig

package com.example.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory){
        // 创建RedisTemplate对象
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        // 设置连接工厂
        template.setConnectionFactory(connectionFactory);
        // 创建JSON序列化工具
        GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
        // 设置Key的序列化
        template.setKeySerializer(RedisSerializer.string());
        template.setHashKeySerializer(RedisSerializer.string());
        // 设置Value的序列化
        template.setValueSerializer(jsonRedisSerializer);
        template.setHashValueSerializer(jsonRedisSerializer);
        // 返回
        return template;
    }
}

com.example.pojo包

com.example.pojo.Student

package com.example.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

/**
 * @projectName: week11_redis_ 
 * @package: com.example.pojo
 * @className: Student
 * @author: GCT
 * @description: TODO
 * @date: 2022/11/11 20:38
 * @version: 1.0
 */

@Data
@NoArgsConstructor //自动生成无参构造函数
@AllArgsConstructor
public class Student {
    Long id;
    String sname;
    String dept;
    int age;
}

com.example.mapper包

com.example.mapper.StudentMapper

interface类型文件

package com.example.mapper;

import com.example.pojo.Student;
import com.example.pojo.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.io.IOException;
import java.util.List;

@Mapper
public interface StudentMapper {

    //   直接使用@Select()注解
    @Select("SELECT * FROM student")
    public List<Student> getAllStudentMap();

    // 该方法使用了带一个参数的查询语句,返回一条记录
    public Student findStudentById(Long id);

//    根据传入的id数据查找出一个或多个学生信息
    public List<Student> findStudentByIds(Long[] ids);

    //更新用户信息
    public int updateStudentById(Student student);

    // 该方法插入一条记录,带参数,更新操作一定要提交事务
    public int addStudent(Student student);

    //    根据id删除记录
    public int deleteStudentById(Long id);
}

com/example/mapper/StudentMapper.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.example.mapper.StudentMapper">
    <!--
            1.SQL语句带一个参数
            parameterType:指定接收参数类型,返回一条记录,用下标取参数
            parameterType:参数类型
         -->
    <select id="findStudentById" resultType="Student" parameterType="Long">
        SELECT * FROM student WHERE id=#{0}
    </select>

    <!-- 批量查找 -->
    <select id="findStudentByIds" resultType="Student" parameterType="Long[]" >
        SELECT * FROM student WHERE id IN
        <foreach collection="array" item="id" open="(" close=")" separator=",">
            #{id}
        </foreach>
    </select>


    <update id="updateStudentById"  parameterType="Student">
        UPDATE student set sname=#{sname},dept=#{dept},age=#{age}
        where id=#{id}
    </update>

    <insert id="addStudent" parameterType="Student">
        <!--这个可以在插入记录后将该记录的ip查出来,使用student.getId()可以获取!!-->
        <selectKey keyProperty="id" order="AFTER" resultType="Long">
            select LAST_INSERT_ID()
        </selectKey>
        INSERT INTO student SET sname=#{sname},dept=#{dept},age=#{age}
    </insert>

    <!--    根据id删除记录-->
    <!--	注意传参类型改成long后这里parameterType也要改-->
    <delete id="deleteStudentById" parameterType="Long">
        DELETE FROM student WHERE id=#{id}
    </delete>

</mapper>

com.example.service包

com.example.service.StudentService

interface类型文件

package com.example.service;

import com.example.pojo.Student;

import java.io.IOException;
import java.util.List;

public interface StudentService {

    public Student findStudentById(Long id);

    //    根据传入的id数据查找出一个或多个学生信息
    public List<Student> findStudentByIds(Long[] ids);


    public String updateStudentById(Student student);

    public List<Student> getAllStudent();

    public int addStudent(Student student);

//    根据id删除学生
    public String deleteStudentById(Long id);

    //存储Student对象到Redis中
    void saveToRedis(Student student);
}

com.example.service.impl包

com.example.service.impl.StudentServiceImpl
package com.example.service.impl;

import com.example.mapper.StudentMapper;
import com.example.pojo.Student;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * @projectName: week11_redis_ 
 * @package: com.example.service.impl
 * @className: StudentServiceImpl
 * @author: GCT
 * @description:
 *  数据一致性处理:
 *  1.数据写入redis时,设置key的超时时间,
 *  2.修改数据时,先修改mysql,再删除redis缓存
 *  3.开启事务:保证正确事务的提交
 *
 *  缓存穿透和缓存雪崩处理方案:
 *  缓存穿透处理:如果mysql中也没有,则将空对象写入redis进行缓存
 *  缓存雪崩处理 :为存入Redis数据库进行缓存的键值对创建一个随机的Key的有效期
 * @date: 2022/11/11 20:39
 * @version: 1.0
 */
@Service
public class StudentServiceImpl implements StudentService {
//    @Resource
     @Autowired
    private StudentMapper studentMapper;
    @Autowired
    private RedisTemplate redisTemplate;

    //根据id查询学生信息
    public Student findStudentById(Long id){
        //1.查看Redis缓存中是否有数据
        Student student =getStudentByRedis(id);

        //2.如果Redis中有该学生,则返回
        if (student !=null){
            System.out.println("Redis缓存中查询到此学生");
            return student;
        }

        // 3.Redis中没有,则到mysql中查询,
        // 缓存穿透处理:如果mysql中也没有,则将空对象写入redis
        System.out.println("Redis缓存中没有此学生");
        student = studentMapper.findStudentById(id);
        if(student==null){
            System.out.println("Mysql中也没有此学生");
            Student s = new Student();
            s.setId(id);
            saveToRedis(s);
        }
        else{
            System.out.println("Mysql中查询到此学生");
            saveToRedis(student);
        }
        return student;
    }

    //    根据传入的id数据查找出一个或多个学生信息
    /**
     * @param ids:
     * @return List<Student>
     * @author GCT
     * @description 根据传入的id数据查找出一个或多个学生信息
     * @date 2022/11/12 11:30
     */
    public List<Student> findStudentByIds(Long[] ids){
        List<Student> studentList = new ArrayList<Student>();
        for (Long id:ids){
//            遍历ids数组,使用findStudentById(id)将
//            返回的Student类型数据添加到studentList集合中
            studentList.add(findStudentById(id));
        }
        return studentList;
    }


    //根据id修改用户信息
    @Transactional   //修改3:开启事务
    public String updateStudentById(Student student) {
        Long id = student.getId();
        if (id == null) {
            return "学生id不能为空";
        }
        //修改2. 先更新mysql数据库
        studentMapper.updateStudentById(student);
        //修改2. 后删除缓存
        String key="student:"+id;
        redisTemplate.delete(key);
        return "更新成功";
    }

    //保存Student信息到Redis,使用hash类型
    public void saveToRedis(Student student) {
        //设置key: student:ID
        String key="student:"+student.getId();
        //各字段的值都存入Redis
        redisTemplate.opsForHash().put(key,"sname",student.getSname()+"");
        redisTemplate.opsForHash().put(key,"dept",student.getDept()+"");
        redisTemplate.opsForHash().put(key,"age",student.getAge()); //!!! Age为Int类型不用+“”

        //修改 1:设置key的过期时间为6分钟
//        redisTemplate.expire(key,360, TimeUnit.SECONDS);

        //缓存雪崩修改 :创建一个随机的KEY 的有效期
        int expiredTime=360+new Random().nextInt(100);
        System.out.println("过期时间: "+expiredTime);
        redisTemplate.expire(key,expiredTime, TimeUnit.SECONDS);
    }

    //从redis中查询Student
    public Student getStudentByRedis(Long id){
        String key="student:"+id;
        if (redisTemplate.hasKey(key)){
            String sname=(String) redisTemplate.opsForHash().get(key,"sname");
            String dept= (String) redisTemplate.opsForHash().get(key,"dept");
            int age = (Integer)redisTemplate.opsForHash().get(key,"age");
            Student student = new Student();
            student.setId(id);
            student.setSname(sname);
            student.setDept(dept);
            student.setAge(age);
            return student;
        }
        return null;
    }




    //查询用户
    public List<Student> getAllStudent() {
        return studentMapper.getAllStudentMap();
    }

    /**
     * @param student:
     * @return int
     * @author GCT
     * @description
     * 缓存穿透处理时对不存在的学生创建了
     * 对应id的空对象存入缓存,因此在新增学生信息时加个判断,
     * 判断新增的学生id是否存在于Redis缓存中,若存在,则删去对应缓存
     * @date 2022/11/12 11:45
     */
    @Transactional   //开启事务
    public int addStudent(Student student) {

        //先在mysql数据库新增数据
        int i = studentMapper.addStudent(student);
        Long studentId = student.getId();
        Student studentByRedis = getStudentByRedis(studentId);
        //后判断,若在缓存中存在对应信息则删除缓存
        if (studentByRedis!=null){
            String key="student:"+studentId;
            redisTemplate.delete(key);//若存在对应的对象,则删除缓存
        }
        System.out.println("id:  "+studentId);

        return i;
    }

    //    根据id删除学生
    /**
     * @param id:
     * @return int
     * @author GCT
     * @description 根据id删除学生
     * 使用事务
     * 先删除Mysql数据库内信息
     * 再删除redis数据库内信息
     * @date 2022/11/11 21:30
     */
    @Transactional   //开启事务
    public String deleteStudentById(Long id){
        if (id == null) {
            return "学生id不能为空!";
        }
        //先更新mysql数据库
        studentMapper.deleteStudentById(id);
        //后删除缓存
        String key="student:"+id;
        redisTemplate.delete(key);
        return "成功删除id为"+id+"的学生!";

    }


}

com.example.controller包

com.example.controller.StudentController

package com.example.controller;

import com.example.pojo.Student;
import com.example.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.io.IOException;
import java.util.List;

/**
 * @projectName: week11_redis_x
 * @package: com.example.controller
 * @className: StudentController
 * @author: GCT
 * @description: TODO
 * @date: 2022/11/11 20:38
 * @version: 1.0
 */
@RestController
@RequestMapping("/student")
public class StudentController {

    @Autowired
    public StudentService studentServie;


    //根据id查询学生
    @GetMapping("/getAllStudent")
    public List<Student> getAllStudent(){
        List<Student> allStudent = studentServie.getAllStudent();
        System.out.println(allStudent);
        return allStudent;
    }


    //根据id查询学生
    @GetMapping(value = "/findStudentByID")
    public Student findStudentByID(Long id){
        Student student = null;
//        Long id = -1l;
        for(int i=0;i<10;i++) {
            student = studentServie.findStudentById(id);
            System.out.println(student);
//            return student;
        }
        return student;
    }

    //根据多个id查询多个学生
    @PostMapping(value = "/findStudentByIds")
    public List<Student> findStudentByIds(@RequestBody Long[] ids){
        return studentServie.findStudentByIds(ids);
    }



    //修改学生信息
    @PostMapping("/updateStudentById")
    public String updateStudentById(Student student) throws IOException {

        String info= studentServie.updateStudentById(student);
        System.out.println(info);
        return info;

    }

//    增加学生信息
    @PostMapping("/addStudent")
    public int addStudent(Student student){
        int res = studentServie.addStudent(student);
        System.out.println("res"+res);
        return res;
    }

    //    根据id删除学生
    @PostMapping("/deleteStudentById")
    public String deleteStudentById(Long id){
        String info = studentServie.deleteStudentById(id);
        System.out.println("info: "+info);
        return info;
    }

}

src/main/resources文件夹

src/main/resources/application.yml

spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/myschool?serverTimezone=Hongkong?characterEncoding=utf8&serverTimezone=GMT%2B8
    username: root
    password: 密码
  redis:
    host: 127.0.0.1
    port: 6379
    lettuce:
      pool:
        max-active: 10
        max-idle: 10
        min-idle: 0
        max-wait: 1000

mybatis:
  mapper-locations: classpath:com/exmaple/mapper/*.xml    #指定sql配置文件的位置
  type-aliases-package: com.example.pojo      #指定实体类所在的包名
  configuration:
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl   #输出SQL命令

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>week11_redis_xxxxxxxxxxx</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>week11_redis_xxxxxxxxxxx</name>
    <description>week11_redis_xxxxxxxxxxx</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>


        <!--        注意要写这个resources,不然会报Invalid bound statement (not found): com.example.mapper.....-->
        <!--        遇到这个报错:-->
        <!--        1、检查springboot的application中是否有加@MapperScan(basePackages = "com.example.mapper")注解-->
        <!--        2、检查...Mapper.java文件是否有@Mapper注解-->
        <!--        3、检查配置文件(application.yml)中是否有-->
        <!--        mybatis:-->
        <!--            mapper-locations: classpath:com/exmaple/mapper/*.xml    #指定sql配置文件的位置-->
        <!--        4、检查pom.xml文件中<build>标签中是否有配这个resources-->
        <!-- 将java目录下的xml文件打包-->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
            </resource>
            <resource>
                <directory>src/main/resources</directory>
                <includes>
                    <include>**.*</include>
                </includes>
            </resource>
        </resources>

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                </configuration>
            </plugin>
        </plugins>
    </build>

</project>

com.example.Week11RedisxxxxxxxxxxxApplication

package com.example;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
@MapperScan(basePackages = "com.example.mapper") //记得加这个
public class Week11RedisxxxxxxxxxxxApplication {

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

}

五、数据一致性、缓存穿透、缓存雪崩实验

1、数据一致性问题实验

数据一致性处理:

  • 1.数据写入redis时,设置key的超时时间
  • 2.修改数据时,先修改mysql,再删除redis缓存
  • 3.开启事务:保证正确事务的提交

根据id更新学生信息方法数据一致性实验

首次查找(不存在于Redis,从MySQL取数据后存入Redis)

查找前Redis中的键值对截图:
在这里插入图片描述
调用public Student findStudentByID(Long id)接口:
在这里插入图片描述
后台打印输出:
在这里插入图片描述
此时Redis数据库中键值对:
在这里插入图片描述

调用根据id更新学生信息接口public String updateStudentById(Student student):
在这里插入图片描述
后台打印信息:
在这里插入图片描述
此时查看Redis数据库可以看到更新了的对应学生信息已被删除:
在这里插入图片描述
再次调用public Student findStudentByID(Long id)接口查找对应学生信息:
在这里插入图片描述
后台打印信息:

在这里插入图片描述
此时查看Redis数据库可以看到更新后的数据成功缓存:
在这里插入图片描述
可以看到数据成功更新且保持了数据的一致性。

根据id删除学生信息方法数据一致性实验结果:

调用public Student findStudentById(Long id)查找id为61的学生:
调用前Mysql数据库对应信息:
在这里插入图片描述
调用前Redis数据库键值对信息:
在这里插入图片描述

调用该接口:
在这里插入图片描述
后台打印输出:
在这里插入图片描述
此时该学生信息已存入Redis中进行缓存:
在这里插入图片描述
此时调用public String deleteStudentById(Long id)接口删除id为61的学生:
在这里插入图片描述后台打印输出:
在这里插入图片描述
此时查看MySQL数据库可以看到数据已成功删除:
在这里插入图片描述
再查看Redis数据库:
在这里插入图片描述
可以看到对应的缓存信息也已经被删除,由此可以看到成功实现了数据的一致性。

新增学生信息方法数据一致性实验

由于缓存穿透处理时对不存在的学生创建了对应id的空对象存入缓存,因此在新增学生信息时加个判断,判断新增的学生id是否存在于Redis缓存中,若存在,则删去对应缓存

id为109的学生暂不存在于数据库中:
在这里插入图片描述
此时Redis数据库中也无对应的信息:
在这里插入图片描述
调用public List findStudentByIds(@RequestBody Long[] ids)接口查找id为109的学生信息

由于缓存穿透问题处理,程序会将id为109的空对象写入redis进行缓存
在这里插入图片描述
后台打印输出:
在这里插入图片描述
此时查看数据库可以看到id为109的空对象已存入数据库中缓存
在这里插入图片描述
此时调用public int addStudent(Student student)接口新增id为109的学生信息(id自动递增,现在Mysql数据库中末尾id为108,插入新数据后新数据id为109)
在这里插入图片描述
后台打印信息:
在这里插入图片描述
此时查看Redis数据库可以看到id为109的缓存信息已被删除:

在这里插入图片描述
此时调用public Student findStudentByID(Long id)查询id为109的学生信息:
在这里插入图片描述
后台打印输出:
在这里插入图片描述
可以看到成功查找出新增的学生信息。

查看Redis数据库可以看到新增的学生信息也成功加入缓存:
在这里插入图片描述
由此可以看到成功实现了数据的一致性。

2. 缓存穿透问题处理实验

查找的数据如果在mysql中也没有,则将空对象写入redis进行缓存

缓存穿透问题处理实验:
经过删除id为61的学生信息操作后可知此时Mysql数据库与Redis数据库中均无id为61的学生的信息:
在这里插入图片描述
在这里插入图片描述此时调用public Student findStudentById(Long id)接口查找不存在于MySQL与Redis中,id为61的学生信息:
在这里插入图片描述
后台打印信息:

在这里插入图片描述
可以看到当Mysql数据库与Redis数据库中均找不到该学生信息时,程序创建了一个id为61的空对象写
入redis进行缓存,以此来解决缓存穿透问题。

3. 缓存雪崩处理实验

为存入Redis数据库进行缓存的键值对创建一个随机的Key的有效期

缓存雪崩处理实验:
编写public List findStudentByIds(@RequestBody Long[] ids)接口及对应的service层,mapper层代码用于批量查找学生信息

调用public List findStudentByIds(@RequestBody Long[] ids)接口批量查找id为1,2,3,4,5的学生信息并观察各个键值对的有效期:
在这里插入图片描述
后台打印信息:
在这里插入图片描述
可以看到Redis数据库中各个键值对过期时间均为随机产生,以此来解决缓存雪崩问题。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

GCTTTTTT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值