微信公眾號:一個優秀的廢人 如有問題或建議,請後臺留言,我會盡力解決你的問題。
前言
如題,今天介紹 SpringBoot 的資料快取。做過開發的都知道程式的瓶頸在於資料庫,我們也知道記憶體的速度是大大快於硬碟的,當需要重複獲取相同資料時,一次又一次的請求資料庫或者遠端服務,導致大量時間耗費在資料庫查詢或遠端方法呼叫上,導致效能的惡化,這便是資料快取要解決的問題。
Spring 的快取支援
Spring 定義了 org.springframework.cache.CacheManager 和 org.springframework.cache.Cache 介面用於統一不同的快取技術。其中,CacheManager 是 Spring 提供的各種快取技術的抽象介面,Cache 介面則是包含了快取的各種操作(增加,刪除,獲取快取,一般不會直接和此介面打交道)。
*1、*Spring 支援的 CacheManager
針對不同的快取技術,實現了不同的 CacheManager ,Spring 定義了下表所示的 CacheManager:
CacheManager | 描述 |
---|---|
SimpleCacheManager | 使用簡單的 Collection 來儲存快取,主要用於測試 |
ConcurrentMapCacheManager | 使用 ConcurrentMap 來儲存快取 |
NoOpCacheManager | 僅測試用途,不會實際快取資料 |
EhCacheCacheManager | 使用 EhCache 作為快取技術 |
GuavaCacheManager | 使用 Google Guava 的 GuavaCache 作為快取技術 |
HazelcastCacheManager | 使用 Hazelcast 作為快取技術 |
JCacheCacheManager | 支援 JCache(JSR-107) 標準的實現作為快取技術,如 ApacheCommonsJCS |
RedisCacheManager | 使用 Redis 作為快取技術 |
在使用以上任意一個實現的 CacheManager 的時候,需註冊實現的 CacheManager 的 Bean,如:
@Bean
public EhCacheCacheManager cacheManager(CacheManager
ehCacheCacheManager){
return new EhCacheCacheManager(ehCacheCacheManager);
}
複製程式碼
注意,每種快取技術都有很多的額外配置,但配置 cacheManager 是必不可少的。
*2、*宣告式快取註解
Spring 提供了 4 個註解來宣告快取規則(又是使用註解式的 AOP 的一個例子)。4 個註解如下表示:
註解 | 解釋 |
---|---|
@Cacheable | 在方法執行前 Spring 先檢視快取中是否有資料,若有,則直接返回快取資料;若無資料,呼叫方法將方法返回值放入快取中 |
@CachePut | 無論怎樣,都會將方法的返回值放到快取中。 |
@CacheEvict | 將一條或多條資料從快取中刪除 |
@Caching | 可以通過 @Caching 註解組合多個註解策略在一個方法上 |
@Cacheable、@CachePut、@CacheEvict 都有 value 屬性,指定的是要使用的快取名稱;key 屬性指定的是資料在快取中儲存的鍵。
*3、*開啟宣告式快取支援
開啟宣告式快取很簡單,只需在配置類上使用 @EnableCaching 註解即可,例如:
@Configuration
@EnableCaching
public class AppConfig{
}
複製程式碼
SpringBoot 的支援
在 Spring 中使用快取技術的關鍵是配置 CacheManager ,而 SpringBoot 為我們配置了多個 CacheManager 的實現。
它的自動配置放在 org.springframework.boot.autoconfigure.cache 包中。
在不做任何配置的情況下,預設使用的是 SimpleCacheConfiguration ,即使用 ConcurrentMapCacheManager。SpringBoot 支援以字首來配置快取。例如:
spring.cache.type= # 可選 generic、ehcache、hazelcast、infinispan、jcache、redis、guava、simple、none
spring.cache.cache-names= # 程式啟動時建立的快取名稱
spring.cache.ehcache.config= # ehcache 配置檔案的地址
spring.cache.hazelcast.config= # hazelcast配置檔案的地址
spring.cache.infinispan.config= # infinispan配置檔案的地址
spring.cache.jcache.config= # jcache配置檔案的地址
spring.cache.jcache.provider= # 當多個 jcache 實現在類路徑的時候,指定 jcache 實現
# 等等。。。
複製程式碼
在 SpringBoot 環境下,使用快取技術只需要在專案中匯入相關快取技術的依賴包,並在配置類中使用 @EnableCaching 開啟快取支援即可。
程式碼實現
本文將以 SpringBoot 預設的 ConcurrentMapCacheManager 作為快取技術,演示 @Cacheable、@CachePut、@CacheEvict。
*1、*準備工作
- IDEA
- JDK 1.8
- SpringBoot 2.1.3
*2、*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>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.nasus</groupId>
<artifactId>cache</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>cache</name>
<description>cache Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<!-- cache 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!-- JPA 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<!-- web 啟動類 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- mysql 資料庫連線類 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- lombok 依賴,簡化實體 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 單元測試類 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
複製程式碼
註釋很清楚,無需多言。不會就谷歌一下。
*3、*Application.yaml 檔案配置
spring:
# 資料庫相關
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://127.0.0.1:3306/test?useUnicode=true&characterEncoding=utf8&serverTimezone=UTC&useSSL=true
username: root
password: 123456
# jpa 相關
jpa:
hibernate:
ddl-auto: update # ddl-auto: 設為 create 表示每次都重新建表
show-sql: true
複製程式碼
*4、*實體類
package com.nasus.cache.entity;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class Student {
@Id
@GeneratedValue
private Integer id;
private String name;
private Integer age;
}
複製程式碼
*5、*dao 層
package com.nasus.cache.repository;
import com.nasus.cache.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface StudentRepository extends JpaRepository<Student,Integer> {
}
複製程式碼
*6、*service 層
package com.nasus.cache.service;
import com.nasus.cache.entity.Student;
public interface StudentService {
public Student saveStudent(Student student);
public void deleteStudentById(Integer id);
public Student findStudentById(Integer id);
}
複製程式碼
實現類:
package com.nasus.cache.service.impl;
import com.nasus.cache.entity.Student;
import com.nasus.cache.repository.StudentRepository;
import com.nasus.cache.service.StudentService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class StudentServiceImpl implements StudentService {
// 使用 slf4j 作為日誌框架
private static final Logger LOGGER = LoggerFactory.getLogger(StudentServiceImpl.class);
@Autowired
private StudentRepository studentRepository;
@Override
@CachePut(value = "student",key = "#student.id")
// @CachePut 快取新增的或更新的資料到快取,其中快取名稱為 student 資料的 key 是 student 的 id
public Student saveStudent(Student student) {
Student s = studentRepository.save(student);
LOGGER.info("為id、key 為{}的資料做了快取", s.getId());
return s;
}
@Override
@CacheEvict(value = "student")
// @CacheEvict 從快取 student 中刪除 key 為 id 的資料
public void deleteStudentById(Integer id) {
LOGGER.info("刪除了id、key 為{}的資料快取", id);
//studentRepository.deleteById(id);
}
@Override
@Cacheable(value = "student",key = "#id")
// @Cacheable 快取 key 為 id 的資料到快取 student 中
public Student findStudentById(Integer id) {
Student s = studentRepository.findById(id).get();
LOGGER.info("為id、key 為{}的資料做了快取", id);
return s;
}
}
複製程式碼
程式碼講解看註釋,很詳細。
*7、*controller 層
package com.nasus.cache.controller;
import com.nasus.cache.entity.Student;
import com.nasus.cache.service.StudentService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.Mapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/student")
public class StudentController {
@Autowired
private StudentService studentService;
@PostMapping("/put")
public Student saveStudent(@RequestBody Student student){
return studentService.saveStudent(student);
}
@DeleteMapping("/evit/{id}")
public void deleteStudentById(@PathVariable("id") Integer id){
studentService.deleteStudentById(id);
}
@GetMapping("/able/{id}")
public Student findStudentById(@PathVariable("id") Integer id){
return studentService.findStudentById(id);
}
}
複製程式碼
*8、*application 開啟快取功能
package com.nasus.cache;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@EnableCaching // 開啟快取功能
@SpringBootApplication
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
複製程式碼
測試
測試前,先看一眼資料庫當前的資料,如下:
*1、*測試 @Cacheable
訪問 http://localhost:8080/student/able/2 控制檯列印出了 SQL 查詢語句,以及指定日誌。說明這一次程式是直接查詢資料庫得到的結果。
2019-02-21 22:54:54.651 INFO 1564 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : Completed initialization in 11 ms
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 22:54:59.725 INFO 1564 --- [nio-8080-exec-1] c.n.c.service.impl.StudentServiceImpl : 為id、key 為2的資料做了快取
複製程式碼
postman 第一次測試結果 :
再次訪問 http://localhost:8080/student/able/2 結果如下圖。但控制檯無 SQL 語句列印,也無為id、key 為2的資料做了快取這句話輸出。
說明 @Cacheable 確實做了資料快取,第二次的測試結果是從資料快取中獲取的,並沒有直接查資料庫。
*2、*測試 @CachePut
如下圖,postman 訪問 http://localhost:8080/student/put 插入資料:
下面是控制檯列印出了 SQL Insert 插入語句,以及指定日誌。說明程式做了快取。
Hibernate: insert into student (age, name, id) values (?, ?, ?)
2019-02-21 23:12:03.688 INFO 1564 --- [nio-8080-exec-8] c.n.c.service.impl.StudentServiceImpl : 為id、key 為4的資料做了快取
複製程式碼
插入資料返回的結果:
資料庫中的結果:
訪問 http://localhost:8080/student/able/4 Postman 結果如下圖。控制檯無輸出,驗證了 @CachePut 確實做了快取,下圖資料是從快取中獲取的。
*3、*測試 @CacheEvict
postman 訪問 http://localhost:8080/student/able/3 為 id = 3 的資料做快取。
postman 再次訪問 http://localhost:8080/student/able/3 確認資料是從快取中獲取的。
postman 訪問 http://localhost:8080/student/evit/3
從快取中刪除 key 為 3 的快取資料:
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:26:08.516 INFO 8612 --- [nio-8080-exec-2] c.n.c.service.impl.StudentServiceImpl : 為id、key 為3的資料做了快取
2019-02-21 23:27:01.508 INFO 8612 --- [nio-8080-exec-4] c.n.c.service.impl.StudentServiceImpl : 刪除了id、key 為3的資料快取
複製程式碼
再次 postman 訪問 http://localhost:8080/student/able/3 觀察後臺,重新做了資料快取:
Hibernate: select student0_.id as id1_0_0_, student0_.age as age2_0_0_, student0_.name as name3_0_0_ from student student0_ where student0_.id=?
2019-02-21 23:27:12.320 INFO 8612 --- [nio-8080-exec-5] c.n.c.service.impl.StudentServiceImpl : 為id、key 為3的資料做了快取
複製程式碼
這一套測試流程下來,證明了 @CacheEvict 確實刪除了資料快取。
原始碼下載
切換快取技術
切換快取技術除了在 pom 檔案加入相關依賴包配置以外,使用方式與上面的程式碼演示一樣。
*1、*切換 EhCache 在 pom 中新增 Encache 依賴:
<!-- EhCache 依賴 -->
<dependency>
<groupId>net.sf.ehcache</groupId>
<artifactId>ehcache</artifactId>
</dependency>
複製程式碼
Ehcache 所需配置檔案 ehcache.xml 只需放在類路徑(resource 目錄)下,SpringBoot 會自動掃描,如:
<?xml version="1.0" encoding="UTF-8">
<ehcache>
<cache name="student" maxElementsInMmory="1000">
<ehcache>
複製程式碼
SpringBoot 會自動配置 EhcacheManager 的 Bean。
*2、*切換 Guava
只需在 pom 中加入 Guava 依賴即可:
<!-- GuavaCache 依賴 -->
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>18.0</version>
</dependency>
複製程式碼
SpringBoot 會自動配置 GuavaCacheManager 的 Bean。
*3、*切換 RedisCache
與 Guava 一樣,只需在 pom 加入依賴即可:
<!-- cache 依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-redis</artifactId>
</dependency>
複製程式碼
SpringBoot 會自動配置 RedisCacheManager 以及 RedisTemplate 的 Bean。
此外,切換其他快取技術的方式也是類似。這裡不做贅述。
後語
以上為 SpringBoot 資料快取的教程。
如果本文對你哪怕有一丁點幫助,請幫忙點好看。
你的好看是我堅持寫作的動力。
另外,關注之後在傳送 1024 可領取免費學習資料。資料內容詳情請看這篇舊文:Python、C++、Java、Linux、Go、前端、演算法資料分享