
大家好,又見面了,
本文是筆者作為掘金技術社區簽約作者的身份輸出的快取專欄系列內容,將會通過系列專題,講清楚快取的方方面面,如果感興趣,歡迎關注以獲取后續更新,
不知不覺,這已經是《深入理解快取原理與實戰設計》系列專欄的第6篇文章了,經過前面5篇文章的鋪墊,我們系統且全面的介紹了快取相關的概念與典型問題,也手動實操了如何構建一個本地最簡版本的通用快取框架,還對JAVA主流的本地快取規范進行了解讀,
秉持著不重復造輪子的理念,本篇文章中,我們就來一起深入剖析JAVA本地快取的優秀“輪子” —— 來自Google家族的Guava Cache,聊一聊其實作機制、看一看如何使用,

Guava Cache初識
Guava是Google提供的一套JAVA的工具包,而Guava Cache則是該工具包中提供的一套完善的JVM級別的高并發快取框架,其實作機制類似ConcurrentHashMap,但是進行了眾多的封裝與能力擴展,作為JVM級別的本地快取框架,Guava Cache具備快取框架該有的眾多基礎特性,當然,Guava Cache能從眾多本地快取類產品中脫穎而出,除了具備上述基礎快取特性外,還有眾多貼心的能力增強,絕對算得上是工具包屆的超級暖男!為什么這么說呢?我們一起看下Guava Cache的能力介紹,應該可以有所體會,

支持快取記錄的過期設定
作為一個合格的快取容器,支持快取記錄過期是一個基礎能力,Guava Cache不但支持設定過期時間,還支持選擇是根據插入時間進行過期處理(創建過期)、或者是根據最后訪問時間進行過期處理(訪問過期),
| 過期策略 | 具體說明 |
|---|---|
| 創建過期 | 基于快取記錄的插入時間判斷,比如設定10分鐘過期,則記錄加入快取之后,不管有沒有訪問,10分鐘時間到則 |
| 訪問過期 | 基于最后一次的訪問時間來判斷是否過期,比如設定10分鐘過期,如果快取記錄被訪問到,則以最后一次訪問時間重新計時;只有連續10分鐘沒有被訪問的時候才會過期,否則將一直存在快取中不會被過期, |
實際使用的時候,可以在創建快取容器的時候指定過期策略即可:
- 基于創建時間過期
public Cache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.expireAfterWrite(30L, TimeUnit.MINUTES)
.build();
}
- 基于訪問時間過期
public Cache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.expireAfterAccess(30L, TimeUnit.MINUTES)
.build();
}
是不是很方便?

支持快取容量限制與不同淘汰策略
作為記憶體型快取,必須要防止出現記憶體溢位的風險,Guava Cache支持設定快取容器的最大存盤上限,并支持根據快取記錄條數或者基于每條快取記錄的權重(后面會具體介紹)進行判斷是否達到容量閾值,
當容量觸達閾值后,支持根據FIFO + LRU策略實施具體淘汰處理以騰出位置給新的記錄使用,
| 淘汰策略 | 具體說明 |
|---|---|
| FIFO | 根據快取記錄寫入的順序,先寫入的先淘汰 |
| LRU | 根據訪問順序,淘汰最久沒有訪問的記錄 |
實際使用的時候,同樣是在創建快取容器的時候指定容量上限與淘汰策略,這樣就可以放心大膽的使用而不用擔心記憶體溢位問題咯,
- 限制快取記錄條數
public Cache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.maximumSize(10000L)
.build();
}
- 限制快取記錄權重
public Cache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.maximumWeight(10000L)
.weigher((key, value) -> (int) Math.ceil(instrumentation.getObjectSize(value) / 1024L))
.build();
}
這里需要注意:按照權重進行限制快取容量的時候必須要指定weighter屬性才可以生效,上面代碼中我們通過計算value物件的位元組數(byte)來計算其權重資訊,每1kb的位元組數作為1個權重,整個快取容器的總權重限制為1w,這樣就可以實作將快取記憶體占用控制在10000*1k≈10M左右,
有沒有很省心?

支持集成資料源能力
在前面文章中,我們有介紹過快取的三種模型,分別是旁路型、穿透型、異步型,Guava Cache作為一個封裝好的快取框架,是一個典型的穿透型快取,正常業務使用快取時通常會使用旁路型快取,即先去快取中嘗試查詢獲取資料,如果獲取不到則會從資料庫中進行查詢并加入到快取中;而為了簡化業務端使用復雜度,Guava Cache支持集成資料源,業務層面呼叫介面查詢快取資料的時候,如果快取資料不存在,則會自動去資料源中進行資料獲取并加入快取中,
public User findUser(Cache<String, User> cache, String userId) {
try {
return cache.get(userId, () -> {
System.out.println(userId + "用戶快取不存在,嘗試回源查找并回填...");
return userDao.getUser(userId);
});
} catch (ExecutionException e) {
e.printStackTrace();
}
return null;
}
實際使用的時候如果查詢的用戶不存在,則會自動去回源查找并寫入快取里,再次獲取的時候便可以從快取直接獲取:

上面的方法里,是通過在get方法里傳入Callable實作的方式指定回源獲取資料的方式,來實作快取不存在情況的自動資料拉取與回填到快取中的,實際使用的時候,除了Callable方式,還有一種CacheLoader的模式,也可以實作這一效果,
需要我們在創建快取容器的時候宣告容器為LoadingCache型別(下面的章節中有介紹),并且指定CacheLoader處理邏輯:
public LoadingCache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
System.out.println(key + "用戶快取不存在,嘗試CacheLoader回源查找并回填...");
return userDao.getUser(key);
}
});
}
這樣,獲取不到資料的時候,也會自動回源查詢并填充,比如我們執行如下呼叫邏輯:
public static void main(String[] args) {
CacheService cacheService = new CacheService();
LoadingCache<String, User> cache = cacheService.createUserCache();
try {
System.out.println(cache.get("123"));
System.out.println(cache.get("124"));
System.out.println(cache.get("123"));
} catch (Exception e) {
e.printStackTrace();
}
}
執行結果如下:
123用戶快取不存在,嘗試CacheLoader回源查找并回填...
User(userId=123, userName=鐵柱, department=研發部)
124用戶快取不存在,嘗試CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=測驗部)
User(userId=123, userName=鐵柱, department=研發部)
兩種方式都可以實作這一效果,實際可以根據需要與場景選擇合適的方式,
當然,有些時候,可能也會涉及到CacheLoader與Callable兩種方式結合使用的場景,這種情況下優先會執行Callable提供的邏輯,Callable缺失的場景會使用CacheLoader提供的邏輯,
public static void main(String[] args) {
CacheService cacheService = new CacheService();
LoadingCache<String, User> cache = cacheService.createUserCache();
try {
System.out.println(cache.get("123", () -> new User("xxx")));
System.out.println(cache.get("124"));
System.out.println(cache.get("123"));
} catch (Exception e) {
e.printStackTrace();
}
}
執行后,可以看出Callable邏輯被優先執行,而CacheLoader作為兜底策略存在:
User(userId=xxx, userName=null, department=null)
124用戶快取不存在,嘗試CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=測驗部)
User(userId=xxx, userName=null, department=null)

支持更新鎖定能力
這個是與上面資料源集成一起的輔助增強能力,在高并發場景下,如果某個key值沒有命中快取,大量的請求同步打到下游模塊處理的時候,很容易造成快取擊穿問題,

為了防止快取擊穿問題,可以通過加鎖的方式來規避,當快取不可用時,僅持鎖的執行緒負責從資料庫中查詢資料并寫入快取中,其余請求重試時先嘗試從快取中獲取資料,避免所有的并發請求全部同時打到資料庫上,
作為穿透型快取的保護策略之一,Guava Cache自帶了并發鎖定機制,同一時刻僅允許一個請求去回源獲取資料并回填到快取中,而其余請求則阻塞等待,不會造成資料源的壓力過大,
有沒有被暖心到?

提供了快取相關的一些監控統計
引入快取的一個初衷是希望快取能夠提升系統的處理性能,而有限快取容量中僅存盤部分資料的時候,我們會希望存盤的有限資料可以盡可能的覆寫并抗住大部分的請求流量,所以對快取的命中率會非常關注,
Guava Cache深知這一點,所以提供了stat統計日志,支持查看快取資料的加載或者命中情況統計,我們可以基于命中情況,不斷的去優化代碼中快取的資料策略,以發揮出快取的最大價值,
Guava Cache的統計資訊封裝為CacheStats物件進行承載,主要包含一下幾個關鍵指標項:
| 指標 | 含義說明 |
|---|---|
| hitCount | 命中快取次數 |
| missCount | 沒有命中快取次數(查詢的時候記憶體中沒有) |
| loadSuccessCount | 回源加載的時候加載成功次數 |
| loadExceptionCount | 回源加載但是加載失敗的次數 |
| totalLoadTime | 回源加載操作總耗時 |
| evictionCount | 洗掉記錄的次數 |
快取容器創建的時候,可以通過recordStats()開啟快取行為的統計記錄:
public static void main(String[] args) {
LoadingCache<String, User> cache = CacheBuilder.newBuilder()
.recordStats()
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
System.out.println(key + "用戶快取不存在,嘗試CacheLoader回源查找并回填...");
User user = userDao.getUser(key);
if (user == null) {
System.out.println(key + "用戶不存在");
}
return user;
}
});
try {
System.out.println(cache.get("123");
System.out.println(cache.get("124"));
System.out.println(cache.get("123"));
System.out.println(cache.get("126"));
} catch (Exception e) {
} finally {
CacheStats stats = cache.stats();
System.out.println(stats);
}
}
上述代碼執行之后結果輸出如下:
123用戶快取不存在,嘗試CacheLoader回源查找并回填...
User(userId=123, userName=鐵柱, department=研發部)
124用戶快取不存在,嘗試CacheLoader回源查找并回填...
User(userId=124, userName=翠花, department=測驗部)
User(userId=123, userName=鐵柱, department=研發部)
126用戶快取不存在,嘗試CacheLoader回源查找并回填...
126用戶不存在
CacheStats{hitCount=1, missCount=3, loadSuccessCount=2, loadExceptionCount=1, totalLoadTime=1972799, evictionCount=0}
可以看出,一共執行了4次請求,其中1次命中,3次回源處理,2次回源加載成功,1次回源沒找到資料,與列印出來的CacheStats統計結果完全吻合,
有著上述能力的加持,前面將Guava Cache稱作“暖男”不過分吧?

Guava Cache適用場景
在本系列專欄的第一篇文章《聊一聊作為高并發系統基石之一的快取,會用很簡單,用好才是技識訓》中,我們在快取的一步步演進介紹中提過本地快取與集中式快取的區別,也聊了各自的優缺點,
作為一款純粹的本地快取框架,Guava Cache具備本地快取該有的優勢,也無可避免的存在著本地快取的弊端,
| 維度 | 簡要概述 |
|---|---|
| 優勢 | 基于空間換時間的策略,利用記憶體的高速處理效率,提升機器的處理性能,減少大量對外的IO請求互動,比如讀取DB、請求外部網路、讀取本地磁盤資料等等操作, |
| 弊端 | 整體容量受限,可能對本機記憶體造成壓力,此外,對于分布式多節點集群部署的場景,快取更新場景會出現快取漂移問題,導致各個節點之間的快取資料不一致, |
鑒于上述優劣綜合判斷,可以大致圈定Guava Cache的實際適用場合:
- 資料讀多寫少且對一致性要求不高的場景
這類場景中,會將資料快取到本地記憶體中,采用定時觸發(或者事件推送)的策略重新加載到記憶體中,這樣業務處理邏輯直接從記憶體讀取需要的資料,修改系統配置項之后,需要等待一定的時間后方可生效,
很多的配置中心采用的都是這個快取策略,統一配置中心中管理配置資料,然后各個業務節點會從統一配置中心拉取配置并存盤在自己本地的記憶體中然后使用本地記憶體中的資料,這樣可以有效規避配置中心的單點故障問題,降低了配置中心的請求壓力,也提升了業務節點自身的業務處理性能(減少了與配置中心之間的網路互動請求),
- 對性能要求極其嚴苛的場景
對于分布式系統而言,集中式快取是一個常規場景中很好的選項,但是對于一些超大并發量且讀性能要求嚴苛的系統而言,一個請求流程中需要頻繁的去與Redis互動,其網路開銷也是不可忍受的,所以可以采用將資料本機記憶體快取的方式,分散redis的壓力,降低對外請求互動的次數,提升介面回應速度,
- 簡單的本地資料快取,作為
HashMap/ConcurrentHashMap的替代品
這種場景也很常見,我們在專案中經常會遇到一些資料的需要臨時快取一下,為了方便很多時候直接使用的HashMap或者ConcurrentHashMap來實作,而Guava Cache聚焦快取場景做了很多額外的功能增強(比如資料過期能力支持、容量上限約束等),可以完美替換掉HashMap/ConcurrentHashMap,更適合快取場景使用,

Guava Cache使用
引入依賴
使用Guava Cache,首先需要引入對應的依賴包,對于Maven專案,可以在pom.xml中添加對應的依賴宣告即可:
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>31.1-jre</version>
</dependency>
這樣,就完成了依賴引入,

容器創建 —— CacheBuilder
具體使用前首先面臨的就是如何創建Guava Cache實體,可以借助CacheBuilder以一種優雅的方式來構建出合乎我們訴求的Cache實體,
對CacheBuilder中常見的屬性方法,歸納說明如下:
| 方法 | 含義說明 |
|---|---|
| newBuilder | 構造出一個Builder實體類 |
| initialCapacity | 待創建的快取容器的初始容量大小(記錄條數) |
| maximumSize | 指定此快取容器的最大容量(最大快取記錄條數) |
| maximumWeight | 指定此快取容器的最大容量(最大比重值),需結合weighter方可體現出效果 |
| expireAfterWrite | 設定過期策略,按照資料寫入時間進行計算 |
| expireAfterAccess | 設定過期策略,按照資料最后訪問時間來計算 |
| weighter | 入參為一個函式式介面,用于指定每條存入的快取資料的權重占比情況,這個需要與maximumWeight結合使用 |
| refreshAfterWrite | 快取寫入到快取之后 |
| concurrencyLevel | 用于控制快取的并發處理能力,同時支持多少個執行緒并發寫入操作 |
| recordStats | 設定開啟此容器的資料加載與快取命中情況統計 |
基于CacheBuilder及其提供的各種方法,我們可以輕松的進行快取容器的構建、并指定容器的各種約束條件,
比如下面這樣:
public LoadingCache<String, User> createUserCache() {
return CacheBuilder.newBuilder()
.initialCapacity(1000) // 初始容量
.maximumSize(10000L) // 設定最大容量
.expireAfterWrite(30L, TimeUnit.MINUTES) // 設定寫入過期時間
.concurrencyLevel(8) // 設定最大并發寫操作執行緒數
.refreshAfterWrite(1L, TimeUnit.MINUTES) // 設定自動重繪資料時間
.recordStats() // 開啟快取執行情況統計
.build(new CacheLoader<String, User>() {
@Override
public User load(String key) throws Exception {
return userDao.getUser(key);
}
});
}

業務層使用
Guava Cache容器物件創建完成后,可以基于其提供的對外介面完成相關快取的具體操作,首先可以了解下Cache提供的對外操作介面:

對關鍵介面的含義梳理歸納如下:
| 介面名稱 | 具體說明 |
|---|---|
| get | 查詢指定key對應的value值,如果快取中沒匹配,則基于給定的Callable邏輯去獲取資料回填快取中并回傳 |
| getIfPresent | 如果快取中存在指定的key值,則回傳對應的value值,否則回傳null(此方法不會觸發自動回源與回填操作) |
| getAllPresent | 針對傳入的key串列,回傳快取中存在的對應value值串列(不會觸發自動回源與回填操作) |
| put | 往快取中添加key-value鍵值對 |
| putAll | 批量往快取中添加key-value鍵值對 |
| invalidate | 從快取中洗掉指定的記錄 |
| invalidateAll | 從快取中批量洗掉指定記錄,如果無引數,則清空所有快取 |
| size | 獲取快取容器中的總記錄數 |
| stats | 獲取快取容器當前的統計資料 |
| asMap | 將快取中的資料轉換為ConcurrentHashMap格式回傳 |
| cleanUp | 清理所有的已過期的資料 |
在專案中,可以基于上述介面,實作各種快取操作功能,
public static void main(String[] args) {
CacheService cacheService = new CacheService();
LoadingCache<String, User> cache = cacheService.createUserCache6();
cache.put("122", new User("122"));
cache.put("122", new User("122"));
System.out.println("put操作后查詢:" + cache.getIfPresent("122"));
cache.invalidate("122");
System.out.println("invalidate操作后查詢:" + cache.getIfPresent("122"));
System.out.println(cache.stats());
}
執行后,結果如下:
put操作后查詢:User(userId=122, userName=null, department=null)
invalidate操作后查詢:null
CacheStats{hitCount=1, missCount=1, loadSuccessCount=0, loadExceptionCount=0, totalLoadTime=0, evictionCount=0}
當然,上述示例代碼中這種使用方式有個明顯的弊端就是業務層面對Guava Cache的私有API依賴過深,后續如果需要替換Cache組件的時候會比較痛苦,需要對業務呼叫的地方進行大改,所以真正專案里面,最好還是對其適當封裝,以實作業務層面的解耦,如果你的專案是使用Spring框架,也可以基于Spring Cache統一規范來集成并使用Guava Cache,降低對業務邏輯的侵入,

小結回顧
好啦,關于Guava Cache的功能與關鍵特性介紹,以及專案中具體的集成與使用方法,就介紹到這里了,總結一下,Guava Cache其實就是一個增強版的大號ConcurrentHashMap,在保證執行緒安全的情況下,增加了快取必備的資料過期、容量限制、回源策略等能力,既保證了本身的精簡,又使得整體能力足以滿足大部分本地快取場景的使用訴求,也正是由于這些原因,Guava Cache在JAVA領域廣受好評,使用范圍非常的廣泛,
下一篇文章中,我們將繼續對Guava Cache展開討論,跳出使用層面,剖析其內部核心實作邏輯,如果有興趣,歡迎關注后續文章的更新,
那么,關于本文中提及的內容,你是否有自己的一些想法與見解呢?歡迎評論區一起交流下,期待和各位小伙伴們一起切磋、共同成長,
?? 補充說明1 :
本文屬于《深入理解快取原理與實戰設計》系列專欄的內容之一,該專欄圍繞快取這個宏大命題進行展開闡述,全方位、系統性地深度剖析各種快取實作策略與原理、以及快取的各種用法、各種問題應對策略,并一起探討下快取設計的哲學,
如果有興趣,也歡迎關注此專欄,
?? 補充說明2 :
- 關于本文中涉及的演示代碼的完整示例,我已經整理并提交到github中,如果您有需要,可以自取:https://github.com/veezean/JavaBasicSkills

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

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