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
  • GoF之工廠模式 @目錄GoF之工廠模式每博一文案1. 簡單說明“23種設計模式”1.2 介紹工廠模式的三種形態1.3 簡單工廠模式(靜態工廠模式)1.3.1 簡單工廠模式的優缺點:1.4 工廠方法模式1.4.1 工廠方法模式的優缺點:1.5 抽象工廠模式1.6 抽象工廠模式的優缺點:2. 總結:3 ...
  • 新改進提供的Taurus Rpc 功能,可以簡化微服務間的調用,同時可以不用再手動輸出模塊名稱,或調用路徑,包括負載均衡,這一切,由框架實現並提供了。新的Taurus Rpc 功能,將使得服務間的調用,更加輕鬆、簡約、高效。 ...
  • 本章將和大家分享ES的數據同步方案和ES集群相關知識。廢話不多說,下麵我們直接進入主題。 一、ES數據同步 1、數據同步問題 Elasticsearch中的酒店數據來自於mysql資料庫,因此mysql數據發生改變時,Elasticsearch也必須跟著改變,這個就是Elasticsearch與my ...
  • 引言 在我們之前的文章中介紹過使用Bogus生成模擬測試數據,今天來講解一下功能更加強大自動生成測試數據的工具的庫"AutoFixture"。 什麼是AutoFixture? AutoFixture 是一個針對 .NET 的開源庫,旨在最大程度地減少單元測試中的“安排(Arrange)”階段,以提高 ...
  • 經過前面幾個部分學習,相信學過的同學已經能夠掌握 .NET Emit 這種中間語言,並能使得它來編寫一些應用,以提高程式的性能。隨著 IL 指令篇的結束,本系列也已經接近尾聲,在這接近結束的最後,會提供幾個可供直接使用的示例,以供大伙分析或使用在項目中。 ...
  • 當從不同來源導入Excel數據時,可能存在重覆的記錄。為了確保數據的準確性,通常需要刪除這些重覆的行。手動查找並刪除可能會非常耗費時間,而通過編程腳本則可以實現在短時間內處理大量數據。本文將提供一個使用C# 快速查找並刪除Excel重覆項的免費解決方案。 以下是實現步驟: 1. 首先安裝免費.NET ...
  • C++ 異常處理 C++ 異常處理機制允許程式在運行時處理錯誤或意外情況。它提供了捕獲和處理錯誤的一種結構化方式,使程式更加健壯和可靠。 異常處理的基本概念: 異常: 程式在運行時發生的錯誤或意外情況。 拋出異常: 使用 throw 關鍵字將異常傳遞給調用堆棧。 捕獲異常: 使用 try-catch ...
  • 優秀且經驗豐富的Java開發人員的特征之一是對API的廣泛瞭解,包括JDK和第三方庫。 我花了很多時間來學習API,尤其是在閱讀了Effective Java 3rd Edition之後 ,Joshua Bloch建議在Java 3rd Edition中使用現有的API進行開發,而不是為常見的東西編 ...
  • 框架 · 使用laravel框架,原因:tp的框架路由和orm沒有laravel好用 · 使用強制路由,方便介面多時,分多版本,分文件夾等操作 介面 · 介面開發註意欄位類型,欄位是int,查詢成功失敗都要返回int(對接java等強類型語言方便) · 查詢介面用GET、其他用POST 代碼 · 所 ...
  • 正文 下午找企業的人去鎮上做貸後。 車上聽同事跟那個司機對罵,火星子都快出來了。司機跟那同事更熟一些,連我在內一共就三個人,同事那一手指桑罵槐給我都聽愣了。司機也是老社會人了,馬上聽出來了,為那個無辜的企業經辦人辯護,實際上是為自己辯護。 “這個事情你不能怪企業。”“但他們總不能讓銀行的人全權負責, ...