- 并发:多个线程同时操作某一个(些)资源,带来数据的不确定性、不稳定性、不安全性
- 同步:在某一个时刻,只有一个线程访问资源 解决并发问题,性能低下(程序不能让性能过于低下)
- 锁:唯一 对象监视器
- 缓存穿(刺)透:缓存有(没有)数据,访问了数据库
- 缓存雪崩:在某一个时刻,缓存中大部分 同时失效,而此时恰好有很多线程并发访问,导致数据库无法处理这么多访问而瘫痪
- 评估
- 多线程:不是计算一个数据 而是描述的是一种状态
一、Spring Boot 集成Redis单机模式
1. 案例思路
完善根据学生id查询学生的功能,先从redis缓存中查找,如果找不到,再从数据库中查找,然后放到redis缓存中
2. 实现步骤
首先通过MyBatis逆向工程生成实体bean和数据持久层
A. 在pom.xml文件中添加redis依赖和其它配置
<!-- 加载spring boot redis包 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
<build>
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
</resource>
</resources>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
B. 在Spring Boot核心配置文件application.properties中配置redis连接信息
server.port=9005
server.servlet.context-path=/005-springboot-redis
#数据库的配置
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/springboot?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT%2B8
spring.datasource.username=root
spring.datasource.password=root
#redis配置
spring.redis.host=192.168.60.130
spring.redis.port=6379
spring.redis.password=123456
C. 启动redis服务
[root@Suke /]# cd /usr/local/redis-4.0.6/src/
[root@Suke src]# ./redis-server ../redis.conf &
D. RedisController 类
@RestController
public class RedisController {
@Autowired
private StudentService studentService;
/*
* http://localhost:9005/005-springboot-redis/springboot/allStudentCount
* */
@GetMapping(value = "/springboot/allStudentCount")
public Object allStudentCount(HttpServletRequest request) {
Long allStudentCount = studentService.queryAllStudentCount();
return "学生总人数:" + allStudentCount;
}
}
E. StudentService 接口
public interface StudentService {
Long queryAllStudentCount();
}
F. 在StudentServiceImpl中注入RedisTemplate并修改根据id获取学生的方法
配置了上面的步骤,Spring Boot将自动配置RedisTemplate,在需要操作redis的类中注入redisTemplate即可。
注意:Spring Boot帮我们注入RedisTemplate类,泛型里面只能写 <String, String>、<Object, Object>或者什么都不写
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Autowired
RedisTemplate redisTemplate;
@Override
public Long queryAllStudentCount() {
//从redis缓存中获取总人数
Long allStudentCount = (Long) redisTemplate.opsForValue().get("allStudentCount");
//判断是否为空
if (allStudentCount == null) {
//从数据库查询
allStudentCount = studentMapper.selectAllStudentCount();
//将值再存放到redis缓存中
redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);
}
return allStudentCount;
}
}
G. Student 实体类
public class Student {
private Integer id;
private String name;
private Integer age;
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
}
H. StudentMapper 接口
@Repository
public interface StudentMapper {
Long selectAllStudentCount();
}
I. 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.suke.springboot.mapper.StudentMapper">
<resultMap id="BaseResultMap" type="com.suke.springboot.model.Student">
<id column="id" jdbcType="INTEGER" property="id"/>
<result column="name" jdbcType="VARCHAR" property="name"/>
<result column="age" jdbcType="INTEGER" property="age"/>
</resultMap>
<sql id="Base_Column_List">
id
, name, age
</sql>
<select id="selectAllStudentCount" resultType="long">
select count(123)
from tb_student
</select>
</mapper>
J. 启动类Application
在SpringBoot启动类上添加扫描数据持久层的注解并指定扫描包
@SpringBootApplication
@MapperScan("com.suke.springboot.mapper")//扫描数据持久层
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
K. 启动SpringBoot应用,访问测试
L. 打开Redis Desktop Mananger查看Redis中的情况
设置20秒后,缓存消失
M. 在StudentServiceImpl添加一行代码解决乱码
关系型数据库:数据安全的
redis:二进制数据安全的 数据直接序列化 存储到redis中,获取的时候,直接把序列化数据拿出来,反序列化成对象 不会产生乱码的
//设置key键,序列化,与功能无关
redisTemplate.setKeySerializer(new StringRedisSerializer());
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Autowired
RedisTemplate redisTemplate;
@Override
public Long queryAllStudentCount() {
//设置key键,序列化,与功能无关
redisTemplate.setKeySerializer(new StringRedisSerializer());
//从redis缓存中获取总人数
Long allStudentCount = (Long) redisTemplate.opsForValue().get("allStudentCount");
//判断是否为空
if (allStudentCount == null) {
//从数据库查询
allStudentCount = studentMapper.selectAllStudentCount();
//将值再存放到redis缓存中
redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);
}
return allStudentCount;
}
}
二、缓存穿透现象
1. RedisController 类
创建线程池模拟千人并发
@RestController
public class RedisController {
@Autowired
private StudentService studentService;
/*
* http://localhost:9005/005-springboot-redis/springboot/allStudentCount
* */
@GetMapping(value = "/springboot/allStudentCount")
public Object allStudentCount(HttpServletRequest request) {
/*Long allStudentCount = studentService.queryAllStudentCount();
return "学生总人数:" + allStudentCount;*/
//创建线程池
ExecutorService executorService = Executors.newFixedThreadPool(16);
//模拟千人并发
for (int i = 0; i < 1000; i++) {
executorService.submit(new Runnable() {
@Override
public void run() {
studentService.queryAllStudentCount();
}
});
}
return "学生总人数:" + 7;
}
}
2. StudentServiceImpl 类
在控制台输出”查询数据库“和“缓存命中”显示查询结果情况
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Autowired
RedisTemplate redisTemplate;
@Override
public Long queryAllStudentCount() {
//设置key键,序列化,与功能无关
redisTemplate.setKeySerializer(new StringRedisSerializer());
//从redis缓存中获取总人数
Long allStudentCount = (Long) redisTemplate.opsForValue().get("allStudentCount");
//判断是否为空
if (allStudentCount == null) {
System.out.println("----查询数据库-----");
//从数据库查询
allStudentCount = studentMapper.selectAllStudentCount();
//将值再存放到redis缓存中
redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);
} else {
System.out.println("-----缓存命中-------");
}
return allStudentCount;
}
}
3. 启动应用程序,浏览器访问测试
4. 造成的问题
Tip:多个线程都去查询数据库,这种现象就叫做缓存穿透,如果并发比较大,对数据库的压力过大,有可能造成数据库宕机。
三、缓存穿透现象-解决方法
1. 修改StudentServiceImpl中的代码
@Service
public class StudentServiceImpl implements StudentService {
@Autowired
StudentMapper studentMapper;
@Autowired
RedisTemplate redisTemplate;
@Override
public Long queryAllStudentCount() {
//设置key键,序列化,与功能无关
redisTemplate.setKeySerializer(new StringRedisSerializer());
//从redis缓存中获取总人数
Long allStudentCount = (Long) redisTemplate.opsForValue().get("allStudentCount");
//判断学生总人数是否为空
if (allStudentCount == null) {
//设置同步代码块
synchronized (this) {
//再次从redis缓存中获取学生总人数
allStudentCount = (Long) redisTemplate.opsForValue().get("allStudentCount");
//双重检测判断缓存中是否有数据
if (allStudentCount == null) {
System.out.println("----查询数据库-----");
//从数据库查询
allStudentCount = studentMapper.selectAllStudentCount();
//将值再存放到redis缓存中
redisTemplate.opsForValue().set("allStudentCount", allStudentCount, 20, TimeUnit.SECONDS);
} else {
System.out.println("-----缓存命中-------");
}
}
} else {
System.out.println("-----缓存命中-------");
}
return allStudentCount;
}
}
2. 启动应用程序,浏览器访问测试,查看控制台输出
只有第一个线程查询数据库,其它线程查询Redis缓存,这样的解决的小问题就是第一批进来的用户会有一个等待,但是这样的影响可以忽略
3. springboot集成Redis阻止缓存穿透,为什么要做双层验证
- 防止线程获取到cpu执行权限的时候,其他线程已经将数据放到Redis中了,所以再次判断
- 不能将synchronized范围扩大,因为如果Redis缓存中如果有数据,线程不应该同步,否则影响效率
- 解决缓存穿透:2次查询缓存,再一次判断