JAVA緩存規範 —— 雖遲但到的JCache API與天生不俗的Spring Cache

来源:https://www.cnblogs.com/softwarearch/archive/2022/11/15/16892344.html
-Advertisement-
Play Games

業界各大廠商或開源團隊都會構建並提供一些緩存框架組件提供給開發者按需選擇,這裡就會涉及到一個標準規範的遵循問題,本文我們一起聊聊JCache API規範與SpringCache規範。 ...


大家好,又見面了。


本文是筆者作為掘金技術社區簽約作者的身份輸出的緩存專欄系列內容,將會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關註以獲取後續更新。


有詩雲“紙上得來終覺淺,絕知此事要躬行”,在上一篇文章《手寫本地緩存實戰2—— 打造正規軍,構建通用本地緩存框架》中,我們一起論證並逐步實現了一套簡化版本的通用本地緩存框架,併在過程中逐步剖析了緩存設計關鍵要素的實現策略。本篇文章中,我們一起來聊一聊緩存框架實現所需要遵循的規範。

為何需要規範

上一章中構建的最簡化版本的緩存框架,雖然可以使用,但是也存在一個問題,就是它對外提供的實現介面都是框架根據自己的需要而自定義的。這樣一來,項目集成了此緩存框架,後續如果想要更換緩存框架的時候,業務層面的改動會比較大。 —— 因為是自定義的框架介面,無法基於里氏替換原則來進行靈活的更換。

在業界各大廠商或者開源團隊都會構建並提供一些自己實現的緩存框架或者組件,提供給開發者按需選擇使用。如果大家都是各自閉門造車,勢必導致業務中集成並使用某一緩存實現之後,想要更換緩存實現組件會難於登天。

千古一帝秦始皇統一天下後,頒佈了書同文、車同軌等一系列法規制度,使得所有的車輛都遵循統一的軸距,然後都可以在官道上正常的通行,大大提升了流通性。而正所謂“國有國法、行有行規”,為了保證緩存框架的通用性、提升項目的可移植性,JAVA行業也迫切需要這麼一個緩存規範,來約束各個緩存提供商給出的緩存框架都遵循相同的規範介面,業務中按照標準介面進行調用,無需與緩存框架進行深度耦合,使得緩存組件的更換成為一件簡單點的事情。

在JAVA的緩存領域,流傳比較廣泛的主要是JCache APISpring Cache兩套規範,下麵就一起來看下。

雖遲但到的JSR107 —— JCache API

提到JAVA中的“行業規矩”,JSR是一個繞不開的話題。它的全稱為Java Specification Requests,意思是JAVA規範提案。在該規範標準中,有公佈過一個關於JAVA緩存體系的規範定義,也即JSR 107規範(JCache API),主要明確了JAVA中基於記憶體進行對象緩存構建的一些要求,涵蓋記憶體對象的創建查詢更新刪除一致性保證等方面內容。

JSR107規範早在2012年時草案就被提出,但卻直到2014年才正式披露首個規範版本,也即JCache API 1.0.0版本,至此JAVA領域總算是有個正式的關於緩存的官方規範要求。

揭秘JSR107 —— JCache API內容探究

JSR107規範具體的要求形式,都以介面的形式封裝在javax.cache包中進行提供。我們要實現的緩存框架需要遵循該規範,也就是需要引入javax.cache依賴包,並實現其中提供的相關介面即可。對於使用maven構建的項目中,可以在pom.xml中引入javax.cache依賴:

<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
    <version>1.1.1</version>
</dependency>

JCache API規範中,定義的緩存框架相關介面類之間的關係邏輯梳理如下:

我們要實現自己的本地緩存框架,也即需要實現上述各個介面。對上述各介面類的含義介紹說明如下:

介面類 功能定位描述
CachingProvider SPI介面,緩存框架的載入入口。每個Provider中可以持有1個或者多個CacheManager對象,用來提供不同的緩存能力
CacheManager 緩存管理器介面,每個緩存管理器負責對具體的緩存容器的創建與管理,可以管理1個或者多個不同的Cache對象
Cache Cache緩存容器介面,負責存儲具體的緩存數據,可以提供不同的容器能力
Entry Cache容器中存儲的key-value鍵值對記錄

作為通用規範,這裡將CachingProvider定義為了一個SPI介面Service Provider Interface,服務提供介面),主要是藉助JDK自帶的服務提供發現能力,來實現按需載入各自實現的功能邏輯,有點IOC的意味。這樣設計有一定的好處:

  • 對於框架

需要遵循規範,提供上述介面的實現類。然後可以實現熱插拔,與業務解耦。

  • 對於業務

先指定需要使用的SPI的具體實現類,然後業務邏輯中便無需感知緩存具體的實現,直接基於JCache API通用介面進行使用即可。後續如果需要更換緩存實現框架,只需要切換下使用的SPI的具體實現類即可。

根據上述介紹,一個基於JCache API實現的緩存框架在實際項目中使用時的對象層級關係可能會是下麵這種場景(假設使用LRU策略存儲部門信息、使用普通策略存儲用戶信息):

那麼如何去理解JCache API中幾個介面類的關係呢?

幾個簡單的說明:

  1. CachingProvider並無太多實際邏輯層面的功能,只是用來基於SPI機制,方便項目中集成插拔使用。內部持有CacheManager對象,實際的緩存管理能力,由CacheManager負責提供。

  2. CacheManager負責具體的緩存管理相關能力實現,實例由CachingProvider提供並持有,CachingProvider可以持有一個或者多個不同的CacheManager對象。這些CacheManager對象可以是相同類型,也可以是不同類型,比如我們可以實現2種緩存框架,一種是基於記憶體的緩存,一種是基於磁碟的緩存,則可以分別提供兩種不同的CacheManager,供業務按需調用。

  3. Cache是CacheManager負責創建並管理的具體的緩存容器,也可以有一個或者多個,如業務中會涉及到為用戶列表和部門列表分別創建獨立的Cache存儲。此外,Cache容器也可以根據需要提供不同的Cache容器類型,以滿足不同場景對於緩存容器的不同訴求,如我們可以實現一個類似HashMap的普通鍵值對Cache容器,也可以提供一個基於LRU淘汰策略的Cache容器。

至此呢,我們釐清了JCache API規範的大致內容。

插敘 —— SPI何許人也

按照JSR107規範試編寫緩存具體能力時,我們需要實現一個SPI介面的實現類,然後由JDK提供的載入能力將我們擴展的緩存服務載入到JVM中供使用。

提到API我們都耳熟能詳,也就是我們常規而言的介面。但說起SPI也許很多小伙伴就有點陌生了。其實SPI也並非是什麼新鮮玩意,它是JDK內置的一種服務的提供發現載入機制。按照JAVA的面向對象編碼的思想,為了降低代碼的耦合度、提升代碼的靈活性,往往需要利用好抽象這一特性,比如一般會比較推薦基於介面進行編碼、而儘量避免強依賴某個具體的功能實現類 —— 這樣才能讓構建出的系統具有更好的擴展性,更符合面向對象設計原則中的里式替換原則。SPI便是為了支持這一訴求而提供的能力,它允許將介面具體的實現類交由業務或者三方進行獨立構建,然後載入到JVM中以供業務進行使用。

為了這一點,我們需要在resource/META-INF/services目錄下新建一個文件,文件名即為SPI介面名稱javax.cache.spi.CachingProvider,然後在文件內容中,寫入我們要註入進入的我們自己的Provider實現類:

這樣,我們就完成了將我們自己的MyCachingProvider功能註入到系統中。在業務使用時,可以通過Caching.getCachingProvider()獲取到註入的自定義Provider

public static void main(String[] args) {
    CachingProvider provider =  Caching.getCachingProvider();
    System.out.println(provider);
}

從輸出的結果可以看出,獲取到了自定義的Provider對象:

com.veezean.skills.cache.fwk.MyCachingProvider@7adf9f5f

獲取到Provider之後,便可以進一步的獲取到Manager對象,進而業務層面層面可以正常使用。

JCache API規範的實現

JSR作為JAVA領域正統行規,制定的時候往往考慮到各種可能的靈活性與通用性。作為JSR中根正苗紅的JCache API規範,也沿襲了這一風格特色,框架介面的定義與實現也非常的豐富,幾乎可以擴展自定義任何你需要的處理策略。 —— 但恰是這一點,也讓其整個框架的介面定義過於重量級。對於緩存框架實現者而言,遵循JCache API需要實現眾多的介面,需要做很多額外的實現處理。

比如,我們實現CacheManager的時候,需要實現如下這麼多的介面:

public class MemCacheManager implements CacheManager {
    private CachingProvider cachingProvider;
    private ConcurrentHashMap<String, Cache> caches;
    public MemCacheManager(CachingProvider cachingProvider, ConcurrentHashMap<String, Cache> caches) {
        this.cachingProvider = cachingProvider;
        this.caches = caches;
    }
    @Override
    public CachingProvider getCachingProvider() {
    }
    @Override
    public URI getURI() {
    }
    @Override
    public ClassLoader getClassLoader() {
    }
    @Override
    public Properties getProperties() {
    }
    @Override
    public <K, V, C extends Configuration<K, V>> Cache<K, V> createCache(String s, C c) throws IllegalArgumentException {
    }
    @Override
    public <K, V> Cache<K, V> getCache(String s, Class<K> aClass, Class<V> aClass1) {
    }
    @Override
    public <K, V> Cache<K, V> getCache(String s) {
    }
    @Override
    public Iterable<String> getCacheNames() {
    }
    @Override
    public void destroyCache(String s) {
    }
    @Override
    public void enableManagement(String s, boolean b) {
    }
    @Override
    public void enableStatistics(String s, boolean b) {
    }
    @Override
    public void close() {
    }
    @Override
    public boolean isClosed() {
    }
    @Override
    public <T> T unwrap(Class<T> aClass) {
    }
}

長長的一摞介面等著實現,看著都令人上頭,作為緩存提供商,便需要按照自己的能力去實現這些介面,以保證相關緩存能力是按照規範對外提供。也正是因為JCache API這種不接地氣的表現,讓其雖是JAVA 領域的正統規範,卻經常被束之高閣,淪落成為了一種名義規範。業界主流的本地緩存框架中,比較出名的當屬Ehcache了(當然,Spring4.1中也增加了對JSR規範的支持)。此外,Redis的本地客戶端Redisson也有實現全套JCache API規範,用戶可以基於Redisson調用JCache API的標準介面來進行緩存數據的操作。

JSR107提供的註解操作方法

前面提到了作為供應商想要實現JSR107規範的時候會比較複雜,需要做很多自己的處理邏輯。但是對於業務使用者而言,JSR107還是比較貼心的。比如JSR107中就將一些常用的API方法封裝為註解,利用註解來大大簡化編碼的複雜度,降低緩存對於業務邏輯的侵入性,使得業務開發人員可以更加專註於業務本身的開發。

JSR107規範中常用的一些緩存操作註解方法梳理如下麵的表格:

註解 含義說明
@CacheResult 將指定的keyvalue映射內容存入到緩存容器中
@CachePut 更新指定緩存容器中指定key值緩存記錄內容
@CacheRemove 移除指定緩存容器中指定key值對應的緩存記錄
@CacheRemoveAll 字面含義,移除指定緩存容器中的所有緩存記錄
@CacheKey 作為介面參數前面修飾,用於指定特定的入參作為緩存key值的組成部分
@CacheValue 作為介面參數前面的修飾,用於指定特定的入參作為緩存value

上述註解主要是添加在方法上面,用於自動將方法的入參與返回結果之間進行一個映射與自動緩存,對於後續請求如果命中緩存則直接返回緩存結果而無需再次執行方法的具體處理,以此來提升介面的響應速度與承壓能力。

比如下麵的查詢介面上,通過@CacheResult註解可以將查詢請求與查詢結果緩存起來進行使用:

@CacheResult(cacheName = "books")
public Book findBookByName(@CacheKey String bookName) {
    return bookDao.queryByName(bookName);
}

Book信息發生變更的時候,為了保證緩存數據的準確性,需要同步更新緩存內容。可以通過在更新方法上面添加@CachePut介面即可達成目的:

@CachePut(cacheName = "books")
public void updateBookInfo(@CacheKey String bookName, @CacheValue Book book) {
    bookDao.updateBook(bookName, book);
}

這裡分別適用了@CacheKey@CacheValue指定了需要更新的緩存記錄key值,以及需要將其更新為的新的value值。

同樣地,藉助註解@CacheRemove可以完成對應緩存記錄的刪除:

@CacheRemove(cacheName = "books")
public void deleteBookInfo(@CacheKey String bookName) {
    bookDao.deleteBookByName(bookName)
}

愛屋及烏 —— Spring框架制定的Cache規範

JSR 107(JCache API)規範的誕生可謂是一路坎坷,拖拖拉拉直到2014年才發佈了首個1.0.0版本規範。但是在JAVA界風頭無兩的Spring框架早在2011年就已經在其3.1版本中提供了緩存抽象層的規範定義,並藉助Spring的優秀設計與良好生態,迅速得到了各個軟體開發團體的青睞,各大緩存廠商也陸續提供了符合Spring Cache規範的自家緩存產品。

Spring Cache並非是一個具體的緩存實現,而是和JSR107類似的一套緩存規範,基於註解並可實現與Spring的各種高級特性無縫集成,受到了廣泛的追捧。各大緩存提供商幾乎都有基於Spring Cache規範進行實現的緩存組件。比如後面我們會專門介紹的Guava CacheCaffeine Cache以及同樣支持JSR107規範的Ehcache等等。

得力於Spring在JAVA領域無可撼動的地位,造就了Spring Cache已成為JAVA緩存領域的“事實標準”,深有“功高蓋主”的味道。

Spring Cache使用不同緩存組件

如果要基於Spring Cache規範來進行緩存的操作,首先在項目中需要引入此規範的定義:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

這樣,在業務代碼中,就可以使用Spring Cache規範中定義的一些註解方法。前面有提過,Spring Cache只是一個規範聲明,可以理解為一堆介面定義,而並沒有提供具體的介面功能實現。具體的功能實現,由業務根據實際選型需要,引入相應緩存組件的jar庫文件依賴即可 —— 這一點是Spring框架中極其普遍的一種做法。

假如我們需要使用Guava Cache來作為我們實際緩存能力提供者,則我們只需要引入對應的依賴即可:

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

這樣一來,我們便實現了使用Guava cache作為存儲服務提供者、且基於Spring Cache介面規範進行緩存操作。Spring作為JAVA領域的一個相當優秀的框架,得益於其優秀的封裝設計思想,使得更換緩存組件也顯得非常容易。比如現在想要將上面的Guava cache更換為Caffeine cache作為新的緩存能力提供者,則業務代碼中將依賴包改為Caffeine cache並簡單的做一些細節配置即可:

<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.1</version>
</dependency>

這樣一來,對於業務使用者而言,可以方便的進行緩存具體實現者的替換。而作為緩存能力提供商而言,自己可以輕易的被同類產品替換掉,所以也鞭策自己去提供更好更強大的產品,鞏固自己的地位,也由此促進整個生態的良性演進

Spring Cache規範提供的註解

需要註意的是,使用Spring Cache緩存前,需要先手動開啟對於緩存能力的支持,可以通過@EnableCaching註解來完成。

除了@EnableCaching,在Spring Cache中還定義了一些其它的常用註解方法,梳理歸納如下:

註解 含義說明
@EnableCaching 開啟使用緩存能力
@Cacheable 添加相關內容到緩存中
@CachePut 更新相關緩存記錄
@CacheEvict 刪除指定的緩存記錄,如果需要清空指定容器的全部緩存記錄,可以指定allEntities=true來實現

具體的使用上,其實和JSR107規範中提供的註解用法相似。

當然了,JAVA領域緩存事實規範地位雖已奠定,但是Spring Cache依舊是保持著一個兼收並蓄的姿態,並積極的相容了JCache API相關規範,比如Spring4.1起項目中可以使用JSR107規範提供的相關註解方法來操作。

小結回顧

好啦,關於JAVA中的JSR107規範以及Spring Cache規範,以及各自典型代表,我們就聊到這裡。

那麼,關於本文中提及的緩存規範的內容,你是否有自己的一些想法與見解呢?歡迎評論區一起交流下,期待和各位小伙伴們一起切磋、共同成長。


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

-Advertisement-
Play Games
更多相關文章
  • 引入課程和Maven 1.Maven maven中央倉庫:Maven Repository: Search/Browse/Explore (mvnrepository.com) maven倉庫是國外的一個網站,由於網路問題,我們也常使用maven倉庫的鏡像 maven的原理和java程式操作資料庫, ...
  • hello,大家好呀,我是既寫 Java 又寫 Go 的小樓,在寫 Go 的過程中經常對比這兩種語言的特性,踩了不少坑,也發現了不少有意思的地方,今天就來聊聊 Go 自帶的 HttpClient 的超時機制。 Java HttpClient 超時底層原理 在介紹 Go 的 HttpClient 超時 ...
  • 這篇文章主要介紹列表的一些知識。 函數list 首先需要說明的是,列表與元組、字元串一樣都是一種序列,但不同的是列表是可變的,即可修改其內容。 因為不能像修改列表那樣修改字元串,所以有些情況下使用字元串來創建列表很有幫助,函數list可以用來創建列表。 >>> list('hello') ['h', ...
  • 一、前言 是這樣的,之前手機備份圖片到電腦,由於蘋果拍照開了Live模式,所以它導出的圖片有一個2秒的視頻(.mov) 跟一張靜態圖(.jpg / .heic),靜態圖輸出取決當時導出的選項。 現在想恢復到手機,導入發現Live圖不能動了。 欸 無非就是找到兩個同名的,然後移到另一個文件夾嘛,一開始 ...
  • Python基礎之網路編程 一、網路編程前戲 1.什麼是網路編程: ​ 網路編程是指基於網路編寫代碼,能夠實現數據的遠程交互 2.學習網路編程的目的: ​ 能夠開發基於網路,實現與多用戶交互的C/S架構的軟體 3.網路編程的起源: ​ 最早起源於美國軍事領域,早期人們想要實現不同電腦內的數據交互只 ...
  • Spring AOP中增強Advice的執行順序 Spring AOP中Advice分類 同一Apsect中不同類型Advice執行順序 配置基礎環境 實驗結果 結論 不同Aspect中Advice執行順序 實驗一: Aspect1為高優先順序,Aspect2為低優先順序 實驗結果 實驗二: Aspec ...
  • 0. 目錄 1)MySQL總體架構介紹 2)MySQL存儲引擎調優 3)常用慢查詢分析工具 4)如何定位不合理的SQL 5)SQL優化的一些建議 1 MySQL總體架構介紹 1.1 MySQL總體架構介紹 引言 MySQL是一個關係型資料庫 應用十分廣泛 在學習任何一門知識之前 對其架構有一個概括性 ...
  • 本文乾貨充足篇幅較長,建議收藏後閱讀避免迷路。文末可獲取【自動聊天機器人源碼和Demo】。 本教程教大家使用即構 ZIM SDK 創建一個能與微信端互動消息的自動聊天機器人應用。ZIM SDK可廣泛應用於娛樂社交、電商購物、線上教育、互動直播等多種場景下即時通訊功能實現 。 原文作者:RTC_程式猿 ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...