quarkus資料庫篇之四:本地緩存

来源:https://www.cnblogs.com/bolingcavalry/archive/2023/08/18/17624471.html
-Advertisement-
Play Games

### 歡迎訪問我的GitHub > 這裡分類和彙總了欣宸的全部原創(含配套源碼):[https://github.com/zq2599/blog_demos](https://github.com/zq2599/blog_demos) ### 本篇概覽 - 本文是《quarkus資料庫篇》系列的第 ...


歡迎訪問我的GitHub

這裡分類和彙總了欣宸的全部原創(含配套源碼):https://github.com/zq2599/blog_demos

本篇概覽

  • 本文是《quarkus資料庫篇》系列的第四篇,來實戰一個非常有用的知識點:本地緩存
  • 本地緩存可以省去遠程查詢資料庫的操作,這就讓查詢性能有了顯著提升,然而,對quarkus資料庫本地緩存,我們不能抱太大希望,甚至在使用此功能時候要保持剋制,不要用在重要場合,官方原文如下

image-20220521225623705

  • 個人的理解(請原諒我不入流的英文水平)
  1. quarkus的資料庫本地緩存功能,還處於早期的、原始的、收到諸多限制的階段
  2. 相容性還沒有做好(說不定quarkus一升級就會出現諸多問題)
  3. 將來可能會把更好的緩存方案集成進來(意思就是現在整個方案都不穩定)
  • 實用的功能與搖擺不定的官方態度夾雜在一起,註定了本文不會展開細節,大家隨我一道瞭解quarkus的緩存怎麼用、效果如何,這就夠了,主要分為以下四部分
  1. 新建一個子工程,寫好未使用緩存的資料庫查詢代碼
  2. 增加單個實體類的緩存,並驗證效果
  3. 增加自定義SQL查詢結果的緩存,並驗證效果
  4. 增加一對多關聯查詢的緩存,並驗證效果
  • 這麼水的內容,註定今天是一場輕鬆愉快的體驗之旅(捂臉)
  • 今天實戰用的資料庫依然是PostgreSQL,您可以根據自己情況自行調整

源碼下載

名稱 鏈接 備註
項目主頁 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文件夾下,如下圖紅框
    image-20220312091203116
  • quarkus-tutorials是個父工程,裡面有多個module,本篇實戰的module是basic-cache,如下圖紅框
    image-20220522091827427

開發-創建子工程

  • 《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
  • 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);
  • 配置完成,接下來把代碼功能先想清楚,然後再編碼

基本功能概述

  • 接下來的功能會圍繞兩個表展開
  1. city:每一條記錄是一個城市
  2. country:每一條記錄是一個國家
  3. 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
  • 啥都準備好了,有請本地緩存閃亮登場

實體類緩存

  • 先看不用緩存的時候,查詢單個實體類的性能,增加一個單元測試方法testCacheEntity,用RepeatedTest讓此方法執行一萬次
    @DisplayName("cacheEntity")
    @Order(6)
    @RepeatedTest(10000)
    public void testCacheEntity() {
        City city = cityService.getSingle(EXIST_FIRST_ID);
        // 判定非空
        Assertions.assertNotNull(city);
    }
  • 點擊下圖紅框中的綠色三角形按鈕,會立即執行一萬次testCacheEntity方法
image-20220522110625900
  • 執行完畢後,耗時統計如下圖紅框所示,47秒,單次查詢耗時約為5毫秒左右,記住這兩個數字
image-20220522111025705
  • 接下來是本篇的第一個關鍵:開啟實體類緩存,其實很簡單,如下圖紅框,增加Cacheable註解即可
image-20220522111339094
  • 再次運行單元測試的方法,如下圖紅框,總耗時從之前的47秒縮減到1秒多,黃框中有一些時間統計為空,這表示單次執行的時候耗時低於1毫秒
image-20220522111622929
  • 可見本地緩存的效果是顯著的

SQL查詢結果緩存

  • 回顧city的entity類代碼,如下圖黃框,有一個自定義SQL
image-20220522113005724
  • 寫一個單元測試方法,驗證上述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
  • 然後是本篇的第二個重點:給SQL查詢增加緩存,方法如下圖紅框,增加hints屬性

image-20220522113905266

  • 為SQL添加了本地緩存後,再次執行同樣的單元測試方法,效果如下圖,本地緩存將SQL查詢的耗時從1分零5秒縮短到1秒多鐘
image-20220522114121833
  • 另外要註意的是,如果您的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
  • 新增一個單元測試方法,查詢一條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
  • 接下來,就是本篇的第三個重點:設置一對多關聯查詢緩存,設置方法如下圖紅框所示
image-20220522120307852
  • 再次執行方法testCacheOne2Many,效果如下圖紅框所示,1秒多完成,緩存已生效
image-20220522154156324
  • 最後還要做件事情,就是完整的運行單元測試類CacheTest.java,如此做是為了驗證這個場景:緩存開啟的時候,如果做了寫操作,接下來讀取的也是最新的記錄,而非緩存的之前的舊數據,即緩存失效功能,如下圖,所有測試方法都順利通過,總耗時3秒

重要提示

  • 在使用本地緩存時有個問題需要註意:以city表為例,如果對city表的所有寫操作都是通過當前應用完成的,那麼使用本地緩存是沒有問題的,如果除了basic-cache,還有另一個應用在修改city表,那麼basic-cache中的緩存就不會失效(因為沒人告訴它),這樣從basic-cache中讀取的數據因為是本地緩存,所以還是更新前的數據

  • 至此,quarkus資料庫本地緩存的現有方案,咱們已全部完成了,希望本文能給您一些參考,協助您提升應用性能

歡迎關註博客園:程式員欣宸

學習路上,你不孤單,欣宸原創一路相伴...


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • ##### 2 超鏈接標簽 超鏈接是瀏覽者和伺服器的交互的主要手段,也叫超級鏈接或a鏈接,是網頁中指向一個目標的連接關係,這個目標可以是網頁、網頁中的具體位置、圖片、郵件地址、文件、應用程式等。 超鏈接是網頁中最重要的元素之一。一個網站的各個網頁就是通過超鏈接關聯起來的,用戶通過點擊超鏈接可以從一個 ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 前言 每次用vite創建項目秒建好,前幾天用vue-cli創建了一個項目,足足等了我一分鐘,那為什麼用 vite 比 webpack 要快呢,這篇文章帶你梳理清楚它們的原理及不同之處!文章有一點長,看完絕對有收穫! 正文 一、webpac ...
  • ### 一、實現效果 + 點擊全選按鈕/取消全選,控制商品的全選或取消 + 每個商品的覆選框都選中後,自動勾上全選按鈕,或者商品任何一個覆選框沒有選中,取消全選 ![image](https://img2023.cnblogs.com/blog/2408012/202308/2408012-2023 ...
  • ```html 1基本標簽 你是風兒我是沙 你是風兒我是沙 你是風兒我是沙 你是風兒我是沙 你是風兒我是沙 你是風兒我是沙 床前明月光, 疑是地上霜. 舉頭望明月, 低頭思故鄉. 大家好,我是段落標簽p。我按了enter一下 換行了 大家好,我是段落標簽p。我按了enter一下 換行了 定義粗體文本 ...
  • 本文主要講述京東門詳業務在支撐過程中遇到的困境,面對問題我們在效率提升、質量保障等方向的探索和實踐,在此將實踐過程中問題解決的思路和方案與大家一起分享,也希望能給大家帶來一些新的啟發 ...
  • >我們是[袋鼠雲數棧 UED 團隊](http://ued.dtstack.cn/),致力於打造優秀的一站式數據中台產品。我們始終保持工匠精神,探索前端道路,為社區積累並傳播經驗價值。 >本文作者:[霜序](https://luckyfbb.github.io/blog) ## 前言 在[前一篇文章 ...
  • 本篇博文將深入介紹 Vue3 組合式 API 和單文件組件的寫法。我們將從安裝和配置 Vue3 開始,然後逐步詳細展示如何創建一個簡單的單文件組件。除此之外,我們還將討論使用組合式 API 的常見模式和技巧,例如響應式狀態管理、替代生命周期鉤子函數的方法、自定義組合式 API、數據的響應式處理和偵聽... ...
  • 一、背景 會員系統是一種基礎系統,跟公司所有業務線的下單主流程密切相關。如果會員系統出故障,會導致用戶無法下單,影響範圍是全公司所有業務線。所以,會員系統必須保證高性能、高可用,提供穩定、高效的基礎服務。 隨著同程和藝龍兩家公司的合併,越來越多的系統需要打通同程APP、藝龍APP、同程微信小程式、藝 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...