
大家好,又見面了,
本文是筆者作為掘金技術社區簽約作者的身份輸出的快取專欄系列內容,將會通過系列專題,講清楚快取的方方面面,如果感興趣,歡迎關注以獲取后續更新,
有詩云“紙上得來終覺淺,絕知此事要躬行”,在上一篇文章《手寫本地快取實戰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規范,以及各自典型代表,我們就聊到這里,
那么,關于本文中提及的快取規范的內容,你是否有自己的一些想法與見解呢?歡迎評論區一起交流下,期待和各位小伙伴們一起切磋、共同成長,
?? 補充說明1 :
本文屬于《深入理解快取原理與實戰設計》系列專欄的內容之一,該專欄圍繞快取這個宏大命題進行展開闡述,全方位、系統性地深度剖析各種快取實作策略與原理、以及快取的各種用法、各種問題應對策略,并一起探討下快取設計的哲學,
如果有興趣,也歡迎關注此專欄,
?? 補充說明2 :
- 關于本文中涉及的演示代碼的完整示例,我已經整理并提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills

我是悟道,聊技術、又不僅僅聊技術~
如果覺得有用,請點贊 + 關注讓我感受到您的支持,也可以關注下我的公眾號【架構悟道】,獲取更及時的更新,
期待與你一起探討,一起成長為更好的自己,

本文來自博客園,作者:架構悟道,歡迎關注公眾號[架構悟道]持續獲取更多干貨,轉載請注明原文鏈接:https://www.cnblogs.com/softwarearch/p/16892344.html
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/534089.html
標籤:Java
上一篇:運算子
