### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus資料庫篇》系列的第 ...
歡迎訪問我的GitHub
這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos
本篇概覽
- 本文是《quarkus資料庫篇》系列的第四篇,來實戰一個非常有用的知識點:本地緩存
- 本地緩存可以省去遠程查詢資料庫的操作,這就讓查詢性能有了顯著提升,然而,對quarkus資料庫本地緩存,我們不能抱太大希望,甚至在使用此功能時候要保持剋制,不要用在重要場合,官方原文如下
- 個人的理解(請原諒我不入流的英文水平)
- quarkus的資料庫本地緩存功能,還處於早期的、原始的、收到諸多限制的階段
- 相容性還沒有做好(說不定quarkus一升級就會出現諸多問題)
- 將來可能會把更好的緩存方案集成進來(意思就是現在整個方案都不穩定)
- 實用的功能與搖擺不定的官方態度夾雜在一起,註定了本文不會展開細節,大家隨我一道瞭解quarkus的緩存怎麼用、效果如何,這就夠了,主要分為以下四部分
- 新建一個子工程,寫好未使用緩存的資料庫查詢代碼
- 增加單個實體類的緩存,並驗證效果
- 增加自定義SQL查詢結果的緩存,並驗證效果
- 增加一對多關聯查詢的緩存,並驗證效果
- 這麼水的內容,註定今天是一場輕鬆愉快的體驗之旅(捂臉)
- 今天實戰用的資料庫依然是PostgreSQL,您可以根據自己情況自行調整
源碼下載
- 如果您想寫代碼,可以在我的GitHub倉庫下載到完整源碼,地址和鏈接信息如下表所示(https://github.com/zq2599/blog_demos)
名稱 | 鏈接 | 備註 |
---|---|---|
項目主頁 | https://github.com/zq2599/blog_demos | 該項目在GitHub上的主頁 |
git倉庫地址(https) | https://github.com/zq2599/blog_demos.git | 該項目源碼的倉庫地址,https協議 |
git倉庫地址(ssh) | [email protected]:zq2599/blog_demos.git | 該項目源碼的倉庫地址,ssh協議 |
- 這個git項目中有多個文件夾,本次實戰的源碼在quarkus-tutorials文件夾下,如下圖紅框
- quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-cache,如下圖紅框
開發-創建子工程
- 《quarkus實戰之一:準備工作》已創建了父工程,今天在此父工程下新增名為basic-cache的子工程,其pom與前文的工程區別不大,新增MySQL庫,所有依賴如下
<dependencies>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-arc</artifactId>
</dependency>
<!-- JDBC庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-agroal</artifactId>
</dependency>
<!-- hibernate庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-orm</artifactId>
</dependency>
<!-- postgresql庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-jdbc-postgresql</artifactId>
</dependency>
<!-- 單元測試庫 -->
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
開發-配置文件
- 為了滿足多個profile的需要,配置文件繼續使用application.properties和application-xxx.properties組合的方式,application.properties里存放公共配置,例如資料庫類型,而application-xxx.properties裡面是和各個profile環境有關的配置項,例如資料庫IP地址、賬號密碼等,如下圖
![image-20220522093404215](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045032-1813767749.png)
- application.properties內容如下
quarkus.datasource.db-kind=postgresql
quarkus.hibernate-orm.log.sql=true
quarkus.datasource.jdbc.max-size=8
quarkus.datasource.jdbc.min-size=2
- application-test.properties
quarkus.datasource.username=quarkus
quarkus.datasource.password=123456
quarkus.datasource.jdbc.url=jdbc:postgresql://192.168.50.43:15432/quarkus_test
quarkus.hibernate-orm.database.generation=drop-and-create
quarkus.hibernate-orm.sql-load-script=import.sql
- 應用啟動時載入數據的腳本import.sql
INSERT INTO city(id, name) VALUES (1, 'BeiJing');
INSERT INTO city(id, name) VALUES (2, 'ShangHai');
INSERT INTO city(id, name) VALUES (3, 'GuangZhou');
INSERT INTO country(id, name) VALUES (1, 'China');
INSERT INTO country_city(country_id, cities_id) VALUES (1, 1);
INSERT INTO country_city(country_id, cities_id) VALUES (1, 2);
INSERT INTO country_city(country_id, cities_id) VALUES (1, 3);
- 配置完成,接下來把代碼功能先想清楚,然後再編碼
基本功能概述
- 接下來的功能會圍繞兩個表展開
- city:每一條記錄是一個城市
- country:每一條記錄是一個國家
- country-cities:每一條記錄是一個城市和國家的關係
- 然後,咱們要寫出city和country的增刪改查代碼,另外city和country是一對多的關係,這裡涉及到關聯查詢
- 最後,全部用單元測試來對比添加緩存前後的查詢介面執行時間,以此驗證緩存生效
開發-實體類
- city表的實體類是City.java,和前面幾篇文章中的實體類沒啥區別,要註意的是有個名為City.findAll的自定義SQL查詢,稍後會用來驗證本地緩存是否對自動一個SQL有效
package com.bolingcavalry.db.entity;
import javax.persistence.*;
@Entity
@Table(name = "city")
@NamedQuery(name = "City.findAll", query = "SELECT c FROM City c ORDER BY c.name")
public class City {
@Id
@SequenceGenerator(name = "citySequence", sequenceName = "city_id_seq", allocationSize = 1, initialValue = 10)
@GeneratedValue(generator = "citySequence")
private Integer id;
@Column(length = 40, unique = true)
private String name;
public City() {
}
public City(String name) {
this.name = name;
}
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;
}
}
- country表的實體類是Country.java,這裡有一處要註意的地方,就是在我們的設計中,city和country表並不是通過欄位關聯的,而是一個額外的表記錄了他們之間的關係,因此,成員變數citys並不對應country或者city表的某個欄位,使用註解OneToMany後,quarkus的hibernate模塊預設用country_cities表來記錄city和country的關係,至於country_cities這個表名,來自quarkus的預設規則,如果您想用city或者country的某個欄位來建立兩表的關聯,請參考javax.persistence.OneToMany源碼的註釋,裡面有詳細說明
package com.bolingcavalry.db.entity;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "country")
public class Country {
@Id
@SequenceGenerator(name = "countrySequence", sequenceName = "country_id_seq", allocationSize = 1, initialValue = 10)
@GeneratedValue(generator = "countrySequence")
private Integer id;
@Column(length = 40, unique = true)
private String name;
@OneToMany
List<City> cities;
public Country() {
}
public Country(String name) {
this.name = name;
}
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 List<City> getCities() {
return cities;
}
public void setCities(List<City> cities) {
this.cities = cities;
}
}
- 兩個實體類寫完了,該寫服務類了
開發-服務類
- city表的增刪改查
@ApplicationScoped
public class CityService {
@Inject
EntityManager entityManager;
public City getSingle(Integer id) {
return entityManager.find(City.class, id);
}
public List<City> get() {
return entityManager.createNamedQuery("City.findAll", City.class)
.getResultList();
}
@Transactional
public void create(City fruit) {
entityManager.persist(fruit);
}
@Transactional
public void update(Integer id, City fruit) {
City entity = entityManager.find(City.class, id);
if (null!=entity) {
entity.setName(fruit.getName());
}
}
@Transactional
public void delete(Integer id) {
City entity = entityManager.getReference(City.class, id);
if (null!=entity) {
entityManager.remove(entity);
}
}
}
- country表的增刪改查,為了簡化,只寫一個按照id查詢的,至於其他的操作如新增刪除等,在本篇研究緩存時用不上就不寫了
@ApplicationScoped
public class CountyService {
@Inject
EntityManager entityManager;
public Country getSingle(Integer id) {
return entityManager.find(Country.class, id);
}
}
- 應用代碼已經寫完了,接下來是驗證基本功能的單元測試代碼
開發-單元測試
- 資料庫數據被修改後,再次讀取的時候,是讀到最新的數據,還是之前緩存的舊數據呢?顯然前者才是正確的,這就需要單元測試來保證正確性了
@QuarkusTest
@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class CacheTest {
/**
* import.sql中導入的記錄數量,這些是應用啟動是導入的
*/
private static final int EXIST_CITY_RECORDS_SIZE = 3;
private static final int EXIST_COUNTRY_RECORDS_SIZE = 1;
/**
* 在City.java中,id欄位的SequenceGenerator指定了initialValue等於10,
* 表示自增ID從10開始
*/
private static final int ID_SEQUENCE_INIT_VALUE = 10;
/**
* import.sql中,第一條記錄的id
*/
private static final int EXIST_FIRST_ID = 1;
@Inject
CityService cityService;
@Inject
CountyService countyService;
@Test
@DisplayName("list")
@Order(1)
public void testGet() {
List<City> list = cityService.get();
// 判定非空
Assertions.assertNotNull(list);
// import.sql中新增3條記錄
Assertions.assertEquals(EXIST_CITY_RECORDS_SIZE, list.size());
}
@Test
@DisplayName("getSingle")
@Order(2)
public void testGetSingle() {
City city = cityService.getSingle(EXIST_FIRST_ID);
// 判定非空
Assertions.assertNotNull(city);
// import.sql中的第一條記錄
Assertions.assertEquals("BeiJing", city.getName());
}
@Test
@DisplayName("update")
@Order(3)
public void testUpdate() {
String newName = LocalDateTime.now().toString();
cityService.update(EXIST_FIRST_ID, new City(newName));
// 從資料庫取出的對象,其名稱應該等於修改的名稱
Assertions.assertEquals(newName, cityService.getSingle(EXIST_FIRST_ID).getName());
}
@Test
@DisplayName("create")
@Order(4)
public void testCreate() {
int numBeforeDelete = cityService.get().size();
City city = new City("ShenZhen");
cityService.create(city);
// 由於是第一次新增,所以ID應該等於自增ID的起始值
Assertions.assertEquals(ID_SEQUENCE_INIT_VALUE, city.getId());
// 記錄總數應該等於已有記錄數+1
Assertions.assertEquals(numBeforeDelete + 1, cityService.get().size());
}
@Test
@DisplayName("delete")
@Order(5)
public void testDelete() {
// 先記刪除前的總數
int numBeforeDelete = cityService.get().size();
// 刪除testCreate方法中新增的記錄,此記錄的是第一次使用自增主鍵,所以id等於自增主鍵的起始id
cityService.delete(ID_SEQUENCE_INIT_VALUE);
// 記錄數應該應該等於刪除前的數量減一
Assertions.assertEquals(numBeforeDelete-1, cityService.get().size());
}
}
- 運行單元測試,如下圖,兩個表的操作都正常,建表語句也符合預期
![image-20220522105210894](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045220-1948219242.png)
- 啥都準備好了,有請本地緩存閃亮登場
實體類緩存
- 先看不用緩存的時候,查詢單個實體類的性能,增加一個單元測試方法testCacheEntity,用RepeatedTest讓此方法執行一萬次
@DisplayName("cacheEntity")
@Order(6)
@RepeatedTest(10000)
public void testCacheEntity() {
City city = cityService.getSingle(EXIST_FIRST_ID);
// 判定非空
Assertions.assertNotNull(city);
}
- 點擊下圖紅框中的綠色三角形按鈕,會立即執行一萬次testCacheEntity方法
![image-20220522110625900](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045095-1183097969.png)
- 執行完畢後,耗時統計如下圖紅框所示,47秒,單次查詢耗時約為5毫秒左右,記住這兩個數字
![image-20220522111025705](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045150-1829458574.png)
- 接下來是本篇的第一個關鍵:開啟實體類緩存,其實很簡單,如下圖紅框,增加Cacheable註解即可
![image-20220522111339094](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045030-1436233989.png)
- 再次運行單元測試的方法,如下圖紅框,總耗時從之前的47秒縮減到1秒多,黃框中有一些時間統計為空,這表示單次執行的時候耗時低於1毫秒
![image-20220522111622929](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045354-869837379.png)
- 可見本地緩存的效果是顯著的
SQL查詢結果緩存
- 回顧city的entity類代碼,如下圖黃框,有一個自定義SQL
![image-20220522113005724](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045056-188430478.png)
- 寫一個單元測試方法,驗證上述SQL的實際性能
@DisplayName("cacheSQL")
@Order(7)
@RepeatedTest(10000)
public void testCacheSQL() {
List<City> cities = cityService.get();
// 判定非空
Assertions.assertNotNull(cities);
// import.sql中新增3條city記錄
Assertions.assertEquals(EXIST_CITY_RECORDS_SIZE, cities.size());
}
- 單元測試效果如下圖,紅框顯示,沒有使用緩存時,一萬次自定義SQL查詢需要1分鐘零5秒
![image-20220522113546498](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045089-1275829950.png)
- 然後是本篇的第二個重點:給SQL查詢增加緩存,方法如下圖紅框,增加hints屬性
- 為SQL添加了本地緩存後,再次執行同樣的單元測試方法,效果如下圖,本地緩存將SQL查詢的耗時從1分零5秒縮短到1秒多鐘
![image-20220522114121833](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045168-108229320.png)
- 另外要註意的是,如果您的SQL是通過API執行的,而不是基於NamedQuery註解,那就要通過API來開啟SQL緩存,示例如下
Query query = ...
query.setHint("org.hibernate.cacheable", Boolean.TRUE);
一對多關聯查詢緩存
- country和city是一對多的關係,查詢Country記錄的時候,與其關聯的city表記錄也會被查詢出來,填入Country對象的cities成員變數中
- 所以,是不是只要給實體類Country增加緩存註解,在查詢Country的時候,其關聯的City對象也會走本地緩存呢?
- 咱們來實際驗證一下吧,先給Country類增加緩存註解,如下圖紅框
![image-20220522115127475](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045124-147486149.png)
- 新增一個單元測試方法,查詢一條Country記錄
@DisplayName("cacheOne2Many")
@Order(8)
@RepeatedTest(10000)
public void testCacheOne2Many() {
Country country = countyService.getSingle(EXIST_FIRST_ID);
// 判定非空
Assertions.assertNotNull(country);
// import.sql中新增3條city記錄
Assertions.assertEquals(EXIST_CITY_RECORDS_SIZE, country.getCities().size());
}
- 執行方法testCacheOne2Many,效果如下圖紅框所示,34秒,這顯然是本地緩存沒有生效的結果
![image-20220522115658747](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045136-48339089.png)
- 接下來,就是本篇的第三個重點:設置一對多關聯查詢緩存,設置方法如下圖紅框所示
![image-20220522120307852](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045143-670313141.png)
- 再次執行方法testCacheOne2Many,效果如下圖紅框所示,1秒多完成,緩存已生效
![image-20220522154156324](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045142-2051075298.png)
- 最後還要做件事情,就是完整的運行單元測試類CacheTest.java,如此做是為了驗證這個場景:緩存開啟的時候,如果做了寫操作,接下來讀取的也是最新的記錄,而非緩存的之前的舊數據,即緩存失效功能,如下圖,所有測試方法都順利通過,總耗時3秒
![](https://img2023.cnblogs.com/blog/485422/202308/485422-20230812101045147-926244488.png)
重要提示
-
在使用本地緩存時有個問題需要註意:以city表為例,如果對city表的所有寫操作都是通過當前應用完成的,那麼使用本地緩存是沒有問題的,如果除了basic-cache,還有另一個應用在修改city表,那麼basic-cache中的緩存就不會失效(因為沒人告訴它),這樣從basic-cache中讀取的數據因為是本地緩存,所以還是更新前的數據
-
至此,quarkus資料庫本地緩存的現有方案,咱們已全部完成了,希望本文能給您一些參考,協助您提升應用性能