Java開發(fā)利器之Guava?Cache的使用教程
前言
緩存技術(shù)被認為是減輕服務器負載、降低網(wǎng)絡擁塞、增強Web可擴展性的有效途徑之一,其基本思想是利用客戶訪問的時間局部性(Temproral Locality)原理, 將客戶訪問過的內(nèi)容在Cache中存放一個副本,當該內(nèi)容下次被訪問時,不必連接到駐留網(wǎng)站,而是由Cache中保留的副本提供。
在企業(yè)Web應用中,通過緩存技術(shù)能夠提高請求的響應速度;減少系統(tǒng)IO開銷;降低系統(tǒng)數(shù)據(jù)讀寫壓力...
緩存的意義
首先我們要知道,在我們開發(fā)過程中,為什么要使用緩存,緩存能夠為我們帶來哪些好處!
優(yōu)點
- 通過緩存承載系統(tǒng)壓力,減少對系統(tǒng)或網(wǎng)絡資源訪問而引起的性能消耗,在流量較大時能夠很好地減少系統(tǒng)擁塞
- 緩存一般都是使用存取非??斓慕M件實現(xiàn),通過緩存能夠快速響應客戶端請求,從而降低客戶訪問延遲,提審系統(tǒng)響應速度
- 在配備負載均衡的應用架構(gòu)中,通過緩存靜態(tài)資源能夠有效減少服務器負載壓力
- 當下游應用故障時,通過返回緩存數(shù)據(jù)能夠在一定程度上增強應用容錯性
缺點
- 緩存數(shù)據(jù)與實際數(shù)據(jù)不一致問題問題
- 高并發(fā)場景時存在緩存擊穿、緩存穿透、緩存雪崩等問題
總的來說,緩存主要是針對高頻訪問但低頻更新的數(shù)據(jù),從而加快服務器響應與原資源訪問壓力
Guava Cache是一個相對比較簡單并且容易理解的本地緩存框架,今天主要以此為開端來認識并學習如何使用緩存
Guava Cache特色
本地緩存我們可以簡單的理解為Map,將數(shù)據(jù)保存到Map(內(nèi)存)中,下次使用該數(shù)據(jù)時,通過key直接從Map中取即可。但是使用Map會有一些幾個問題需要考慮:
- 緩存的容量。不可能無限制的對數(shù)據(jù)進行緩存,當數(shù)據(jù)較大時占用系統(tǒng)資源會導致主業(yè)務受影響
- 緩存的清理。有些緩存使用頻率很低,如果一直占用資源也是一種浪費
- 并發(fā)訪問時的效率問題。緩存更新時瞬時對系統(tǒng)、網(wǎng)絡資源的訪問導致故障
- 緩存使用情況評估
當然以上問題我們通過我們對Map包裝下即可實現(xiàn),當然Guava Cache也就是基于這種思想,底層原理則是基于Map實現(xiàn),我們看下其有哪些特色:
緩存過期和淘汰機制
通過設(shè)置Key的過期時間,包括訪問過期和創(chuàng)建過期;設(shè)置緩存容量大小,采用LRU的方式,選擇最近最久的緩存進行刪除。
并發(fā)處理能力
Cache主要基于CurrentHashMap實現(xiàn)線程安全;通過對key的計算,基于分段鎖,提高緩存讀寫效率,降低鎖的粒度,提升并發(fā)能力
更新鎖定
在緩存中查詢某個key,如果不存在,則查源數(shù)據(jù),并回填緩存。在高并發(fā)下會出現(xiàn),多次查詢元數(shù)據(jù)并重復回填緩存,可能會造成系統(tǒng)故障,最明顯的DB服務器宕機,性能下降等。GuavaCache通過在CacheLoader調(diào)用load方法時,對同一個key同一時刻只會有一個請求去讀源數(shù)據(jù)并回填緩存,后面的請求則直接繼續(xù)從緩存讀取,有效阻斷并發(fā)請求對資源服務的影響。
集成數(shù)據(jù)源
一般我們在業(yè)務中操作緩存,都會操作緩存和數(shù)據(jù)源兩部分GuavaCache的get可以集成數(shù)據(jù)源,在從緩存中讀取不到時可以從數(shù)據(jù)源中讀取數(shù)據(jù)并回填緩存
監(jiān)控統(tǒng)計
監(jiān)控緩存加載次數(shù)、命中率、失誤率以及數(shù)據(jù)加載時長等
API介紹
1.緩存構(gòu)建
ManualCache 此時Cache相當于一個Map,對數(shù)據(jù)進行CRUD操作時,需要同步操作緩存Map; 高并發(fā)情況時,可以使用get(k,loader)讀緩存,通過Cache鎖機制,防止對系統(tǒng)資源(DB)的并發(fā)訪問 通過put方法實現(xiàn)緩存的存入與更新;
LoadingCache 此時構(gòu)建的是一個實現(xiàn)了Cache接口的LoadingCache,相比ManualCache,提供了緩存回填機制,即當緩存不存在時,會基于CacheLoader查詢數(shù)據(jù)并將結(jié)果回填到緩存, 在高并發(fā)時,可以有效地基于緩存鎖減少對系統(tǒng)資源的調(diào)用。此時僅需要關(guān)注緩存的使用,緩存的更新與存入都是基于CacheLoader實現(xiàn);
2.緩存獲取
get(k) 根據(jù)key查詢,沒有則觸發(fā)load;如果load為空則拋出異常
getUnchecked(k) 緩存不存在或返回為null會拋出檢查異常
get(k,loader) 根據(jù)key查詢,沒有則調(diào)用loader方法,且對結(jié)果緩存;如果loader返回null則拋出異常,此時不會調(diào)用默認的load方法
getIfPresent(k) 有緩存則返回,否則返回null,不會觸發(fā)load
3.緩存更新
put(k,v) 如果緩存已經(jīng)存在,則會先進行一次刪除
4.緩存刪除
invalidate(k) 根據(jù)key使緩存失效
過期 通過配置的過期參數(shù),比如expireAfterAccess、expireAfterWrite、refreshAfterWrite
過載 當緩存數(shù)據(jù)量超過設(shè)置的最大值時,根據(jù)LRU算法進行刪除
引用 構(gòu)建緩存時將鍵值設(shè)置為弱引用、軟引用,基于GC機制來清理緩存
5.統(tǒng)計
hitRate() 緩存命中率;
hitMiss() 緩存失誤率;
loadCount() 加載次數(shù);
averageLoadPenalty() 加載新值的平均時間,單位為納秒;
evictionCount() 緩存項被回收的總數(shù),不包括顯式清除。
Builder配置
配置 | 描述 |
---|---|
expireAfterAccess | 多久沒有讀寫則過期 |
expireAfterWrite | 寫入后多久沒更新自動過期,先刪除,后load |
refreshAfterWrite | 上一次更新后多久自動刷新,先reload后刪除,并發(fā)時會取到老的數(shù)據(jù) |
removalListener | 設(shè)置緩存刪除監(jiān)聽 |
initialCapacity | 緩存初始化大小 |
concurrencyLevel | 最大的并發(fā)數(shù),可以理解為并發(fā)線程數(shù)量 |
maximumSize | 最大緩存數(shù)量,超過時會根據(jù)策略清除 |
maximumWeight | 最大權(quán)重容量數(shù),僅用于確定緩存是否超過容量 |
recordStats | 緩存命中統(tǒng)計 |
簡單示例
ManualCache模式
下面以用戶服務為例,我們看下如何在增刪改查方法中使用緩存:
private?Cache<String,?User>?cache?=?CacheBuilder.newBuilder() ????????????.expireAfterWrite(3,?TimeUnit.SECONDS)//寫入多久沒更新自動過期,先刪除,后load ????????????.removalListener(new?RemovalListener<Object,?Object>()?{ ????????????????@Override ????????????????public?void?onRemoval(RemovalNotification<Object,?Object>?notification)?{ ????????????????????LOGGER.info("{}?remove?{}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd?HH:mm:ss")),notification.getKey()); ????????????????} ????????????}) ????????????.initialCapacity(20)?//初始化容量 ????????????.concurrencyLevel(10)?//?并發(fā) ????????????.maximumSize(100)?//最多緩存數(shù)量 ????????????.recordStats()?//?開啟統(tǒng)計 ????????????.build(); @Override ????public?User?getUser(String?id){ //????????緩存不存在時,通過LocalCache鎖機制,防止對數(shù)據(jù)庫的高頻訪問 ???????User?user; ???????try?{ ???????????user?=?cache.get(id,()->?{ ???????????????LOGGER.info("緩存不存在,從loader加載數(shù)據(jù)"); ???????????????return?userDao.get(id); ???????????}); ???????}?catch?(ExecutionException?e)?{ ???????????throw?new?RuntimeException(e); ???????} ????????return?user; ????} ????@Override ????public?User?saveOrUpdateUser(User?user){ ????????userDao.saveOrUpdate(user); ????????cache.put(user.getId(),user); ????????return?user; ????} ????@Override ????public?void?removeUser(String?id){ ????????userDao.remove(id); ????????cache.invalidate(id); ????}
LoadingCache模式
private?LoadingCache<String,?User>?cache?=?CacheBuilder.newBuilder() ????????????//?省略 ????????????.build(new?CacheLoader<String,?User>()?{ ????????????????@Override ????????????????public?User?load(String?key)?throws?Exception?{ ????????????????????LOGGER.info("{}?load?{}",LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd?HH:mm:ss")),key); ????????????????????return?userDao.get(key); ????????????????} ????????????????@Override ????????????????public?ListenableFuture<User>?reload(String?key,?User?oldUser)?throws?Exception?{ ????????????????????LOGGER.info("{}?reload?{}",?LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd?HH:mm:ss")),key); ????????????????????ListenableFutureTask<User>?listenableFutureTask?=?ListenableFutureTask.create(()?->?userDao.get(key)); ????????????????????CompletableFuture.runAsync(listenableFutureTask); ????????????????????return?listenableFutureTask; ????????????????} ????????????}); ????@SneakyThrows ????@Override ????public?User?getUser(String?id){ ????????//?緩存不存在或返回為null會拋出異常 ????????try?{ ????????????return?cache.getUnchecked(id); ????????}?catch?(Exception?e)?{ ????????????return?null; ????????} ????} ????@Override ????public?User?saveOrUpdateUser(User?user){ ????????cache.invalidate(user.getId()); ????????return?userDao.saveOrUpdate(user); ????} ????@Override ????public?void?removeUser(String?id){ ????????cache.invalidate(id); ????????userDao.remove(id); ????}
總結(jié):第一種寫法更像是前面說到的Map,在對數(shù)據(jù)進行CRUD操作時,需要用戶手動對緩存進行同步的更新或刪除操作,所以叫ManualCache(手動),當然Guava Cache對Map的加強依然有效,比如過期清除,緩存容量限制。第二種方式寫法差不多,主要是引入了CacheLoader接口,在讀數(shù)據(jù)時緩存數(shù)據(jù)不存在時,通過CacheLoader的load方法先寫緩存后返回數(shù)據(jù)
注意
1.expireAfterWrite、refreshAfterWrite的區(qū)別
在refreshAfterWrite導致緩存失效時,并不會因為更新緩存而阻塞緩存數(shù)據(jù)的返回,只不過是返回老的數(shù)據(jù)
2.不能緩存null
有時候為了將值為null的數(shù)據(jù)統(tǒng)一緩存,這樣就不會因為沒有緩存數(shù)據(jù)而訪問數(shù)據(jù)庫造成壓力
3.讀寫時才進行刪除
Guava Cache的緩存數(shù)據(jù)刪除是在更新或?qū)懭霑r才會觸發(fā),沒有單獨的調(diào)度服務完成這一工作
本地緩存
類似的本地緩存還有,有興趣的可以自己嘗試,其實實現(xiàn)思想應該也差不多
到此這篇關(guān)于Java開發(fā)利器之Guava Cache的使用教程的文章就介紹到這了,更多相關(guān)Java Guava Cache內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java利用數(shù)組隨機抽取幸運觀眾如何實現(xiàn)
這篇文章主要介紹了Java利用數(shù)組隨機抽取幸運觀眾如何實現(xiàn),需要的朋友可以參考下2014-02-02