Java 緩存精要
實現更低延遲、降低成本並賦能智能體架構
作者:GRANVILLE BARNETT 架構師,HAZELCAST
緩存技術在系統中的作用日益重要,對於大規模解鎖眾多用例至關重要。幾十年來,緩存已實現低成本、可擴展地訪問會話狀態和數據存儲等信息。更現代的緩存用例正在實現低成本、可擴展的工具鏈,並在智能體架構中實現嵌入生成,這正在解鎖下一代系統創新。
本參考資料卡介紹了使用 Java 的 JCache(Java 臨時緩存 API)將緩存融入系統的方法。文中首先討論了緩存的基礎知識,然後通過代碼示例簡要介紹了 JCache API,最後總結了緩存部署架構。
緩存概述
緩存是先前計算結果的一個存儲,以便可以省略後續計算。理解緩存最簡單的方式是將其視為鍵值存儲:對於給定的輸入(鍵),輸出(值)代表先前基於該輸入計算出的結果。
緩存命中表示特定數據存在於緩存中,這種情況下可以使用其值。否則,就會發生緩存未命中,此時需要執行相關計算並將其輸出放入緩存。緩存未命中的代價可能除了昂貴的計算操作外,還涉及網絡通信。
圖 1: 簡化的緩存命中/未命中流程
採用緩存是為了減少延遲並降低運營成本,幾十年來對於實現眾多類別的應用程序至關重要。緩存數據的例子包括 Web 應用程序的會話狀態、數據庫查詢結果、網頁渲染結果,以及來自通用網絡和計算成本高昂的操作的結果。
緩存的一個更現代的用途是在 AI 領域。在這裏,緩存的使用減少了昂貴的 API 調用(例如,嵌入生成),並最大限度地減少了智能體架構中智能體之間的對話斷續(例如,由於工具調用和網絡通信所致),從而解鎖了新一波的解決方案和用户體驗。
緩存可以駐留在進程內,作為客户端-服務器架構的一部分存在於服務中,或者是兩者的結合。此外,緩存的部署通常可以組合。例如,應用程序可能與位於同一數據中心的緩存服務通信,而數據中心的本地緩存又是跨越多個數據中心的緩存的緩存。這種靈活性,加上緩存所支持的應用類別,使得緩存在過去幾十年中成為一種主導的抽象概念。
本參考資料卡的剩餘部分將討論 JCache——Java 用於將緩存融入應用程序的抽象——首先簡要概述您將經常使用的類,然後深入探討 JCache 更廣泛功能所提供的特性。最後,我們將總結緩存部署策略。
JCACHE 精要
JCache 在 Java 規範請求(JSR)107 中引入,並提供了一套關於緩存的抽象。JCache 有兩個突出的特性:
- JCache 是一個規範。 JSR 是由專家組設計和提交,並最終由 Java 社區過程執行委員會批准的規範。因為 JCache 是一個規範,所以它與那些 API 頻繁變化的實現隔離開來。
- JCache 是提供商獨立的。 JCache 作為規範的一個副作用是,緩存解決方案提供商可以通過實現其暴露的服務提供程序接口(SPI)來與 JCache 集成。這為系統設計者提供了靈活性並避免了供應商鎖定。
以下是一個簡單的 JCache 示例,以便理解其使用方式。javax.cache 依賴項的獲取方式可以在此處找到。
import java.util.Map;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;
public class App {
public static void main(String[] args) {
CachingProvider cachingProvider = Caching.getCachingProvider(); // (1)
CacheManager cacheManager = cachingProvider.getCacheManager(); // (2)
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>(); // (3)
Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig); // (4)
cache.put("England", "London"); // (5)
cache.putAll(Map.of("France", "Paris", "Ireland", "Dublin")); // (6)
assert cache.get("England").equals("London"); // (7)
assert cache.get("Italy") == null; // (8)
}
}
對上述示例的簡要説明: (1) 獲取底層緩存提供程序的句柄 (2) 管理緩存的生命週期(例如,創建和銷燬緩存) (3) 允許啓用/禁用緩存的特定功能(例如,統計信息、條目監聽器) (4) 創建由緩存提供程序支持的緩存 (5) 在緩存中放入單個鍵值條目 (6) 將鍵值條目放入緩存 (7) 斷言緩存條目的存在 (8) 斷言某個條目不在緩存中
本節的剩餘部分將更詳細地討論上述示例中引入的抽象,以及您將經常遇到的相關類的其他方法。
javax.cache.spi.CachingProvider 構成了 JCache SPI,緩存提供者可以與之集成。您將使用的最常見功能是獲取對 CacheManager 的引用。我們稍後將討論 Caching。
getCacheManager 是 getCacheManager 變體中最簡單的一個。這將根據提供者的默認設置獲取一個 CacheManager。可以使用 javax.cache.CacheManager 創建和銷燬緩存:
createCache創建一個具有給定名稱和配置的緩存。destroyCache銷燬具有給定名稱的緩存。
javax.cache.Cache 是對提供者緩存的抽象,並暴露了少量用於查詢和變更緩存項的操作:
put和putAll將條目放入緩存。請注意,這些方法不返回與正在放入的鍵先前關聯的任何值。containsKey測試鍵是否存在於緩存中。get和getAll返回與指定鍵關聯的值。remove和removeAll從緩存中移除項。
JCACHE 包
在本節中,我們將快速概述 javax.cache 更廣泛包結構中的一些重要接口,並提供常用功能的示例。我們可以參考文檔來瀏覽其內容的詳盡列表。
圖 2:javax.cache 的組成包
JAVAX.CACHE
通用管理(CacheManager)和與緩存交互(Cache)的設施位於 javax.cache 包內。除了初始配置之外,除非您想為緩存添加額外功能,否則僅使用此包中的類型就可以完成很多工作。例如,"JCache 精要"部分介紹中的示例用法主要使用了 javax.cache 中定義的接口。
JAVAX.CACHE.CONFIGURATION
在創建緩存期間,您可能希望添加功能,例如啓用統計信息或通讀緩存。此包提供了一個 Configuration 接口和一個實現 MutableConfiguration,可用於此類目的。
// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig);
JAVAX.CACHE.EXPIRY
有時您希望駐留在緩存中的項過期。例如,我們可能有一個家庭保險報價的緩存,有效期為 24 小時。在這種情況下,我們可以使用過期策略如下:
// ...
MutableConfiguration<String, Double> cacheConfig = new MutableConfiguration<String, Double>();
cacheConfig.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_DAY));
Cache<String, Double> cache = cacheManager.createCache("insurance-home-quotes", cacheConfig);
cache.put(quote.getId(), quote.getValue());
javax.cache.expiry 包提供了額外的過期策略,可能對其他場景有用。例如,AccessedExpiryPolicy 允許基於緩存條目的最後訪問時間附加過期設置。
JAVAX.CACHE.EVENT
JCache 的一個強大功能是能夠訂閲緩存事件。例如,我們可能希望在創建或刪除緩存條目後運行某些領域邏輯。javax.cache.event 包提供了實現此功能的抽象,特別是訂閲緩存創建、更新、過期和移除的能力。以下基本示例在緩存條目創建後運行某些領域邏輯:
// ...
CacheEntryCreatedListener<String, String> createdListener = new CacheEntryCreatedListener<String, String>() {
@Override
public void onCreated(Iterable<CacheEntryEvent<? extends String, ? extends String>> events) throws CacheEntryListenerException {
for (var c : events) {
performDomainLogic(c);
}
}
};
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
MutableCacheEntryListenerConfiguration<String, String> listenerConfig = new MutableCacheEntryListenerConfiguration<>(() -> createdListener, null, false, true); // 請參閲文檔
cacheConfig.addCacheEntryListenerConfiguration(listenerConfig);
Cache<String, String> cache = cacheManager.createCache("events", cacheConfig);
cache.put("key", "value"); // 調用創建監聽器
JAVAX.CACHE.PROCESSOR
JCache 的一個強大組件是能夠使用 EntryProcessor 將計算移至數據所在處,然後以編程方式調用該計算。當使用在分佈式系統(例如,Hazelcast)內託管其緩存的提供者時,這尤其強大,因為它以很少的投入為分佈式計算提供了一個簡單的入口點。以下是一個 EntryProcessor 的簡單示例,它將 UUID 附加到緩存條目:
// ...
class AppendUuidEntryProcessor implements EntryProcessor<String, String, String> {
@Override
public String process(MutableEntry<String, String> entry, Object... arguments) throws EntryProcessorException {
if (entry.exists()) {
String newValue = entry.getValue() + "-" + UUID.randomUUID();
entry.setValue(newValue);
return newValue;
}
return null;
}
}
// ...
cache.invoke(key, new AppendUuidEntryProcessor())
JAVAX.CACHE.MANAGEMENT
JCache 提供的管理鈎子非常強大且易於啓用。例如,下面的小代碼片段暴露了由 Java 管理擴展(JMX)規範定義的託管 Bean。這使得諸如 jconsole 和 JDK Mission Control 之類的 JMX 客户端能夠查看緩存配置和統計信息(例如,命中和未命中百分比、平均獲取和放置時間)。
// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("management", cacheConfig);
// ...
JAVAX.CACHE.SPI
"JCache 精要"部分提供的示例省略了我們如何註冊緩存提供者,即使用 JCache API 與我們的應用程序交互的緩存宿主服務。這就是 JCache 的 SPI 組件發揮作用的地方。
實現這一點有兩個組成部分:
- 將我們的緩存提供者添加到類路徑中
- 告訴 JCache 使用該提供者
第一步很簡單:只需添加對任何符合 JSR 107 標準的提供者的依賴。
第二步有幾種通用的方法:
- 我們可以通過調用
Caching#getCachingProvider(...)的某個變體(以及其他方法)來告訴 JCache。 - 我們可以提供一個
META-INF/services/javax.cache.spi.CachingProvider文件,並讓其指定提供者實現。指定的提供者是您的提供者的緩存提供者實現的完全限定名稱。 - 我們可以使用
Caching#getCachingProvider();但是,最好明確限定要使用的提供者,因為您的類路徑上可能有多個提供者,這會拋出javax.cache.CacheException。
例如,以下代碼使用 CachingProvider Caching.getCachingProvider(String) 指定 Hazelcast 為提供者:
CachingProvider cachingProvider = Caching.getCachingProvider("com.hazelcast.cache.HazelcastCachingProvider");
CacheManager cacheManager = cachingProvider.getCacheManager();
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
Cache<String, String> cache = cacheManager.createCache("spi-example", cacheConfig);
cache.put("k", "v");
JAVAX.CACHE.ANNOTATION
JCache 定義了許多註解,用於集成到上下文和依賴注入環境中。Spring Framework 原生支持 JCache 註解。我們可以參考 JCache 文檔以獲取更多信息。
JAVAX.CACHE.INTEGRATION
javax.cache.integration 包提供了 CacheLoader(需要通讀)和 CacheWriter(需要通寫)。CacheLoader 在將數據讀入緩存時使用——例如 Cache#loadAll(...)。CacheWriter 可以作為一個集成點,將緩存變更(例如,寫入、刪除)傳播到外部存儲服務。
緩存部署
JCache 沒有緩存部署策略的概念;它僅僅是緩存提供者之上的一個 API。然而,不同的提供者支持不同類型的緩存部署。請考慮哪種緩存部署對您的應用程序有意義,並由此反向確定合適的緩存提供者。
圖 3: 緩存部署示例
請注意,一些緩存提供者可能支持所有這三種緩存部署,而其他提供者可能不支持。
本節的剩餘部分討論圖 2 中所示的常見緩存部署:
- 嵌入式 – 緩存與應用程序位於同一進程中。
- 客户端-服務器 – 緩存託管在獨立的服務中,客户端與該服務通信以確定緩存駐留。
- 嵌入式/客户端-服務器 – 這是一種混合模式,整個緩存託管在不同的服務上,但客户端在同一進程中擁有一個較小的本地緩存。
重要的是要注意,上述緩存部署並非互斥的;它們可以通過多種方式組合以滿足應用程序需求。
最簡單的緩存部署是讓緩存與應用程序駐留在同一進程中,這樣做的好處是提供低延遲的緩存訪問。嵌入式緩存不能在應用程序之間共享,並且在應用程序重啓或故障時,其託管(它們所需的資源)和重建成本可能很高。
客户端-服務器緩存部署將緩存託管在與客户端不同的服務中。緩存服務允許通過跨服務複製來滿足容錯需求,提供更大的容量、更多的可擴展性選項,以及跨應用程序共享緩存的能力。客户端-服務器模型的主要缺點在於客户端緩存查詢期間網絡通信的成本。
混合嵌入式/客户端-服務器部署是指我們擁有一個嵌入式緩存,它包含來自服務緩存條目的一個子集,作為應用程序緩存請求的副作用被填充。在這裏,客户端可以對頻繁訪問的數據(或表現出特定訪問模式的數據)實現低延遲的緩存命中,並省去與緩存服務通信所帶來的網絡通信開銷。如果嵌入式緩存過期,一些提供者會負責使用服務託管的緩存來更新它們。
結論
本參考資料卡介紹了緩存以及如何將其與 Java 的 JCache API 一起使用。JCache API 直觀、強大,並且由於其是一個規範而避免了供應商鎖定,為架構師和系統設計者提供了他們所需的靈活性。這種靈活性在我們進入基於智能體架構的新一代創新時尤為重要,其中緩存對於工具鏈和嵌入生成至關重要。
作者:GRANVILLE BARNETT, 架構師,HAZELCAST Granville Barnett 擁有計算機科學博士學位,是擁有超過 15 年經驗的分佈式系統專家。他目前是 Hazelcast 的架構師,此前曾在 HP Labs 和 Microsoft 任職。Granville 擁有多項美國專利,並發表了關於程序驗證主題的研究。
附加資源:
- 《Java 應用程序容器化與部署》,作者 Mark Heckler,DZone 參考資料卡
- Java 社區進程
【注】本文譯自:Java Caching Essentials