Guava Cache 介紹

来源:https://www.cnblogs.com/wuyongyin/archive/2023/11/12/17788830.html
-Advertisement-
Play Games

Guava 是 Google 提供的一套 Java 工具包,而 Guava Cache 是該工具包中提供的一套完善的 JVM 級別高併發緩存框架;本文主要介紹它的相關功能及基本使用,文中所使用到的軟體版本:Java 1.8.0_341、Guava 32.1.3-jre。 1、簡介 緩存在很多情況下非 ...


Guava 是 Google 提供的一套 Java 工具包,而 Guava Cache 是該工具包中提供的一套完善的 JVM 級別高併發緩存框架;本文主要介紹它的相關功能及基本使用,文中所使用到的軟體版本:Java 1.8.0_341、Guava 32.1.3-jre。

1、簡介

緩存在很多情況下非常有用。例如,當某個值的計算或檢索代價很高,並且你需要在特定輸入下多次使用該值時,就應該考慮使用緩存。

Guava Cache 與 ConcurrentMap 類似,但並不完全相同。最基本的區別在於,ConcurrentMap 會一直保存所有添加到其中的元素,直到顯式地將它們刪除。而 Guava Cache 通常會配置自動刪除條目,以限制其記憶體占用。在某些情況下,即使不刪除條目,LoadingCache 也很有用,因為它具有自動載入條目的功能。

通常情況下,Guava Cache 適用於以下情況:

  • 你願意花費一些記憶體來提高速度。
  • 你預期某些鍵有時會被多次查詢。
  • 你的緩存不需要存儲超過記憶體容量的數據。(Guava Cache 是局限於應用程式運行期間的本地緩存。它們不會將數據存儲在文件或外部伺服器上。如果這不符合你的需求,可以考慮使用像Memcached 這樣的工具。)

如果你的情況符合上述每一點,那麼 Guava Cache 可能適合你。

註意:如果你不需要緩存的特性,ConcurrentHashMap 在記憶體效率方面更高——但是使用任何 ConcurrentMap 幾乎不可能複製大多數 Guava Cache 的特性。

2、數據載入

使用 Guava Cache 時,首先要問自己一個問題:是否有合理的預設函數來載入或計算需緩存的數據?如果是這樣,應該使用 CacheLoader。如果沒有,或者需要覆蓋預設函數,但仍然希望具有原子的“如果不存在則計算並獲取”語義,你應該將一個 Callable 對象傳遞給 get 方法。可以直接使用 Cache.put 方法插入元素,但更推薦自動載入數據,因為這樣可以更容易地推斷所有緩存內容的一致性。

2.1、CacheLoader

LoadingCache 是一個帶有 CacheLoader 的緩存。創建 CacheLoader 很容易,只需要實現方法 V load(K key) throws Exception 即可。

LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<Long, String>() {
            @Override
            public String load(Long key) throws Exception {
                //TODO: 根據業務載入數據
                return RandomStringUtils.randomAlphanumeric(10);
            }
        });
try {
    log.info(loadingCache.get(1L));
} catch (ExecutionException e) {
    e.printStackTrace();
}

LoadingCache 使用 get(K) 方法來獲取數據。該方法要麼返回已緩存的值,要麼使用 CacheLoader 來原子地載入一個新值到緩存中。由於 CacheLoader 可能會拋出異常,LoadingCache.get(K) 方法會拋出 ExecutionException 異常。(如果 CacheLoader 拋出未經檢查異常,get(K) 方法將拋出包裝異常 UncheckedExecutionException)。也可以選擇使用 getUnchecked(K) 方法,它將所有異常都包裝在UncheckedExecutionException 中,但如果底層的 CacheLoader 拋出已檢查異常,這可能導致意外行為。

LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build(new CacheLoader<Long, String>() {
            @Override
            public String load(Long key) {//拋出未檢查異常
                //TODO: 根據業務載入數據
                return RandomStringUtils.randomAlphanumeric(10);
            }
        });
log.info(loadingCache.getUnchecked(1L));

可以使用 getAll(Iterable<? extends K>)方法執行批量查詢。預設情況下,getAll 會為緩存中不存在的每個鍵單獨調用 CacheLoader.load 方法。當批量檢索比多個單獨查找更高效時,可以重寫CacheLoader.loadAll 以利用此功能。getAll(Iterable)的性能將相應提高。

2.2、Callable

所有 Guava Cache,無論是 LoadingCache 還是非 LoadingCache,都支持 get(K, Callable<V>) 方法。該方法返回與緩存中鍵相關聯的值,或者從指定的 Callable 計算它並將其添加到緩存中。在載入完成之前,與此緩存關聯的任何可觀察狀態都不會被修改。該方法為傳統的“如果有緩存,則返回;否則創建、緩存並返回”模式提供了一個簡單的替代方案。

Cache<Long, String> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .build();
try {
    String s = cache.get(1L, new Callable<String>() {
        @Override
        public String call() throws Exception {
            //TODO: 根據業務載入數據
            return RandomStringUtils.randomAlphanumeric(10);
        }
    });
    log.info(s);
} catch (ExecutionException e) {
    e.printStackTrace();
}

2.3、直接插入

可以使用 cache.put(key, value) 方法直接將值插入到緩存中。這會覆蓋緩存中指定鍵的的任何先前條目。還可以使用 Cache.asMap() 視圖公開的任何 ConcurrentMap 方法更改緩存。請註意,asMap 視圖上的任何方法都不會自動將條目載入到緩存中。此外,視圖上的原子操作在緩存自動載入的範圍之外運行,因此在使用 CacheLoader 或 Callable 載入值的緩存中,始終應優先選擇 Cache.get(K, Callable<V>) 而不是 Cache.asMap().putIfAbsent()。

3、數據淘汰

現實情況是,我們幾乎肯定沒有足夠的記憶體來緩存所有可能的內容。你必須決定:什麼時候不值得保留緩存條目?Guava提供了三種數據淘汰方式:基於大小的淘汰、基於時間的淘汰和基於引用的淘汰。

3.1、基於容量的淘汰

 如果你的緩存不應該超過一定大小,只需使用 CacheBuilder.maximumSize(long) 。緩存將嘗試淘汰最近未被使用或使用頻率很低的條目。警告:在達到限制之前,緩存可能會淘汰條目,通常是在緩存大小接近限制時。

或者,如果不同的緩存條目具有不同的“權重”——例如,如果你的緩存值具有截然不同的記憶體占用,你可以使用 CacheBuilder.weigher(Weigher) 來指定一個權重函數,並使用CacheBuilder.maximumWeight(long) 來設置最大的緩存權重。除了與 maximumSize 相同的註意事項外,請註意權重是在條目創建時計算的,並且在此後是靜態的。

Cache<Long, String> cache = CacheBuilder.newBuilder()
        .maximumWeight(100000)
        .weigher(new Weigher<Long, String>() {
            @Override
            public int weigh(Long key, String value) {
                return value.getBytes().length;
            }
        }).build();

3.2、基於時間的淘汰

CacheBuilder 提供了兩種基於時間淘汰數據的方法:

expireAfterAccess(long, TimeUnit):在最後一次讀取或寫入條目後,僅在指定的持續時間過去後才淘汰條目。需要註意的是,條目的淘汰順序類似於基於大小的淘汰策略。
expireAfterWrite(long, TimeUnit):在條目創建或最近一次替換值之後,僅在指定的持續時間過去後才淘汰條目。如果緩存數據在一段時間後變得過時,這種方式可能是可取的。

3.3、基於引用的淘汰

Guava 允許通過對鍵或值使用弱引用和對值使用軟引用來設置緩存,從而利用垃圾回收來淘汰數據。

  • CacheBuilder.weakKeys() 使用弱引用來存儲鍵。這意味著當鍵沒有其他(強或軟)引用時,條目可以被垃圾回收。由於垃圾回收僅依賴於記憶體地址相等性,這導致整個緩存使用(==)來比較鍵,而不是equals()方法。
  • CacheBuilder.weakValues() 使用弱引用來存儲值。這意味著當值沒有其他(強或軟)引用時,條目可以被垃圾回收。由於垃圾回收僅依賴於記憶體地址相等性,這導致整個緩存使用(==)來比較值,而不是equals()方法。
  • CacheBuilder.softValues() 使用軟引用來存儲值。以軟引用方式引用的對象會根據記憶體需求以全局最近最少使用的方式進行垃圾回收。由於使用軟引用可能會影響性能,我們通常建議使用更可預測的 maximum cache size 替代。使用 softValues() 將導致值使用(==)來比較,而不是 equals() 方法。

3.4、顯式刪除

在任何時候,你可以顯式地使緩存條目失效,而不是等待條目被淘汰。可以通過以下方式實現:

單個失效:使用 Cache.invalidate(key)
批量失效:使用 Cache.invalidateAll(keys)
全部失效:使用 Cache.invalidateAll()

3.5、刪除監聽器

你可以為緩存指定一個刪除監聽器(RemovalListener),以在條目被移除時執行某些操作,通過 CacheBuilder.removalListener(RemovalListener) 方法指定刪除監聽器。RemovalListener 會接收到一個RemovalNotification 對象,其中包含了 RemovalCause、鍵和值的信息。

需要註意的是,任何由 RemovalListener 拋出的異常都會被記錄(使用 Logger 時)並被忽略。

Cache<Long, String> cache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .removalListener(new RemovalListener<Long, String>() {
            @Override
            public void onRemoval(RemovalNotification<Long, String> notification) {
                log.info(notification.toString());
            }
        })
        .build();

警告:預設情況下,移除監聽器操作是同步執行的。由於緩存維護通常在正常緩存操作期間執行,因此移除監聽器可能會降低緩存的速度!如果需要移除監聽器,請使用RemovalListeners.asynchronous(RemovalListener, Executor) 方法來裝飾 RemovalListener,這樣可以以非同步的方式運行。

3.6、數據清理時機

使用 CacheBuilder 構建的緩存不會“自動”執行清理和逐出值,也不會在值過期後立即執行清理和逐出值,也不會執行任何類似操作。相反,它會在寫入操作期間執行少量維護,或者在偶爾的讀取操作期間(如果寫入很少)執行少量維護。

原因是:如果我們想要連續執行緩存維護,我們需要創建一個線程,它的操作將與用戶操作競爭共用鎖。此外,某些環境限制了線程的創建,這將使 CacheBuilder 在該環境中無法使用。

相反,我們將選擇權交到你手中。如果你的緩存是高吞吐量的,那麼你無需擔心執行緩存維護來清除過期條目等問題。如果你的緩存只偶爾進行寫操作,並且不想讓清理阻塞緩存讀取,你可以創建自己的維護線程,定期調用 Cache.cleanUp() 方法。

如果要為很少進行寫入操作的緩存安排定期緩存維護,請使用 ScheduledExecutorService

3.7、刷新

刷新(Refreshing)與淘汰(Eviction)並不完全相同。根據 LoadingCache.refresh(K) 的定義,刷新一個鍵會載入該鍵的新值,這可能是非同步的。在鍵正在刷新的過程中,舊值(如果存在)仍然會被返回,這與淘汰操作不同,淘汰操作會導致獲取操作等待直到新值載入完成。

如果在刷新過程中發生異常,舊值將被保留,異常將被記錄並忽略。

可以根據業務需要,重寫 CacheLoader 的 CacheLoader.reload(K, V) 方法來重新定義刷新操作;這允許你在計算新值時使用舊值。

LoadingCache<Integer, String> loadingCache = CacheBuilder.newBuilder()
        .maximumSize(1000)
        .refreshAfterWrite(1, TimeUnit.MINUTES)
        .build(new CacheLoader<Integer, String>() {
            @Override
            public String load(Integer key) throws Exception {
                //TODO: 根據業務載入數據
                return RandomStringUtils.randomAlphanumeric(10);
            }

            @Override
            public ListenableFuture<String> reload(Integer key, String oldValue) throws Exception {
                if (neverNeedsRefresh(key)) {//不需要刷新
                    return Futures.immediateFuture(oldValue);
                } else {
                    //非同步刷新
                    ListenableFutureTask<String> task = ListenableFutureTask.create(new Callable<String>() {
                        public String call() {
                            return RandomStringUtils.randomAlphanumeric(10);
                        }
                    });
                    executorService.execute(task);
                    return task;
                }
            }
        });

可以使用 CacheBuilder.refreshAfterWrite(long, TimeUnit) 為緩存添加自動定時刷新。與 expireAfterWrite 不同,refreshAfterWrite 會使一個鍵在指定的時間後變為可刷新狀態,但只有在查詢該條目時才會實際啟動刷新(如果 CacheLoader.reload 被實現為非同步,則查詢不會因刷新而變慢)。因此,可以在同一個緩存上同時指定 refreshAfterWrite 和 expireAfterWrite,以便在條目變為可刷新狀態時不會盲目地重置過期計時器,如果一個條目在變為可刷新狀態後沒有被查詢,它就允許過期。

4、特點

4.1、統計信息

使用 CacheBuilder.recordStats() 可以為 Guava Cache 打開統計信息收集功能。Cache.stats() 方法返回一個 CacheStats 對象,該對象提供了諸如以下統計信息:

還有許多其他的統計信息。這些統計信息在緩存調優中非常重要,我們建議在性能關鍵的應用程式中密切關註這些統計信息。

4.2、asMap

可以使用 Cache 的 asMap 視圖將任何緩存視為 ConcurrentMap,但是 asMap 視圖與緩存的交互需要一些說明。

  • cache.asMap() 包含當前載入在緩存中的所有條目。例如,cache.asMap().keySet() 包含當前載入的所有鍵。
  • asMap().get(key) 基本等同於 cache.getIfPresent(key),並且不會導致值被載入。這與 Map 的約定一致。
  • 訪問時間會被讀取和寫入操作重置(包括 Cache.asMap().get(Object) 和 Cache.asMap().put(K, V)),但不會被 containsKey(Object) 或其他操作所重置。因此,遍歷 cache.asMap().entrySet() 不會重置條目的訪問時間。

5、簡單使用

5.1、引入依賴

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

5.2、簡單使用

public static void main(String[] args) {
    LoadingCache<Long, String> loadingCache = CacheBuilder.newBuilder()
            .initialCapacity(1000)
            .maximumSize(10000)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .refreshAfterWrite(3, TimeUnit.MINUTES)
            .recordStats()
            .build(new CacheLoader<Long, String>() {
                @Override
                public String load(Long key) throws Exception {//拋出已檢查異常
                    //TODO: 根據業務載入數據
                    return RandomStringUtils.randomAlphanumeric(10);
                }
            });
    try {
        log.info(loadingCache.get(1L));
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
}

 

 

參考:https://github.com/google/guava/wiki/CachesExplained


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

-Advertisement-
Play Games
更多相關文章
  • 六、判斷(一) 1、關係運算符 1)每一個關係運算符得出的結果都是一個布爾類型值(true、flase或真、假或1、0) 關係運算符 說明 關係運算符 說明 > 大於 >= 大於等於 < 小於 <= 小於等於 == 等於 != 不等於 #include <iostream> int main() { ...
  • 常見鎖介紹 synchronized鎖的八中情況 package com.shaonian.juc.more_thread_lock; import java.util.concurrent.TimeUnit; class Phone { public static synchronized voi ...
  • Python MySQL 限制結果 限制結果數量 示例 1: 獲取您自己的 Python 伺服器 選擇 "customers" 表中的前 5 條記錄: import mysql.connector mydb = mysql.connector.connect( host="localhost", u ...
  • 操作系統 :Windows 10_x64 python版本 :3.9.2 pymysql版本: 1.0.2 MySQL版本: 5.7.38 之前寫過一篇關於python操作mysql資料庫的文章: https://www.cnblogs.com/MikeZhang/p/pythonOptMysql2 ...
  • 寫在前面 目前已經上班快兩個月了,對現在的工作很滿意,甚至更喜歡這的氛圍吧。 如題所示,從今年5月開始,發生的所有事,都完全超出了我自己可以承受的範圍,好在這一切都過去了,真的感謝上天安排,讓我能更加確信自己要的是什麼,以後該怎麼生活。 爸爸被診斷為肺癌 我每年都會帶父母去做體檢,因為去年疫情全面放 ...
  • 本文適用範圍 主要適用於debug python 程式,尤其是深度學習剛入門需要使用remote 連接到linux進行程式運行,想調試一下的同學。 當然非深度學習也可以參考食用本文哈哈哈。 極速入門版 提前準備:代碼倉庫已經拉取到linux上面,且已經知道運行的方式。 比如: 項目的啟動命令為:py ...
  • 五、位運算 ​ 位運算主要計算記憶體中每個小格的數據 1、輸出二進位內容 頭文件調用 語法 示例 include <bitser> std::bitset<要顯示的二進位位數>(要顯示的變數) std::cout << std::bitset<16>(a); //二進位內容輸出 #include <i ...
  • 在 Java 中,有四種方法可以獲取當前正在執行方法體的方法名稱,分別是: 使用 Thread.currentThread().getStackTrace() 方法 使用異常對象的 getStackTrace() 方法 使用匿名內部類的 getClass().getEnclosingMethod() ...
一周排行
    -Advertisement-
    Play Games
  • 示例項目結構 在 Visual Studio 中創建一個 WinForms 應用程式後,項目結構如下所示: MyWinFormsApp/ │ ├───Properties/ │ └───Settings.settings │ ├───bin/ │ ├───Debug/ │ └───Release/ ...
  • [STAThread] 特性用於需要與 COM 組件交互的應用程式,尤其是依賴單線程模型(如 Windows Forms 應用程式)的組件。在 STA 模式下,線程擁有自己的消息迴圈,這對於處理用戶界面和某些 COM 組件是必要的。 [STAThread] static void Main(stri ...
  • 在WinForm中使用全局異常捕獲處理 在WinForm應用程式中,全局異常捕獲是確保程式穩定性的關鍵。通過在Program類的Main方法中設置全局異常處理,可以有效地捕獲並處理未預見的異常,從而避免程式崩潰。 註冊全局異常事件 [STAThread] static void Main() { / ...
  • 前言 給大家推薦一款開源的 Winform 控制項庫,可以幫助我們開發更加美觀、漂亮的 WinForm 界面。 項目介紹 SunnyUI.NET 是一個基於 .NET Framework 4.0+、.NET 6、.NET 7 和 .NET 8 的 WinForm 開源控制項庫,同時也提供了工具類庫、擴展 ...
  • 說明 該文章是屬於OverallAuth2.0系列文章,每周更新一篇該系列文章(從0到1完成系統開發)。 該系統文章,我會儘量說的非常詳細,做到不管新手、老手都能看懂。 說明:OverallAuth2.0 是一個簡單、易懂、功能強大的許可權+可視化流程管理系統。 有興趣的朋友,請關註我吧(*^▽^*) ...
  • 一、下載安裝 1.下載git 必須先下載並安裝git,再TortoiseGit下載安裝 git安裝參考教程:https://blog.csdn.net/mukes/article/details/115693833 2.TortoiseGit下載與安裝 TortoiseGit,Git客戶端,32/6 ...
  • 前言 在項目開發過程中,理解數據結構和演算法如同掌握蓋房子的秘訣。演算法不僅能幫助我們編寫高效、優質的代碼,還能解決項目中遇到的各種難題。 給大家推薦一個支持C#的開源免費、新手友好的數據結構與演算法入門教程:Hello演算法。 項目介紹 《Hello Algo》是一本開源免費、新手友好的數據結構與演算法入門 ...
  • 1.生成單個Proto.bat內容 @rem Copyright 2016, Google Inc. @rem All rights reserved. @rem @rem Redistribution and use in source and binary forms, with or with ...
  • 一:背景 1. 講故事 前段時間有位朋友找到我,說他的窗體程式在客戶這邊出現了卡死,讓我幫忙看下怎麼回事?dump也生成了,既然有dump了那就上 windbg 分析吧。 二:WinDbg 分析 1. 為什麼會卡死 窗體程式的卡死,入口門檻很低,後續往下分析就不一定了,不管怎麼說先用 !clrsta ...
  • 前言 人工智慧時代,人臉識別技術已成為安全驗證、身份識別和用戶交互的關鍵工具。 給大家推薦一款.NET 開源提供了強大的人臉識別 API,工具不僅易於集成,還具備高效處理能力。 本文將介紹一款如何利用這些API,為我們的項目添加智能識別的亮點。 項目介紹 GitHub 上擁有 1.2k 星標的 C# ...