2019-08-13
一直想在springboot上整合帶快取的redis,終於成功了。網上有1000種寫法,想找到一篇合適的還真不容易?。走下流程,加深下印象。
環境:
springboot版本:2.1.7
orm框架:mybatis
實現?:
在serviceImpl層方法上加註解@Cacheable和@CachEvict。
@Cacheable把資料放進redis,下一次需要資料直接在快取中取;@CacheEvict使redis中的快取失效。
關於註解的更多詳細可以參考https://www.cnblogs.com/fashflying/p/6908028.html寫得很詳細。
準備工作?:
pom.xml?:
1 <parent> 2 <groupId>org.springframework.boot</groupId> 3 <artifactId>spring-boot-starter-parent</artifactId> 4 <version>2.1.7.RELEASE</version> 5 <relativePath/> 6 </parent> 7 8 <dependencies> 9 <dependency> 10 <groupId>org.springframework.boot</groupId> 11 <artifactId>spring-boot-starter-web</artifactId> 12 </dependency> 13 <dependency> 14 <groupId>org.springframework.boot</groupId> 15 <artifactId>spring-boot-starter-test</artifactId> 16 <scope>test</scope> 17 </dependency> 18 <!-- servlet依賴. --> 19 <dependency> 20 <groupId>javax.servlet</groupId> 21 <artifactId>javax.servlet-api</artifactId> 22 </dependency> 23 <dependency> 24 <groupId>javax.servlet</groupId> 25 <artifactId>jstl</artifactId> 26 </dependency> 27 28 <!--tomcat--> 29 <dependency> 30 <groupId>org.apache.tomcat.embed</groupId> 31 <artifactId>tomcat-embed-jasper</artifactId> 32 </dependency> 33 34 <!--熱部署--> 35 <dependency> 36 <groupId>org.springframework.boot</groupId> 37 <artifactId>spring-boot-devtools</artifactId> 38 <optional>true</optional> 39 </dependency> 40 41 <!-- mysql --> 42 <dependency> 43 <groupId>mysql</groupId> 44 <artifactId>mysql-connector-java</artifactId> 45 <version>5.1.21</version> 46 </dependency> 47 <!--mybatis--> 48 <dependency> 49 <groupId>org.mybatis.spring.boot</groupId> 50 <artifactId>mybatis-spring-boot-starter</artifactId> 51 <version>1.1.1</version> 52 </dependency> 53 <!--mybatis逆向工程--> 54 <dependency> 55 <groupId>org.mybatis.generator</groupId> 56 <artifactId>mybatis-generator-core</artifactId> 57 <version>1.3.7</version> 58 </dependency> 59 60 <!--json格式--> 61 <dependency> 62 <groupId>com.fasterxml.jackson.datatype</groupId> 63 <artifactId>jackson-datatype-jsr310</artifactId> 64 </dependency> 65 66 <!-- redis --> 67 <dependency> 68 <groupId>org.springframework.boot</groupId> 69 <artifactId>spring-boot-starter-data-redis</artifactId> 70 </dependency> 71 72 <!--thymeleaf--> 73 <!--<dependency>--> 74 <!--<groupId>org.springframework.boot</groupId>--> 75 <!--<artifactId>spring-boot-starter-thymeleaf</artifactId>--> 76 <!--</dependency>--> 77 </dependencies>
application.properties檔案?:
#mvc spring.mvc.view.prefix=/WEB-INF/jsp/ spring.mvc.view.suffix=.jsp #mysql spring.datasource.url=jdbc:mysql://localhost:3306/common?characterEncoding=utf-8 spring.datasource.username=xfk spring.datasource.password=123456 spring.datasource.driver-class-name=com.mysql.jdbc.Driver #mybatis mybatis.mapper-locations=classpath:/mapper/*.xml mybatis.type-aliases-package=com.xfk.sb.pojo #redis spring.redis.database=0 spring.redis.host=127.0.0.1 spring.redis.port=6379 spring.redis.password= spring.redis.timeout=5000
書寫?:
一,允許使用快取:
?在springboot的主啟動類上新增註解@EnableCaching
SbApplication.java
1 package com.xfk.sb; 2 3 import org.springframework.boot.SpringApplication; 4 import org.springframework.boot.autoconfigure.SpringBootApplication; 5 import org.springframework.cache.annotation.EnableCaching; 6 7 @SpringBootApplication 8 @EnableCaching 9 public class SbApplication { 10 public static void main(String[] args) { 11 SpringApplication.run(SbApplication.class, args); 12 } 13 }
二,redis配置類:
?這裡一個重要的點就是Serializer。RedisCache預設使用的是
JdkSerializationRedisSerializer,我們要實現json格式的資料在redis上的儲存。利用
Jackson2JsonRedisSerializer
或GenericJackson2JsonRedisSerializer,其優點是儲存的長度小。在這裡我們用
。成功之後可以換GenericJackson2JsonRedisSerializer
試試,看一看儲存的資料有什麼不同。Jackson2JsonRedisSerializer
?cacheManager方法是用作註解@Cacheable和@CacheEvict執行service實現層方法快取資料的,另外就是定義一個redisTemplate,哪個controller需要就在哪個controller中注入,靈活使用。
RedisConfig.java
1 package com.xfk.sb.config; 2 3 import org.springframework.cache.annotation.CachingConfigurerSupport; 4 import org.springframework.cache.annotation.EnableCaching; 5 import org.springframework.context.annotation.Bean; 6 import org.springframework.context.annotation.Configuration; 7 import org.springframework.data.redis.cache.RedisCacheConfiguration; 8 import org.springframework.data.redis.cache.RedisCacheManager; 9 import org.springframework.data.redis.connection.RedisConnectionFactory; 10 import org.springframework.data.redis.core.RedisTemplate; 11 import org.springframework.data.redis.serializer.*; 12 import java.time.Duration; 13 14 @Configuration 15 @EnableCaching 16 public class RedisConfig extends CachingConfigurerSupport { 17 // 過期時間 18 private Duration timeToLive = Duration.ofHours(12); 19 20 @Bean 21 public RedisCacheManager cacheManager(RedisConnectionFactory connectionFactory) { 22 RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() 23 .entryTtl(this.timeToLive) 24 .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(keySerializer())) 25 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(valueSerializer())) 26 .disableCachingNullValues(); 27 28 return RedisCacheManager.builder(connectionFactory) 29 .cacheDefaults(config) 30 .transactionAware() 31 .build(); 32 } 33 34 @Bean(name = "redisTemplate") 35 public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) { 36 RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); 37 redisTemplate.setConnectionFactory(redisConnectionFactory); 38 redisTemplate.setKeySerializer(keySerializer()); 39 redisTemplate.setHashKeySerializer(keySerializer()); 40 redisTemplate.setValueSerializer(valueSerializer()); 41 redisTemplate.setHashValueSerializer(valueSerializer()); 42 return redisTemplate; 43 } 44 45 private RedisSerializer<String> keySerializer() { 46 return new StringRedisSerializer(); 47 } 48 49 private RedisSerializer<Object> valueSerializer() { 50 return new GenericJackson2JsonRedisSerializer(); 51 } 52 }
三,增刪改查Demo:
簡單的pojo類,Student.java
1 package com.xfk.sb.pojo; 2 3 public class Student { 4 private int id; 5 private String name; 6 private int age; 7 8 public Student() { 9 } 10 11 public int getId() { 12 return id; 13 } 14 15 public void setId(int id) { 16 this.id = id; 17 } 18 19 public String getName() { 20 return name; 21 } 22 23 public void setName(String name) { 24 this.name = name; 25 } 26 27 public int getAge() { 28 return age; 29 } 30 31 public void setAge(int age) { 32 this.age = age; 33 } 34 35 @Override 36 public String toString() { 37 return "Student{" + 38 "id=" + id + 39 ", name='" + name + '\'' + 40 ", age=" + age + 41 '}'; 42 } 43 }
?我使用的是mybatis的註解方式,簡單的幾個增刪改查方法。
xml檔案,studentMapper.xml
1 <?xml version="1.0" encoding="UTF-8"?> 2 <!DOCTYPE mapper 3 PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" 4 "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> 5 6 <mapper namespace="com.xfk.sb.mapper.StudentMapper"> 7 <select id="selectStudent" resultType="com.xfk.sb.pojo.Student"> 8 select * from student 9 </select> 10 <delete id="deleteStudent" parameterType="Integer"> 11 delete from student where id = #{id} 12 </delete> 13 <insert id="createStudent" parameterType="com.xfk.sb.pojo.Student" keyColumn="id" keyProperty="student.id" useGeneratedKeys="true"> 14 insert into student(id, name, age) values(null, #{student.name}, #{student.age}) 15 </insert> 16 <update id="updateStudent" parameterType="com.xfk.sb.pojo.Student"> 17 update student set name=#{student.name}, age=#{student.age} where id = #{student.id} 18 </update> 19 <select id="selectStudentByPrimaryKey" parameterType="Integer" resultType="com.xfk.sb.pojo.Student"> 20 select * from student where id = #{id} limit 1 21 </select> 22 </mapper>
mapper介面,StudentMapper.java
1 package com.xfk.sb.mapper; 2 3 import com.xfk.sb.pojo.Student; 4 import org.apache.ibatis.annotations.Mapper; 5 import org.apache.ibatis.annotations.Param; 6 import org.springframework.stereotype.Component; 7 import java.util.List; 8 9 @Mapper 10 @Component 11 public interface StudentMapper { 12 List<Student> selectStudent(); 13 int deleteStudent(@Param("id")int id); 14 int createStudent(@Param("student")Student student); 15 int updateStudent(@Param("student")Student student); 16 Student selectStudentByPrimaryKey(@Param("id")int id); 17 }
service層介面,StudentService.java
1 package com.xfk.sb.service; 2 3 import com.xfk.sb.pojo.Student; 4 import java.util.List; 5 6 public interface StudentService { 7 List<Student> selectStudent(); 8 int deleteStudent(int id); 9 int createStudent(Student student); 10 int updateStudent(Student student); 11 Student selectStudentByPrimaryKey(int id); 12 }
service實現層,StudentServiceImpl.java
1 package com.xfk.sb.service.implement; 2 3 import com.xfk.sb.mapper.StudentMapper; 4 import com.xfk.sb.pojo.Student; 5 import com.xfk.sb.service.StudentService; 6 import org.springframework.beans.factory.annotation.Autowired; 7 import org.springframework.cache.annotation.CacheConfig; 8 import org.springframework.cache.annotation.CacheEvict; 9 import org.springframework.cache.annotation.Cacheable; 10 import org.springframework.cache.annotation.Caching; 11 import org.springframework.stereotype.Service; 12 import java.util.List; 13 14 @Service 15 @CacheConfig(cacheNames="students") 16 public class StudentServiceImpl implements StudentService { 17 private final StudentMapper studentMapper; 18 19 @Autowired 20 public StudentServiceImpl(StudentMapper studentMapper) { 21 this.studentMapper = studentMapper; 22 } 23 24 @Cacheable(key="'students'") 25 @Override 26 public List<Student> selectStudent() { 27 System.out.println("從資料庫中取selectStudent"); 28 return studentMapper.selectStudent(); 29 } 30 31 @Override 32 @Caching(evict={ 33 @CacheEvict(key="'singleStudent'+#id"), 34 @CacheEvict(key="'students'"), 35 }) 36 public int deleteStudent(int id) { 37 System.out.println("從資料庫中刪除deleteStudent"); 38 return studentMapper.deleteStudent(id); 39 } 40 41 @Override 42 @Caching(evict={ 43 @CacheEvict(key="'singleStudent'+#student.id"), 44 @CacheEvict(key="'students'"), 45 }) 46 public int createStudent(Student student) { 47 System.out.println("從資料庫中建立createStudent"); 48 return studentMapper.createStudent(student); 49 } 50 51 @Caching(evict={ 52 @CacheEvict(key="'singleStudent'+#student.id"), 53 @CacheEvict(key="'students'"), 54 }) 55 @Override 56 public int updateStudent(Student student) { 57 System.out.println("從資料庫中更新updateStudent"); 58 return studentMapper.updateStudent(student); 59 } 60 61 @Cacheable(key="'singleStudent'+#p0") 62 @Override 63 public Student selectStudentByPrimaryKey(int id) { 64 System.out.println("從資料庫中取一個selectStudentByPrimaryKey"); 65 return studentMapper.selectStudentByPrimaryKey(id); 66 } 67 }
?使用@CacheConfig註解,相當於在redis資料庫下建一個資料夾,以cacheNames作為資料夾的名字,統一管理這個實現層快取的資料。正如在Redis Desktop Manager下看到的目錄結構,db0下有一個students資料夾。
?使用@Cacheable註解使快取生效,以實現層的selectStudentByPrimaryKey()方法為例,從資料庫中根據id查詢一個Student物件。
使用@Cacheable(key="'singleStudent'+#p0"),#p0就是形參parameter0,多個引數就是#p1,#p2,,,也可以寫成#id,注意singleStudent字串一定要用單引號擴上,然後使用字串的拼接模式,這個變數的規則是spring的EL表示式,一定要用加上#符號,如果是一個物件則可以直接用"."引用屬性,參考createStudent()方法中的#student.id 。
屬性key相當於在students資料夾下的資料夾建立一條以singleStudent+#p0為名字的一條快取資料,在Redis Desktop Manager可以看到,由於students資料夾下的資料夾沒有名字,所以成功快取資料的命名是students::singleStudent1,兩個引號之間為空。這就相當以這個名稱空間下的唯一key,可以根據唯一key準確的失效快取資料。
?@CacheEvict註解使快取失效,根據需求要保證資料庫與快取的一致性,所以運算元據庫之後要同步快取。
在更新,刪除和增加後要使快取失效,不能返回過時的資訊。在這裡使用@Caching的目的是使多條快取失效,它集合了@Cacheable,@CacheEvict,@CachePut,可以很直觀的管理生效與失效。還可以直接使用@CacheEvict(allEntries=true)使這個名稱空間下的所有快取失效。
到這裡核心工作完成得差不多了,就還差controller返回檢視層了。
controller層,StudentController.java
1 package com.xfk.sb.web; 2 3 import com.xfk.sb.pojo.Student; 4 import com.xfk.sb.service.StudentService; 5 import org.springframework.beans.factory.annotation.Autowired; 6 import org.springframework.data.redis.core.StringRedisTemplate; 7 import org.springframework.stereotype.Controller; 8 import org.springframework.ui.Model; 9 import org.springframework.web.bind.annotation.*; 10 import java.util.List; 11 12 @Controller 13 public class StudentController { 14 private final StudentService studentService; 15 private final StringRedisTemplate redis; 16 17 @Autowired 18 public StudentController(StudentService studentService, StringRedisTemplate redis) { 19 this.studentService = studentService; 20 this.redis = redis; 21 } 22 23 @GetMapping("/students") 24 public String listStudent(Model model){ 25 List<Student> students = studentService.selectStudent(); 26 model.addAttribute("students", students); 27 return "listStudent"; 28 } 29 30 @DeleteMapping("/students/{id}") 31 public String deleteStudent(@PathVariable("id")int id) throws Exception{ 32 studentService.deleteStudent(id); 33 return "redirect:/students"; 34 } 35 36 @PutMapping("/students") 37 public String updateStudent(Student student){ 38 studentService.updateStudent(student); 39 return "redirect:/students"; 40 } 41 @PostMapping("/students") 42 public String createStudent(Student student){ 43 studentService.createStudent(student); 44 return "redirect:/students"; 45 } 46 47 @GetMapping("/students/{id}") 48 public String editStudent(@PathVariable("id")int id, Model model){ 49 Student s = studentService.selectStudentByPrimaryKey(id); 50 model.addAttribute("student", s); 51 return "editStudent"; 52 } 53 54 @RequestMapping("/test") 55 public String test(Model model){ 56 List<Student> students = studentService.selectStudent(); 57 model.addAttribute("students", students); 58 return "test"; 59 } 60 61 @RequestMapping("/getOne") 62 public String getOne(Model model){
// 獲取id為1的Student物件到test.jsp 63 Student student = studentService.selectStudentByPrimaryKey(1); 64 model.addAttribute("student", student); 65 return "test"; 66 } 67 }
?使用的restful風格,返回的jsp頁面,/test和/getOne用來驗證快取是否生效。
小貼一下jsp:
listStudent.jsp
1 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 2 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 3 <html> 4 <head> 5 <title>students</title> 6 </head> 7 <body> 8 <form action="${pageContext.request.contextPath}/students" method="post"> 9 增加:<br/> 10 name:<input type="text" name="name"/> <input type="number" name="age" /><br/> 11 <input type="submit" value="提交"/> 12 </form> 13 <table> 14 <tr> 15 <th>id</th> 16 <th>name</th> 17 <th>age</th> 18 <th>編輯</th> 19 <th>刪除</th> 20 </tr> 21 <c:forEach items="${students}" var="each"> 22 <tr> 23 <td>${each.id}</td> 24 <td>${each.name}</td> 25 <td>${each.age}</td> 26 <td><a href="${pageContext.request.contextPath}/students/${each.id}">修改</a></td> 27 <td> 28 <button class="deleteStudent" value="${pageContext.request.contextPath}/students/${each.id}">刪除</button> 29 </td> 30 </tr> 31 </c:forEach> 32 <form id="deleteType" action="" method="POST"> 33 <input type="hidden" name="_method" value="DELETE"/> 34 </form> 35 </table> 36 <button class="hhh">驗證你的jquer是否生效</button> 37 <script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script> 38 <script> 39 $(function(){ 40 $(".deleteStudent").click(function(){ 41 var href = $(this).attr("value"); 42 alert(href); 43 $("#deleteType").attr("action", href).submit(); 44 }) 45 $(".hhh").click(function(){ 46 alert("你的jquer已生效"); 47 }) 48 }) 49 </script> 50 </body> 51 </html>
editStudent.jsp
1 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 2 <html> 3 <head> 4 <title>editStudent</title> 5 </head> 6 <body> 7 <form action="${pageContext.request.contextPath}/students" method="post"> 8 <input type="hidden" name="_method" value="PUT"/> 9 <input type="hidden" name="id" value="${student.id}"/> 10 name:<input type="text" name="name" value="${student.name}"/><br/> 11 age :<input type="text" name="age" value="${student.age}"/><br/> 12 <input type="submit" value="更新"/><a href="${pageContext.request.contextPath}/">返回主頁</a> 13 </form> 14 </body> 15 </html>
test.jsp
1 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> 2 <%@ page contentType="text/html;charset=UTF-8" language="java" %> 3 <html> 4 <head> 5 <title>test</title> 6 </head> 7 <body> 8 <c:forEach items="${students}" var="each"> 9 <div>${each.id}, ${each.name}, ${each.age}</div> 10 </c:forEach> 11 <br/> 12 得到一個Student<br/> 13 ${student.id}, ${student.name}, ${student.age} 14 </body> 15 </html>
四,驗證測試:
?步驟:
1,/getOne
由於StudentServiceImpl.java中的System.out.println("從資料庫中取一個selectStudentByPrimaryKey"); 檢視後臺控制檯,可以知道這條資料是從資料庫中獲取的。
/getOne獲取的Student的id為1,所以會在Redis Desktop Manager中看到一條singleStudent1的快取記錄。
http://localhost:8080/getOne
2,更改Redis Desktop Manager中的記錄
?比如redis庫中原來的資料是
?更改資料,因為這裡改的資料是redis的,mysql裡的資料還是沒變,這樣就知道了是從快取中讀取的資料
?點選save之後,重新整理http://localhost:8080/getOne
成功!????? 然後@CacheEvict是一樣的邏輯,指定失效的key就好了