Java模擬並解決快取穿透

  • 2019 年 10 月 3 日
  • 筆記

什麼叫做快取穿透

快取穿透只會發生在高並發的時候,就是當有10000個並發進行查詢數據的時候,我們一般都會先去redis裡面查詢進行數據,但是如果redis裡面沒有這個數據的時候,那麼這10000個並發裡面就會有很大一部分並發會一下子都去mysql資料庫裡面進行查詢了

解決快取穿透

首先我模擬一下快取穿透

比如下面的程式碼
在這裡插入圖片描述
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 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>        <groupId>com.example</groupId>      <artifactId>springboot</artifactId>      <version>0.0.1-SNAPSHOT</version>      <packaging>jar</packaging>        <name>springboot</name>      <description>Demo project for Spring Boot</description>        <parent>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-parent</artifactId>          <version>2.1.1.RELEASE</version>          <relativePath></relativePath> <!-- lookup parent from repository -->      </parent>        <properties>          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>          <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>          <java.version>1.8</java.version>      </properties>        <dependencies>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-web</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-devtools</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-test</artifactId>              <scope>test</scope>          </dependency>            <dependency>              <groupId>org.mybatis.spring.boot</groupId>              <artifactId>mybatis-spring-boot-starter</artifactId>              <version>1.1.1</version>          </dependency>          <dependency>              <groupId>mysql</groupId>              <artifactId>mysql-connector-java</artifactId>          </dependency>      </dependencies>        <build>          <plugins>              <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>              </plugin>          </plugins>      </build>      </project>

Application.properties

server.port=8081  #DB Configuration:  spring.datasource.driverClassName=com.mysql.jdbc.Driver  spring.datasource.url=jdbc:mysql://47.91.248.236:3306/hello?useUnicode=true&characterEncoding=utf8  spring.datasource.username=root  spring.datasource.password=root    #spring集成Mybatis環境  #pojo別名掃描包  mybatis.type-aliases-package=com.itheima.domain  #載入Mybatis映射文件  mybatis.mapper-locations=classpath:mapper/*Mapper.xml

MyController程式碼,下面的藍色程式碼是模仿10000個並發執行緒

/**   * sinture.com Inc.   * Copyright (c) 2016-2018 All Rights Reserved.   */  package com.itheima.controller;    import com.itheima.mapper.UserMapper;  import com.itheima.domain.User;  import com.itheima.service.UserService;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.PathVariable;  import org.springframework.web.bind.annotation.RequestMapping;  import org.springframework.web.bind.annotation.ResponseBody;  import org.springframework.web.bind.annotation.RestController;    import java.util.HashMap;  import java.util.List;  import java.util.Map;    /**   * @author xinzhu   * @version Id: MyController.java, v 0.1 2018年12月05日 下午6:29 xinzhu Exp $   */  @RestController  public class MyController {      @Autowired      private UserService userService;        @RequestMapping("/hello/{id}")      @ResponseBody      public User queryUser(@PathVariable Integer id){         // 藍色程式碼注釋開始         new Thread(){              @Override              public void run() {                  for (int x = 0; x < 10000; x++) {                      userService.queryUser(id);                  }              }            }.start();          // 藍色程式碼注釋結束            return userService.queryUser(id);      }  }

User類

/**   * sinture.com Inc.   * Copyright (c) 2016-2018 All Rights Reserved.   */  package com.itheima.domain;    /**   * @author xinzhu   * @version Id: User.java, v 0.1 2018年12月06日 下午1:40 xinzhu Exp $   */  public class User {      // 主鍵      private Integer id;      // 用戶名      private String username;      // 密碼      private String password;      // 姓名      private String name;        public void setId(Integer id) {          this.id = id;      }        @Override      public String toString() {          return "User{" +                  "id=" + id +                  ", username='" + username + ''' +                  ", password='" + password + ''' +                  ", name='" + name + ''' +                  '}';      }        public Integer getId() {          return id;      }          public String getUsername() {          return username;      }          public void setUsername(String username) {          this.username = username;      }        public String getPassword() {          return password;      }          public void setPassword(String password) {          this.password = password;      }        public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }  }

UserService

package com.itheima.service;    import com.itheima.domain.User;    public interface UserService {      public User queryUser(Integer id);  }  

UserServiceImpl,下面的藍色程式碼就是模仿redis,此時要注意下面的模擬redis的map集合必須放到下面的queryUser的外面,也就是說下面的userMap變數必須是成員變數,不然的話,因為redis是被多個執行緒共享的,如果你放到下面的queryUser()方法裡面,那麼就是多個執行緒有多個userMap集合,下面的程式碼就是如果查詢到數據,那麼就用redis裡面的,如果查詢不到就用資料庫裡面的

package com.itheima.service;    import com.itheima.domain.User;  import com.itheima.mapper.UserMapper;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Service;    import java.util.HashMap;  import java.util.Map;    @Service  public class UserServiceImpl implements UserService {      @Autowired      private UserMapper userMapper;      // 藍色程式碼注釋開始      static Map<Integer,User> userMap=new HashMap();      static {          User huancun_user =new User();          huancun_user.setId(1);          huancun_user.setName("zhangsan");          huancun_user.setPassword("123");          huancun_user.setName("張三");          userMap.put(1,huancun_user);      }      // 藍色程式碼注釋結束      public User queryUser(Integer id){          User user=userMap.get(id);          if(user==null){              user= userMapper.queryUser(id);              System.out.println("查詢資料庫");              userMap.put(user.getId(),user);          }else{              System.out.println("查詢快取");          }          return user;      };  }  

SpringbootApplication程式碼

package com.itheima;    import org.springframework.boot.SpringApplication;  import org.springframework.boot.autoconfigure.SpringBootApplication;    @SpringBootApplication  public class SpringbootApplication {        public static void main(String[] args) {          SpringApplication.run(SpringbootApplication.class, args);      }    }

資料庫裡面的數據如下

-- ----------------------------  -- Table structure for `user`  -- ----------------------------  DROP TABLE IF EXISTS `user`;  CREATE TABLE `user` (    `id` int(11) NOT NULL AUTO_INCREMENT,    `username` varchar(50) DEFAULT NULL,    `password` varchar(50) DEFAULT NULL,    `name` varchar(50) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;    -- ----------------------------  -- Records of user  -- ----------------------------  INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '張三');  INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四');  

然後我們查詢下面的鏈接,因為此時上面的模擬redis的map集合裡面沒有id值是2的數據,所以此時都是查詢資料庫,你想這一下子10000並發過去,資料庫會有很大壓力的,

在這裡插入圖片描述

然後列印結果如下,就是列印了很多查詢資料庫和查詢快取,此時也就說明10000個並發裡面有很多去查詢了資料庫,這個是要避免的,至於為什麼有查詢快取的列印,因為我們把查詢的數據給放到模擬的redis裡面了啊,所以剛開始的一大部分執行緒都是查詢資料庫,然後剩下的都是查詢模擬的redis快取裡面的數據

查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢資料庫  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  

然後我們使用雙重檢測鎖來解決上面的快取穿透

我們怎麼解決快取穿透呢,即使10000個並發過來,然後這10000個並發需要的數據在redis裡面都沒有,那麼我們應該第一個執行緒查詢數據裡面的數據,然後把這個數據給放到redis裡面,然後剩下的9999個執行緒都到redis裡面查詢,這樣就解決了快取穿透,所以我們可以把上面的程式碼變成下面這樣

比如下面的程式碼
在這裡插入圖片描述

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 http://maven.apache.org/xsd/maven-4.0.0.xsd">      <modelVersion>4.0.0</modelVersion>        <groupId>com.example</groupId>      <artifactId>springboot</artifactId>      <version>0.0.1-SNAPSHOT</version>      <packaging>jar</packaging>        <name>springboot</name>      <description>Demo project for Spring Boot</description>        <parent>          <groupId>org.springframework.boot</groupId>          <artifactId>spring-boot-starter-parent</artifactId>          <version>2.1.1.RELEASE</version>          <relativePath></relativePath> <!-- lookup parent from repository -->      </parent>        <properties>          <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>          <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>          <java.version>1.8</java.version>      </properties>        <dependencies>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-web</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-devtools</artifactId>          </dependency>          <dependency>              <groupId>org.springframework.boot</groupId>              <artifactId>spring-boot-starter-test</artifactId>              <scope>test</scope>          </dependency>            <dependency>              <groupId>org.mybatis.spring.boot</groupId>              <artifactId>mybatis-spring-boot-starter</artifactId>              <version>1.1.1</version>          </dependency>          <dependency>              <groupId>mysql</groupId>              <artifactId>mysql-connector-java</artifactId>          </dependency>      </dependencies>        <build>          <plugins>              <plugin>                  <groupId>org.springframework.boot</groupId>                  <artifactId>spring-boot-maven-plugin</artifactId>              </plugin>          </plugins>      </build>      </project>

Application.properties

server.port=8081  #DB Configuration:  spring.datasource.driverClassName=com.mysql.jdbc.Driver  spring.datasource.url=jdbc:mysql://47.91.248.236:3306/hello?useUnicode=true&characterEncoding=utf8  spring.datasource.username=root  spring.datasource.password=root    #spring集成Mybatis環境  #pojo別名掃描包  mybatis.type-aliases-package=com.itheima.domain  #載入Mybatis映射文件  mybatis.mapper-locations=classpath:mapper/*Mapper.xml

MyController程式碼,下面的藍色程式碼是模仿10000個並發執行緒

/**   * sinture.com Inc.   * Copyright (c) 2016-2018 All Rights Reserved.   */  package com.itheima.controller;    import com.itheima.mapper.UserMapper;  import com.itheima.domain.User;  import com.itheima.service.UserService;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.web.bind.annotation.PathVariable;  import org.springframework.web.bind.annotation.RequestMapping;  import org.springframework.web.bind.annotation.ResponseBody;  import org.springframework.web.bind.annotation.RestController;    import java.util.HashMap;  import java.util.List;  import java.util.Map;    /**   * @author xinzhu   * @version Id: MyController.java, v 0.1 2018年12月05日 下午6:29 xinzhu Exp $   */  @RestController  public class MyController {      @Autowired      private UserService userService;        @RequestMapping("/hello/{id}")      @ResponseBody      public User queryUser(@PathVariable Integer id){         // 藍色程式碼注釋開始         new Thread(){              @Override              public void run() {                  for (int x = 0; x < 10000; x++) {                      userService.queryUser(id);                  }              }            }.start();          // 藍色程式碼注釋結束            return userService.queryUser(id);      }  }

User類

/**   * sinture.com Inc.   * Copyright (c) 2016-2018 All Rights Reserved.   */  package com.itheima.domain;    /**   * @author xinzhu   * @version Id: User.java, v 0.1 2018年12月06日 下午1:40 xinzhu Exp $   */  public class User {      // 主鍵      private Integer id;      // 用戶名      private String username;      // 密碼      private String password;      // 姓名      private String name;        public void setId(Integer id) {          this.id = id;      }        @Override      public String toString() {          return "User{" +                  "id=" + id +                  ", username='" + username + ''' +                  ", password='" + password + ''' +                  ", name='" + name + ''' +                  '}';      }        public Integer getId() {          return id;      }          public String getUsername() {          return username;      }          public void setUsername(String username) {          this.username = username;      }        public String getPassword() {          return password;      }          public void setPassword(String password) {          this.password = password;      }        public String getName() {          return name;      }        public void setName(String name) {          this.name = name;      }  }  

UserService

package com.itheima.service;    import com.itheima.domain.User;    public interface UserService {      public User queryUser(Integer id);  }

UserServiceImpl,下面的藍色程式碼就是模仿redis,此時要注意下面的模擬redis的map集合必須放到下面的queryUser的外面,也就是說下面的userMap變數必須是成員變數,不然的話,因為redis是被多個執行緒共享的,如果你放到下面的queryUser()方法裡面,那麼就是多個執行緒有多個userMap集合,下面的程式碼就是如果查詢到數據,那麼就用redis裡面的,如果查詢不到就用資料庫裡面的

然後下面的紅色程式碼就是解決上面的快取穿透問題,使用鎖來解決快取穿透問題,而且叫做雙重檢測鎖,為什麼叫做雙重檢測鎖呢,因為有兩個if語句,第一個if語句就是為了減少走紅色程式碼裡面的同步程式碼塊,因為如果換成裡面存在想要的數據,那麼就不需要走下面的紅色程式碼裡面的同步程式碼塊了,所以有兩個if語句,至於為什麼要有下面的 user= userMap.get(id);,是因為第一次執行緒查詢把數據放到模仿的redis快取裡面之後,剩下的執行緒當走到下面的同步程式碼塊的時候,需要在查詢一下快取裡面的數據就會發現剛剛第一個執行緒放到redis裡面的數據了,所以才會有下面的紅色程式碼裡面的 user= userMap.get(id);

package com.itheima.service;    import com.itheima.domain.User;  import com.itheima.mapper.UserMapper;  import org.springframework.beans.factory.annotation.Autowired;  import org.springframework.stereotype.Service;    import java.util.HashMap;  import java.util.Map;    @Service  public class UserServiceImpl implements UserService {      @Autowired      private UserMapper userMapper;      // 藍色程式碼注釋開始      static Map<Integer,User> userMap=new HashMap();      static {          User huancun_user =new User();          huancun_user.setId(1);          huancun_user.setName("zhangsan");          huancun_user.setPassword("123");          huancun_user.setName("張三");          userMap.put(1,huancun_user);      }      // 藍色程式碼注釋結束      public User queryUser(Integer id){          User user=userMap.get(id);          // 紅色程式碼注釋開始          if(user==null){              synchronized (this) {                  user= userMap.get(id);                  if (null == user) {                      user= userMapper.queryUser(id);                      System.out.println("查詢資料庫");                      userMap.put(user.getId(),user);                  }else{                      System.out.println("查詢快取");                  }              }          }else{              System.out.println("查詢快取");          }          //紅色程式碼注釋結束          return user;      };  }

資料庫裡面的數據如下

-- ----------------------------  -- Table structure for `user`  -- ----------------------------  DROP TABLE IF EXISTS `user`;  CREATE TABLE `user` (    `id` int(11) NOT NULL AUTO_INCREMENT,    `username` varchar(50) DEFAULT NULL,    `password` varchar(50) DEFAULT NULL,    `name` varchar(50) DEFAULT NULL,    PRIMARY KEY (`id`)  ) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;    -- ----------------------------  -- Records of user  -- ----------------------------  INSERT INTO `user` VALUES ('1', 'zhangsan', '123', '張三');  INSERT INTO `user` VALUES ('2', 'lisi', '123', '李四');

然後我們查詢下面的鏈接,因為此時上面的模擬redis的map集合裡面沒有id值是2的數據,所以此時都是查詢資料庫,你想這一下子10000並發過去,資料庫會有很大壓力的,
在這裡插入圖片描述
然後列印結果如下,就是就只有第一個列印了查詢資料庫,然後剩下的都是查詢快取了,這就是解決快取穿透

查詢資料庫  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取  查詢快取

能看到這裡的同學,就幫忙點個推薦吧,Thanks♪(・ω・)ノ