效能最佳化2.0,新增快取後,程式的秒開率不升反降

碼農談IT發表於2024-01-15

來源:哪吒程式設計

一、前情提要

在上一篇文章中提到,有一個頁面載入速度很慢,是透過緩衝流最佳化的。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

查詢的時候,會訪問後臺資料庫,查詢前20條資料,按道理來說,這應該很快才對。

追蹤程式碼,看看啥問題,最後發現問題有三:

  1. 表中有一個BLOB大欄位,儲存著一個PDF模板,也就是上圖中的運費模板;
  2. 查詢後會將這個PDF模板儲存到本地磁碟
  3. 點選線上顯示,會讀取本地的PDF模板,透過socket傳到伺服器。

大欄位批次查詢、批次檔案落地、讀取大檔案並進行網路傳輸,不慢才怪,這一頓騷操作,5秒能載入完畢,已經燒高香了。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

經過4次最佳化,將頁面的載入時間控制在了1秒以內,實打實的提升了程式的秒開率。

  1. 批次查詢時,不查詢BLOB大欄位;
  2. 點選運費查詢時,單獨查詢+觸發索引,實現“懶載入”;
  3. 非同步儲存檔案
  4. 透過 緩衝流 -> 記憶體對映技術mmap -> sendFile零複製 讀取本地檔案;
效能最佳化2.0,新增快取後,程式的秒開率不升反降

有一個小夥伴在評論中提到,還可以透過快取繼續最佳化,確實是可以的,快取也是複用最佳化的一種。

為了提高頁面的載入速度,使用了單條查詢 + 觸發索引,提高資料庫查詢速度。

歸根結底,還是查詢了資料庫,如果不查呢,訪問速度肯定會更快。

這個時候,就用到了快取,將運費模板存到快取中。

二、先了解一下,什麼是快取

快取就是把訪問量較高的熱點資料從傳統的關係型資料庫中載入到記憶體中,當使用者再次訪問熱點資料時,是從記憶體中載入,減少了對資料庫的訪問量,解決了高併發場景下容易造成資料庫當機的問題。

我理解的快取的本質就是一個用空間換時間的一個思想。

提供“快取”的目的是為了讓資料訪問的速度適應CPU的處理速度,其基於的原理是記憶體中“區域性性原理”。

CPU 快取的是記憶體資料,用於解決 CPU 處理速度和記憶體不匹配的問題,比如處理器和記憶體之間的快取記憶體,作業系統在記憶體管理上,針對虛擬記憶體 為頁表項使用了一特殊的快取記憶體TLB轉換檢測緩衝區,因為每個虛擬記憶體訪問會引起兩次物理訪問,一次取相關的頁表項,一次取資料,TLB引入來加速虛擬地址到實體地址的轉換。

1、快取有哪些分類

  1. 作業系統磁碟快取,減少磁碟機械操作
  2. 資料庫快取,減少檔案系統 I/O
  3. 應用程式快取,減少對資料庫的查詢
  4. Web 伺服器快取,減少應用程式伺服器請求
  5. 客戶端瀏覽器快取,減少對網站的訪問

2、本地快取與分散式快取

本地快取:在客戶端本地的實體記憶體中劃出一部分空間,來快取客戶端回寫到伺服器的資料。當本地回寫快取達到快取閾值時,將資料寫入到伺服器中。

本地快取是指程式級別的快取元件,它的特點是本地快取和應用程式會執行在同一個程式中,所以本地快取的操作會非常快,因為在同一個程式內也意味著不會有網路上的延遲和開銷。

本地快取適用於單節點非叢集的應用場景,它的優點是快,缺點是多程式無法共享快取。

無法共享快取可能會造成系統資源的浪費,每個系統都單獨維護了一份屬於自己的快取,而同一份快取有可能被多個系統單獨進行儲存,從而浪費了系統資源。

分散式快取是指將應用系統和快取元件進行分離的快取機制,這樣多個應用系統就可以共享一套快取資料了,它的特點是共享快取服務和可叢集部署,為快取系統提供了高可用的執行環境,以及快取共享的程式執行機制。

下面介紹一個小編最常用的本地快取 Guava Cache。

三、Guava Cache本地快取

1、Google Guava

Google Guava是一個Java程式設計庫,其中包含了許多高質量的工具類和方法。其中,Guava的快取工具之一是LoadingCache。LoadingCache是一個帶有自動載入功能的快取,可以自動載入快取中不存在的資料。其實質是一個鍵值對(Key-Value Pair)的快取,可以使用鍵來獲取相應的值。

Guava Cache 的架構設計靈感來源於 ConcurrentHashMap,它使用了多個 segments 方式的細粒度鎖,在保證執行緒安全的同時,支援了高併發的使用場景。Guava Cache 類似於 Map 集合的方式對鍵值對進行操作,只不過多了過期淘汰等處理邏輯。

Guava Cache對比ConcurrentHashMap優勢在哪?

  1. Guava Cache可以設定過期時間,提供資料過多時的淘汰機制;
  2. 執行緒安全,支援併發讀寫;
  3. 在快取擊穿時,GuavaCache 可以使用 CacheLoader 的load 方法控制,對同一個key,只允許一個請求去讀源並回填快取,其他請求阻塞等待;

2、Loadingcache資料結構

效能最佳化2.0,新增快取後,程式的秒開率不升反降
  1. Loadingcache含有多個Segment,每一個Segment中有若干個有效佇列;
  2. 多個Segment之間互不打擾,可以併發執行;
  3. 各個Segment的擴容只需要擴自己,與其它的Segment無關;
  4. 設定合適的初始化容量與併發水平引數,可以有效避免擴容,但是設定的太大了,耗費記憶體,設定的太小,快取價值降低,需要根據業務需求進行權衡;
  5. Loadingcache資料結構和ConcurrentHashMap很相似,ReferenceEntry用於存放key-value;
  6. 每一個ReferenceEntry都會存放一個雙向連結串列,採用的是Entry替換的方式;
  7. 每次訪問某個元素就將元素移動到連結串列頭部,這樣連結串列尾部的元素就是最近最少使用的元素,替換的複雜度為o(1),但是訪問的複雜度還是O(n);
  8. 佇列用於實現LRU快取回收演算法;

3、Loadingcache資料結構構建流程:

  1. 初始化CacheBuilder,指定引數(併發級別、過期時間、初始容量、快取最大容量),使用build()方法建立LocalCache例項;
  2. 建立Segment陣列,初始化每一個Segment;
  3. 為Segment屬性賦值;
  4. 初始化Segment中的table,即一個ReferenceEntry陣列(每一個key-value就是一個ReferenceEntry);
  5. 根據之前類變數的賦值情況,建立相應佇列,用於LRU快取回收演算法。

4、判斷快取是否過期

  1. expireAfterWrite,在put時更新快取時間戳,在get時如果發現當前時間與時間戳的差值大於過期時間戳,就會進行load操作;
  2. expireAfterAccess,在expireAfterWrite的基礎上,不管是寫還是讀都會記錄新的時間戳;
  3. refreshAfterWrite,呼叫get進行值的獲取的時候才會執行reload操作,這裡的重新整理操作可以透過非同步呼叫load實現。

5、Loadingcache如何解決快取穿透

快取穿透是指在Loadingcache快取中,由於某些原因,快取的資料無法被正常訪問或處理,導致快取失去了它的作用。

發生快取穿透的原因有很多,比如資料量過大、資料更新頻繁、資料過期、資料許可權限制、快取效能瓶頸等原因,這裡不過多糾結。

(1)expireAfterAcess和expireAfterWrite同步載入

設定為expireAfterAcess和expireAfterWrite時,在進行get的過程中,快取失效的話,會進行load操作,load是一個同步載入的操作,如下圖:

如果發生了快取穿透,當有大量併發請求訪問快取時,會有一個執行緒去同步查詢DB,隨即透過reeatrantLock進入loading等待狀態,其它請求相同key的執行緒,一部分在waitforvalue,另一部分在reentantloack的阻塞佇列中,等待同步查詢完畢,所有請求都會獲得最新值。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

(2)refreshAfterWrite同步載入

如果採用refresh的話,會透過scheduleRefresh方法進行load,也是一個執行緒同步獲取DB。

其它執行緒不會阻塞,效能比expireAfterWrite同步載入高,但是,可能返回新值、也可能返回舊值。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

(3)refreshAfterWrite非同步載入

當載入快取的執行緒是非同步載入的話,對於請求1,如果在非同步結束前返回,就會返回舊值,反之是新值。

對於其他執行緒來說,不會被阻塞,直接返回,返回值可能是新值或者是舊值。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

Loadingcache沒使用額外的執行緒去做定時清理和載入的功能,而是依賴於get()請求。

在查詢的時候,進行時間對比,如果使用refreshAfterWrite,在長時間沒有查詢時,查詢有可能會得到一箇舊值,我們可以透過設定refreshAfterWrite(寫重新整理,在get時可以同步或非同步快取的時間戳)為5s,將expireAfterWrite(寫過期,在put時更新快取的時間戳)設為10s,當訪問頻繁的時候,會在每5秒都進行refresh,而當超過10s沒有訪問,下一次訪問必須load新值。

四、Redis中如何解決快取穿透

如果發生了快取穿透,可以針對要查詢的資料,在Redis中插入一條資料,新增一個約定好的預設值,比如defaultNull。

比如你想透過某個id查詢某某訂單,Redis中沒有,MySQL中也沒有,此時,就可以在Redis中插入一條,存為defaultNull,下次再查詢就有了,因為是提前約定好的,前端也明白是啥意思,一切OK,歲月靜好。

效能最佳化2.0,新增快取後,程式的秒開率不升反降

五、使用loadingCache最佳化頁面載入

1、引入pom

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>31.0.1-jre</version>
</dependency>

2、初始化LoadingCache

private static LoadingCache<String, String> loadCache;

public static void initLoadingCache() {
    loadCache = CacheBuilder.newBuilder()
            // 併發級別設定為 10,是指可以同時寫快取的執行緒數
            .concurrencyLevel(10)
            // 寫重新整理,在get時可以同步或非同步快取的時間戳
            .refreshAfterWrite(5, TimeUnit.SECONDS)
            // 寫過期,在put時更新快取的時間戳
            .expireAfterWrite(10, TimeUnit.SECONDS)
            // 設定快取容器的初始容量為 10
            .initialCapacity(10)
            // 設定快取最大容量為 100,超過之後就會按照 LRU 演算法移除快取項
            .maximumSize(100)
            // 設定要統計快取的命中率
            .recordStats()
            // 指定 CacheLoader,快取不存在時,可自動載入快取
            .build(new CacheLoader<String, String>() {
                        @Override
                        public String load(String key) throws Exception {
                            // 自動載入快取的業務
                            return "error";
                        }
                    }
            );
}

3、最佳化5:透過LoadingCache快取模板資料,在編輯模板後,更新快取

查詢模板資訊後,透過loadCache.put(uuid, pdf);載入到記憶體中,在編輯模板時,更新快取過期時間,下次獲取模板PDF時,直接從LoadingCache快取中取,降低資料庫訪問壓力,perfect!!!

效能最佳化2.0,新增快取後,程式的秒開率不升反降

然並卵,這種情況是不適合快取的,因為模板pdf本來就是一個BLOB大資料,你把它放記憶體裡快取了,你告訴我,能放幾個?記憶體扛得住嗎?

來自 “ ITPUB部落格 ” ,連結:https://blog.itpub.net/70024924/viewspace-3003858/,如需轉載,請註明出處,否則將追究法律責任。

相關文章