業界各大廠商或開源團隊都會構建並提供一些緩存框架組件提供給開發者按需選擇,這裡就會涉及到一個標準規範的遵循問題,本文我們一起聊聊JCache API規範與SpringCache規範。 ...
大家好,又見面了。
本文是筆者作為掘金技術社區簽約作者的身份輸出的緩存專欄系列內容,將會通過系列專題,講清楚緩存的方方面面。如果感興趣,歡迎關註以獲取後續更新。
有詩雲“紙上得來終覺淺,絕知此事要躬行”,在上一篇文章《手寫本地緩存實戰2—— 打造正規軍,構建通用本地緩存框架》中,我們一起論證並逐步實現了一套簡化版本的通用本地緩存框架,併在過程中逐步剖析了緩存設計關鍵要素的實現策略。本篇文章中,我們一起來聊一聊緩存框架實現所需要遵循的規範。
為何需要規範
上一章中構建的最簡化版本的緩存框架,雖然可以使用,但是也存在一個問題,就是它對外提供的實現介面都是框架根據自己的需要而自定義的。這樣一來,項目集成了此緩存框架,後續如果想要更換緩存框架的時候,業務層面的改動會比較大。 —— 因為是自定義的框架介面,無法基於里氏替換
原則來進行靈活的更換。
在業界各大廠商或者開源團隊都會構建並提供一些自己實現的緩存框架或者組件,提供給開發者按需選擇使用。如果大家都是各自閉門造車,勢必導致業務中集成並使用某一緩存實現之後,想要更換緩存實現組件會難於登天。
千古一帝秦始皇統一天下後,頒佈了書同文、車同軌等一系列法規制度,使得所有的車輛都遵循統一的軸距,然後都可以在官道上正常的通行,大大提升了流通性。而正所謂“國有國法、行有行規”,為了保證緩存框架的通用性、提升項目的可移植性,JAVA行業也迫切需要這麼一個緩存規範,來約束各個緩存提供商給出的緩存框架都遵循相同的規範介面,業務中按照標準介面進行調用,無需與緩存框架進行深度耦合,使得緩存組件的更換成為一件簡單點的事情。
在JAVA的緩存領域,流傳比較廣泛的主要是JCache API
和Spring 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
中幾個介面類的關係呢?
幾個簡單的說明:
-
CachingProvider並無太多實際邏輯層面的功能,只是用來基於SPI機制,方便項目中集成插拔使用。內部持有CacheManager對象,實際的緩存管理能力,由CacheManager負責提供。
-
CacheManager負責具體的緩存管理相關能力實現,實例由
CachingProvider
提供並持有,CachingProvider可以持有一個或者多個不同的CacheManager
對象。這些CacheManager對象可以是相同類型,也可以是不同類型,比如我們可以實現2種緩存框架,一種是基於記憶體
的緩存,一種是基於磁碟
的緩存,則可以分別提供兩種不同的CacheManager,供業務按需調用。 -
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 | 將指定的key 和value 映射內容存入到緩存容器中 |
@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 Cache
、Caffeine 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規範,以及各自典型代表,我們就聊到這裡。
那麼,關於本文中提及的緩存規範的內容,你是否有自己的一些想法與見解呢?歡迎評論區一起交流下,期待和各位小伙伴們一起切磋、共同成長。