1 什么叫做缓存穿透

缓存穿透只会发生在高并发的时候,就是当有10000个并发进行查询数据的时候,我们一般都会先去redis里面查询进行数据,但是如果redis里面没有这个数据的时候,那么这10000个并发里面就会有很大一部分并发会一下子都去mysql数据库里面进行查询了

2 解决缓存穿透

2.1 首先我模拟一下缓存穿透

比如下面的代码
Java模拟并解决Redis缓存穿透 - 图1
Pom.xml代码

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>com.example</groupId>
  6. <artifactId>springboot</artifactId>
  7. <version>0.0.1-SNAPSHOT</version>
  8. <packaging>jar</packaging>
  9. <name>springboot</name>
  10. <description>Demo project for Spring Boot</description>
  11. <parent>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-parent</artifactId>
  14. <version>2.1.1.RELEASE</version>
  15. <relativePath></relativePath> <!-- lookup parent from repository -->
  16. </parent>
  17. <properties>
  18. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  19. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  20. <java.version>1.8</java.version>
  21. </properties>
  22. <dependencies>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-web</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-devtools</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>org.springframework.boot</groupId>
  33. <artifactId>spring-boot-starter-test</artifactId>
  34. <scope>test</scope>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.mybatis.spring.boot</groupId>
  38. <artifactId>mybatis-spring-boot-starter</artifactId>
  39. <version>1.1.1</version>
  40. </dependency>
  41. <dependency>
  42. <groupId>mysql</groupId>
  43. <artifactId>mysql-connector-java</artifactId>
  44. </dependency>
  45. </dependencies>
  46. <build>
  47. <plugins>
  48. <plugin>
  49. <groupId>org.springframework.boot</groupId>
  50. <artifactId>spring-boot-maven-plugin</artifactId>
  51. </plugin>
  52. </plugins>
  53. </build>
  54. </project>

Application.properties

  1. server.port=8081
  2. #DB Configuration:
  3. spring.datasource.driverClassName=com.mysql.jdbc.Driver
  4. spring.datasource.url=jdbc:mysql://47.91.248.236:3306/hello?useUnicode=true&characterEncoding=utf8
  5. spring.datasource.username=root
  6. spring.datasource.password=root
  7. #spring集成Mybatis环境
  8. #pojo别名扫描包
  9. mybatis.type-aliases-package=com.itheima.domain
  10. #加载Mybatis映射文件
  11. mybatis.mapper-locations=classpath:mapper/*Mapper.xml

MyController代码,下面的蓝色代码是模仿10000个并发线程

  1. /**
  2. * sinture.com Inc.
  3. * Copyright (c) 2016-2018 All Rights Reserved.
  4. */
  5. package com.itheima.controller;
  6. import com.itheima.mapper.UserMapper;
  7. import com.itheima.domain.User;
  8. import com.itheima.service.UserService;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.web.bind.annotation.PathVariable;
  11. import org.springframework.web.bind.annotation.RequestMapping;
  12. import org.springframework.web.bind.annotation.ResponseBody;
  13. import org.springframework.web.bind.annotation.RestController;
  14. import java.util.HashMap;
  15. import java.util.List;
  16. import java.util.Map;
  17. /**
  18. * @author xinzhu
  19. * @version Id: MyController.java, v 0.1 2018年12月05日 下午6:29 xinzhu Exp $
  20. */
  21. @RestController
  22. public class MyController {
  23. @Autowired
  24. private UserService userService;
  25. @RequestMapping("/hello/{id}")
  26. @ResponseBody
  27. public User queryUser(@PathVariable Integer id){
  28. // 蓝色代码注释开始
  29. new Thread(){
  30. @Override
  31. public void run() {
  32. for (int x = 0; x < 10000; x++) {
  33. userService.queryUser(id);
  34. }
  35. }
  36. }.start();
  37. // 蓝色代码注释结束
  38. return userService.queryUser(id);
  39. }
  40. }

User类

  1. /**
  2. * sinture.com Inc.
  3. * Copyright (c) 2016-2018 All Rights Reserved.
  4. */
  5. package com.itheima.domain;
  6. /**
  7. * @author xinzhu
  8. * @version Id: User.java, v 0.1 2018年12月06日 下午1:40 xinzhu Exp $
  9. */
  10. public class User {
  11. // 主键
  12. private Integer id;
  13. // 用户名
  14. private String username;
  15. // 密码
  16. private String password;
  17. // 姓名
  18. private String name;
  19. public void setId(Integer id) {
  20. this.id = id;
  21. }
  22. @Override
  23. public String toString() {
  24. return "User{" +
  25. "id=" + id +
  26. ", username='" + username + '\'' +
  27. ", password='" + password + '\'' +
  28. ", name='" + name + '\'' +
  29. '}';
  30. }
  31. public Integer getId() {
  32. return id;
  33. }
  34. public String getUsername() {
  35. return username;
  36. }
  37. public void setUsername(String username) {
  38. this.username = username;
  39. }
  40. public String getPassword() {
  41. return password;
  42. }
  43. public void setPassword(String password) {
  44. this.password = password;
  45. }
  46. public String getName() {
  47. return name;
  48. }
  49. public void setName(String name) {
  50. this.name = name;
  51. }
  52. }

UserService

  1. package com.itheima.service;
  2. import com.itheima.domain.User;
  3. public interface UserService {
  4. public User queryUser(Integer id);
  5. }

UserServiceImpl,下面的蓝色代码就是模仿redis,此时要注意下面的模拟redis的map集合必须放到下面的queryUser的外面,也就是说下面的userMap变量必须是成员变量,不然的话,因为redis是被多个线程共享的,如果你放到下面的queryUser()方法里面,那么就是多个线程有多个userMap集合,下面的代码就是如果查询到数据,那么就用redis里面的,如果查询不到就用数据库里面的

  1. package com.itheima.service;
  2. import com.itheima.domain.User;
  3. import com.itheima.mapper.UserMapper;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. @Service
  9. public class UserServiceImpl implements UserService {
  10. @Autowired
  11. private UserMapper userMapper;
  12. // 蓝色代码注释开始
  13. static Map<Integer,User> userMap=new HashMap();
  14. static {
  15. User huancun_user =new User();
  16. huancun_user.setId(1);
  17. huancun_user.setName("zhangsan");
  18. huancun_user.setPassword("123");
  19. huancun_user.setName("张三");
  20. userMap.put(1,huancun_user);
  21. }
  22. // 蓝色代码注释结束
  23. public User queryUser(Integer id){
  24. User user=userMap.get(id);
  25. if(user==null){
  26. user= userMapper.queryUser(id);
  27. System.out.println("查询数据库");
  28. userMap.put(user.getId(),user);
  29. }else{
  30. System.out.println("查询缓存");
  31. }
  32. return user;
  33. };
  34. }

SpringbootApplication代码

  1. package com.itheima;
  2. import org.springframework.boot.SpringApplication;
  3. import org.springframework.boot.autoconfigure.SpringBootApplication;
  4. @SpringBootApplication
  5. public class SpringbootApplication {
  6. public static void main(String[] args) {
  7. SpringApplication.run(SpringbootApplication.class, args);
  8. }
  9. }

数据库里面的数据如下

  1. -- ----------------------------
  2. -- Table structure for `user`
  3. -- ----------------------------
  4. DROP TABLE IF EXISTS `user`;
  5. CREATE TABLE `user` (
  6. `id` int(11) NOT NULL AUTO_INCREMENT,
  7. `username` varchar(50) DEFAULT NULL,
  8. `password` varchar(50) DEFAULT NULL,
  9. `name` varchar(50) DEFAULT NULL,
  10. PRIMARY KEY (`id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
  12. -- ----------------------------
  13. -- Records of user
  14. -- ----------------------------
  15. INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '张三');
  16. INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四');

然后我们查询下面的链接,因为此时上面的模拟redis的map集合里面没有id值是2的数据,所以此时都是查询数据库,你想这一下子10000并发过去,数据库会有很大压力的,
Java模拟并解决Redis缓存穿透 - 图2
然后打印结果如下,就是打印了很多查询数据库和查询缓存,此时也就说明10000个并发里面有很多去查询了数据库,这个是要避免的,至于为什么有查询缓存的打印,因为我们把查询的数据给放到模拟的redis里面了啊,所以刚开始的一大部分线程都是查询数据库,然后剩下的都是查询模拟的redis缓存里面的数据

  1. 查询数据库
  2. 查询数据库
  3. 查询数据库
  4. 查询数据库
  5. 查询数据库
  6. 查询数据库
  7. 查询数据库
  8. 查询数据库
  9. 查询数据库
  10. 查询数据库
  11. 查询数据库
  12. 查询数据库
  13. 查询数据库
  14. 查询缓存
  15. 查询缓存
  16. 查询缓存
  17. 查询缓存
  18. 查询缓存
  19. 查询缓存
  20. 查询缓存
  21. 查询缓存
  22. 查询缓存
  23. 查询缓存

然后我们使用双重检测锁来解决上面的缓存穿透

我们怎么解决缓存穿透呢,即使10000个并发过来,然后这10000个并发需要的数据在redis里面都没有,那么我们应该第一个线程查询数据里面的数据,然后把这个数据给放到redis里面,然后剩下的9999个线程都到redis里面查询,这样就解决了缓存穿透,所以我们可以把上面的代码变成下面这样
比如下面的代码
Java模拟并解决Redis缓存穿透 - 图3
Pom.xml代码

  1. <?xml version="1.0" encoding="UTF-8"?>
  2. <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  3. xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  4. <modelVersion>4.0.0</modelVersion>
  5. <groupId>com.example</groupId>
  6. <artifactId>springboot</artifactId>
  7. <version>0.0.1-SNAPSHOT</version>
  8. <packaging>jar</packaging>
  9. <name>springboot</name>
  10. <description>Demo project for Spring Boot</description>
  11. <parent>
  12. <groupId>org.springframework.boot</groupId>
  13. <artifactId>spring-boot-starter-parent</artifactId>
  14. <version>2.1.1.RELEASE</version>
  15. <relativePath></relativePath> <!-- lookup parent from repository -->
  16. </parent>
  17. <properties>
  18. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  19. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  20. <java.version>1.8</java.version>
  21. </properties>
  22. <dependencies>
  23. <dependency>
  24. <groupId>org.springframework.boot</groupId>
  25. <artifactId>spring-boot-starter-web</artifactId>
  26. </dependency>
  27. <dependency>
  28. <groupId>org.springframework.boot</groupId>
  29. <artifactId>spring-boot-devtools</artifactId>
  30. </dependency>
  31. <dependency>
  32. <groupId>org.springframework.boot</groupId>
  33. <artifactId>spring-boot-starter-test</artifactId>
  34. <scope>test</scope>
  35. </dependency>
  36. <dependency>
  37. <groupId>org.mybatis.spring.boot</groupId>
  38. <artifactId>mybatis-spring-boot-starter</artifactId>
  39. <version>1.1.1</version>
  40. </dependency>
  41. <dependency>
  42. <groupId>mysql</groupId>
  43. <artifactId>mysql-connector-java</artifactId>
  44. </dependency>
  45. </dependencies>
  46. <build>
  47. <plugins>
  48. <plugin>
  49. <groupId>org.springframework.boot</groupId>
  50. <artifactId>spring-boot-maven-plugin</artifactId>
  51. </plugin>
  52. </plugins>
  53. </build>
  54. </project>

Application.properties

  1. server.port=8081
  2. #DB Configuration:
  3. spring.datasource.driverClassName=com.mysql.jdbc.Driver
  4. spring.datasource.url=jdbc:mysql://47.91.248.236:3306/hello?useUnicode=true&characterEncoding=utf8
  5. spring.datasource.username=root
  6. spring.datasource.password=root
  7. #spring集成Mybatis环境
  8. #pojo别名扫描包
  9. mybatis.type-aliases-package=com.itheima.domain
  10. #加载Mybatis映射文件
  11. mybatis.mapper-locations=classpath:mapper/*Mapper.xml

MyController代码,下面的蓝色代码是模仿10000个并发线程

  1. /**
  2. * sinture.com Inc.
  3. * Copyright (c) 2016-2018 All Rights Reserved.
  4. */
  5. package com.itheima.controller;
  6. import com.itheima.mapper.UserMapper;
  7. import com.itheima.domain.User;
  8. import com.itheima.service.UserService;
  9. import org.springframework.beans.factory.annotation.Autowired;
  10. import org.springframework.web.bind.annotation.PathVariable;
  11. import org.springframework.web.bind.annotation.RequestMapping;
  12. import org.springframework.web.bind.annotation.ResponseBody;
  13. import org.springframework.web.bind.annotation.RestController;
  14. import java.util.HashMap;
  15. import java.util.List;
  16. import java.util.Map;
  17. /**
  18. * @author xinzhu
  19. * @version Id: MyController.java, v 0.1 2018年12月05日 下午6:29 xinzhu Exp $
  20. */
  21. @RestController
  22. public class MyController {
  23. @Autowired
  24. private UserService userService;
  25. @RequestMapping("/hello/{id}")
  26. @ResponseBody
  27. public User queryUser(@PathVariable Integer id){
  28. // 蓝色代码注释开始
  29. new Thread(){
  30. @Override
  31. public void run() {
  32. for (int x = 0; x < 10000; x++) {
  33. userService.queryUser(id);
  34. }
  35. }
  36. }.start();
  37. // 蓝色代码注释结束
  38. return userService.queryUser(id);
  39. }
  40. }

User类

  1. /**
  2. * sinture.com Inc.
  3. * Copyright (c) 2016-2018 All Rights Reserved.
  4. */
  5. package com.itheima.domain;
  6. /**
  7. * @author xinzhu
  8. * @version Id: User.java, v 0.1 2018年12月06日 下午1:40 xinzhu Exp $
  9. */
  10. public class User {
  11. // 主键
  12. private Integer id;
  13. // 用户名
  14. private String username;
  15. // 密码
  16. private String password;
  17. // 姓名
  18. private String name;
  19. public void setId(Integer id) {
  20. this.id = id;
  21. }
  22. @Override
  23. public String toString() {
  24. return "User{" +
  25. "id=" + id +
  26. ", username='" + username + '\'' +
  27. ", password='" + password + '\'' +
  28. ", name='" + name + '\'' +
  29. '}';
  30. }
  31. public Integer getId() {
  32. return id;
  33. }
  34. public String getUsername() {
  35. return username;
  36. }
  37. public void setUsername(String username) {
  38. this.username = username;
  39. }
  40. public String getPassword() {
  41. return password;
  42. }
  43. public void setPassword(String password) {
  44. this.password = password;
  45. }
  46. public String getName() {
  47. return name;
  48. }
  49. public void setName(String name) {
  50. this.name = name;
  51. }
  52. }

UserService

  1. package com.itheima.service;
  2. import com.itheima.domain.User;
  3. public interface UserService {
  4. public User queryUser(Integer id);
  5. }

UserServiceImpl,下面的蓝色代码就是模仿redis,此时要注意下面的模拟redis的map集合必须放到下面的queryUser的外面,也就是说下面的userMap变量必须是成员变量,不然的话,因为redis是被多个线程共享的,如果你放到下面的queryUser()方法里面,那么就是多个线程有多个userMap集合,下面的代码就是如果查询到数据,那么就用redis里面的,如果查询不到就用数据库里面的
然后下面的红色代码就是解决上面的缓存穿透问题,使用锁来解决缓存穿透问题,而且叫做双重检测锁,为什么叫做双重检测锁呢,因为有两个if语句,第一个if语句就是为了减少走红色代码里面的同步代码块,因为如果换成里面存在想要的数据,那么就不需要走下面的红色代码里面的同步代码块了,所以有两个if语句,至于为什么要有下面的 user= userMap.get(id);,是因为第一次线程查询把数据放到模仿的redis缓存里面之后,剩下的线程当走到下面的同步代码块的时候,需要在查询一下缓存里面的数据就会发现刚刚第一个线程放到redis里面的数据了,所以才会有下面的红色代码里面的 user= userMap.get(id);

  1. package com.itheima.service;
  2. import com.itheima.domain.User;
  3. import com.itheima.mapper.UserMapper;
  4. import org.springframework.beans.factory.annotation.Autowired;
  5. import org.springframework.stereotype.Service;
  6. import java.util.HashMap;
  7. import java.util.Map;
  8. @Service
  9. public class UserServiceImpl implements UserService {
  10. @Autowired
  11. private UserMapper userMapper;
  12. // 蓝色代码注释开始
  13. static Map<Integer,User> userMap=new HashMap();
  14. static {
  15. User huancun_user =new User();
  16. huancun_user.setId(1);
  17. huancun_user.setName("zhangsan");
  18. huancun_user.setPassword("123");
  19. huancun_user.setName("张三");
  20. userMap.put(1,huancun_user);
  21. }
  22. // 蓝色代码注释结束
  23. public User queryUser(Integer id){
  24. User user=userMap.get(id);
  25. // 红色代码注释开始
  26. if(user==null){
  27. synchronized (this) {
  28. user= userMap.get(id);
  29. if (null == user) {
  30. user= userMapper.queryUser(id);
  31. System.out.println("查询数据库");
  32. userMap.put(user.getId(),user);
  33. }else{
  34. System.out.println("查询缓存");
  35. }
  36. }
  37. }else{
  38. System.out.println("查询缓存");
  39. }
  40. //红色代码注释结束
  41. return user;
  42. };
  43. }

数据库里面的数据如下

  1. -- ----------------------------
  2. -- Table structure for `user`
  3. -- ----------------------------
  4. DROP TABLE IF EXISTS `user`;
  5. CREATE TABLE `user` (
  6. `id` int(11) NOT NULL AUTO_INCREMENT,
  7. `username` varchar(50) DEFAULT NULL,
  8. `password` varchar(50) DEFAULT NULL,
  9. `name` varchar(50) DEFAULT NULL,
  10. PRIMARY KEY (`id`)
  11. ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;
  12. -- ----------------------------
  13. -- Records of user
  14. -- ----------------------------
  15. INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '张三');
  16. INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四');

然后我们查询下面的链接,因为此时上面的模拟redis的map集合里面没有id值是2的数据,所以此时都是查询数据库,你想这一下子10000并发过去,数据库会有很大压力的,
Java模拟并解决Redis缓存穿透 - 图4
然后打印结果如下,就是就只有第一个打印了查询数据库,然后剩下的都是查询缓存了,这就是解决缓存穿透

  1. 查询数据库
  2. 查询缓存
  3. 查询缓存
  4. 查询缓存
  5. 查询缓存
  6. 查询缓存
  7. 查询缓存
  8. 查询缓存
  9. 查询缓存
  10. 查询缓存
  11. 查询缓存
  12. 查询缓存
  13. 查询缓存
  14. 查询缓存
  15. 查询缓存
  16. 查询缓存
  17. 查询缓存
  18. 查询缓存
  19. 查询缓存
  20. 查询缓存
  21. 查询缓存
  22. 查询缓存

原文链接:https://www.cnblogs.com/mqk100/p/11319758.html