• 并发:多个线程同时操作某一个(些)资源,带来数据的不确定性、不稳定性、不安全性
  • 同步:在某一个时刻,只有一个线程访问资源    解决并发问题,性能低下(程序不能让性能过于低下)
  • 锁:唯一     对象监视器
  • 缓存穿(刺)透:缓存有(没有)数据,访问了数据库
  • 缓存雪崩:在某一个时刻,缓存中大部分 同时失效,而此时恰好有很多线程并发访问,导致数据库无法处理这么多访问而瘫痪
  • 评估
  • 多线程:不是计算一个数据 而是描述的是一种状态

一、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应用,访问测试

springboot项目模拟并发 springboot单机并发_redis

 

springboot项目模拟并发 springboot单机并发_数据库_02

L. 打开Redis Desktop Mananger查看Redis中的情况  

设置20秒后,缓存消失

springboot项目模拟并发 springboot单机并发_redis_03

springboot项目模拟并发 springboot单机并发_springboot项目模拟并发_04

M. 在StudentServiceImpl添加一行代码解决乱码

 关系型数据库:数据安全的
 redis:二进制数据安全的   数据直接序列化 存储到redis中,获取的时候,直接把序列化数据拿出来,反序列化成对象 不会产生乱码的

//设置key键,序列化,与功能无关
redisTemplate.setKeySerializer(new StringRedisSerializer());

springboot项目模拟并发 springboot单机并发_数据库_05

@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:多个线程都去查询数据库,这种现象就叫做缓存穿透,如果并发比较大,对数据库的压力过大,有可能造成数据库宕机。

springboot项目模拟并发 springboot单机并发_数据库_06

三、缓存穿透现象-解决方法

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缓存,这样的解决的小问题就是第一批进来的用户会有一个等待,但是这样的影响可以忽略

springboot项目模拟并发 springboot单机并发_数据库_07

springboot项目模拟并发 springboot单机并发_java_08

3. springboot集成Redis阻止缓存穿透,为什么要做双层验证

  • 防止线程获取到cpu执行权限的时候,其他线程已经将数据放到Redis中了,所以再次判断
  • 不能将synchronized范围扩大,因为如果Redis缓存中如果有数据,线程不应该同步,否则影响效率
  • 解决缓存穿透:2次查询缓存,再一次判断