赞
踩
创建一个 SpringBoot 项目,对 myschool 数据库做操作,要求:
缓存(cache),原始意义是指访问速度比一般随机存取存储器(RAM)快的一种高速存储器,通常它不像系统主存那样使用 DRAM 技术,而使用昂贵但较快速的 SRAM 技术。缓存的设置是所有现代计算机系统发挥高性能的重要因素之一(摘自百度百科)。
Redis 因读写性能较高,它非常适合作为存贮数据的临时地方、成为数据交换的缓冲区,因此在一些大型互联网应用中,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;
}
从 Redis 的角度来说,它的缓存更新策略一般有 3 种,如下表:
在实际的应用中,根据不同的需缓存处理的数据性质,数据的一致性需求存在以下两种情况:
主动更新策略即更新数据库的同时更新 Redis 缓存,具体实现时,一般使用以下方案:
数据一致性问题处理基本代码逻辑:
数据一致性处理:
- 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);
缓存穿透是指客户端请求的数据在缓存中和数据库中都不存在,缓存永远不会生效。这样,每次针对此 key 的请求从缓存获取不到,请求都会压到数据源,从而可能压垮数据源。比如用一个不存在的用户 id 获取用户信息,不论缓存还是数据库都没有,若黑客利用此漏洞进行攻击可能压垮数据库。具体见下图。
常用的缓存穿透的解决方案包括:
缓存穿透问题处理(对空值进行缓存)相关代码逻辑:
if(student==null){
System.out.println("Mysql中也没有此学生");
Student s = new Student();
s.setId(id);
saveToRedis(s);
}
缓存雪崩是指在同一时段大量的缓存 key 同时失效,或者 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);
}
创建的SpringBoot项目需要整合Mybatis 框架、SpringDataRedis 框架;同时需要Mysql的驱动等;
因此创建SpringBoot项目时需要勾选以下依赖:
具体的创建步骤、注意事项及可能的报错处理可参考:软件工程综合实践课程第十一周作业( SpringBoot整合Mybatis完成CRUD操作并使用接口调试工具对接口进行测试)中“创建SpringBoot项目”部分
可参考:软件工程综合实践课程第十一周作业( SpringBoot整合Mybatis完成CRUD操作并使用接口调试工具对接口进行测试)中“接口测试工具的基本使用方法”部分
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;
}
}
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;
}
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);
}
<?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>
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);
}
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+"的学生!";
}
}
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;
}
}
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命令
<?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>
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.数据写入redis时,设置key的超时时间
- 2.修改数据时,先修改mysql,再删除redis缓存
- 3.开启事务:保证正确事务的提交
首次查找(不存在于Redis,从MySQL取数据后存入Redis)
查找前Redis中的键值对截图:
调用public Student findStudentByID(Long id)接口:
后台打印输出:
此时Redis数据库中键值对:
调用根据id更新学生信息接口public String updateStudentById(Student student):
后台打印信息:
此时查看Redis数据库可以看到更新了的对应学生信息已被删除:
再次调用public Student findStudentByID(Long id)接口查找对应学生信息:
后台打印信息:
此时查看Redis数据库可以看到更新后的数据成功缓存:
可以看到数据成功更新且保持了数据的一致性。
调用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数据库可以看到新增的学生信息也成功加入缓存:
由此可以看到成功实现了数据的一致性。
查找的数据如果在mysql中也没有,则将空对象写入redis进行缓存
缓存穿透问题处理实验:
经过删除id为61的学生信息操作后可知此时Mysql数据库与Redis数据库中均无id为61的学生的信息:
此时调用public Student findStudentById(Long id)接口查找不存在于MySQL与Redis中,id为61的学生信息:
后台打印信息:
可以看到当Mysql数据库与Redis数据库中均找不到该学生信息时,程序创建了一个id为61的空对象写
入redis进行缓存,以此来解决缓存穿透问题。
为存入Redis数据库进行缓存的键值对创建一个随机的Key的有效期
缓存雪崩处理实验:
编写public List findStudentByIds(@RequestBody Long[] ids)接口及对应的service层,mapper层代码用于批量查找学生信息
调用public List findStudentByIds(@RequestBody Long[] ids)接口批量查找id为1,2,3,4,5的学生信息并观察各个键值对的有效期:
后台打印信息:
可以看到Redis数据库中各个键值对过期时间均为随机产生,以此来解决缓存雪崩问题。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。