文章目录
一、要求
创建一个 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数据库中各个键值对过期时间均为随机产生,以此来解决缓存雪崩问题。