如何使用Guava Cache做緩存
1. 概述
1.1 適用場景
Cache
在ConcurrentHashMap
的基礎(chǔ)上提供了自動加載數(shù)據(jù)、清除數(shù)據(jù)、get-if-absend-compute的功能,適用場景:
- 愿意花一些內(nèi)存來提高訪問速度
- 緩存的數(shù)據(jù)查詢或計算代碼高昂,但是需要查詢不止一次
- 緩存的數(shù)據(jù)在內(nèi)存中放得下,否則應(yīng)該考慮Redis、Memcached
1.2 Hello world
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterWrite(10, TimeUnit.MINUTES) .removalListener(MY_LISTENER) .build ( new CacheLoader<Key, Graph>() { @Override public Graph load(Key key) throws AnyException { return createExpensiveGraph(key); } } );
2. 數(shù)據(jù)加載使用
2.1 CacheLoader.load(K key)
LoadingCache
是包含了數(shù)據(jù)加載方式的Cache
,加載方式由CacheLoader
指定,CacheLoader
可以簡單到只實現(xiàn)一個V load(K key)
方法,如:
CacheLoader<Key,Graph> cacheLoader = new CacheLoader<Key,Graph> { public Grapch load(Key key) throws AnyException { return createExpensiveGraph(key); } }
LoadingCache
和Cache
都是通過CacheBuilder
創(chuàng)建,唯一的區(qū)別是LoadingCache
需要要提供CacheLoader
實例。
LoadingCache<Key, Graph> graphs = CacheBuilder.newBuilder().maximumSize(1000).build(cacheLoader); graphs.get(key);
LoadingCache
經(jīng)典的使用方式是通過get(K)
獲取數(shù)據(jù),有緩存則直接返回,否則調(diào)用CacheLoader.load(K)
計算并寫入緩存。
CacheLoader
可以拋出異常,檢查型異常會被封裝為ExecutionException
,RuntimeException
會被封裝為UncheckedExecutionException
。
如果不想在客戶端代碼里處理異常,可以使用LoadingCache.getUnchecked(K)
方法,該方法只會拋出UncheckedExecutionException
,它是一個RuntimeException。
2.2 CacheLoader.loadAll(keys) 批量加載
在客戶端調(diào)用LoadingCache.getAll
的時候,會優(yōu)先嘗試CacheLoader.loadAll(Iterable<? extends K> keys)
方法,這個方法默認實現(xiàn)是拋出UnsupportedLoadingOperationException
,LocalCache
默認優(yōu)先嘗試調(diào)用ClassLoader.loadAll
,如果異常則挨個Key調(diào)用CacheLoader.load(K)
并組成Map<Key,Value>返回。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("going to load from data, key:" + s); return s.matches("\\d+") ? Integer.parseInt(s) : -1; } @Override public Map<String, Integer> loadAll(Iterable<? extends String> keys) throws Exception { System.out.println("going to loadAll from data, keys:" + keys); Map<String, Integer> result = new LinkedHashMap<>(); for (String s : keys) { result.put(s, s.matches("\\d+") ? Integer.parseInt(s) : -1); } result.put("99", 99); result.put("WhatIsTheFuck", 100); return result; } }); System.out.println(cache.get("10")); List<String> ls = Lists.newArrayList("1", "2", "a"); System.out.println(cache.getAll(ls)); System.out.println(cache.get("WhatIsTheFuck"));
getAll
調(diào)用CacheLoader.loadAll
,該方法返回一個Map,可以包含非指定Key數(shù)據(jù),整個Map會被緩存,但getAll
只返回指定的Key的數(shù)據(jù)。
2.3 Callable.call
所有Guava Cache的實現(xiàn)類都支持get(K, Callable<V>)
方法, 返回K對應(yīng)的緩存,或者使用Callable<V>
計算新值并存入緩存,實現(xiàn)get-if-absent-compute
。
相同的Key如果有多個調(diào)用同時進入,Guava保證只有一個線程在加載,且其他線程會阻塞等待加載結(jié)果。
Guava Cache內(nèi)部使用了類型ConcurrentHashMap的概念,為了將鎖分片,減少race-condition發(fā)生的范圍。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(10).build(); final String key = "2"; Integer value = cache.get(key, new Callable<Integer>() { public Integer call() throws Exception { System.out.println("Callable.call running, key:" + key); return key.matches("\\d+") ? Integer.parseInt(key) : -1; } }); System.out.println(value); System.out.println(value);
2.4 手工寫入
我們可以通過cache.put(key,value)
直接寫入緩存,寫入會覆蓋之前的值。 也可以通過cache.asMap()
視圖來操作數(shù)據(jù)。 cache.asMap()
并不會促發(fā)緩存的自動加載,應(yīng)該盡可能使用cache.put
和cache.get
。
Cache<String,Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build(); cache.put("1",1); cache.put("2",2); cache.put("3",3); cache.put("4",4); System.out.println(cache.asMap().get("1")); // 因為最多緩存3個,get("1")數(shù)據(jù)被清除,返回null System.out.println(cache.asMap().get("2"));
3. 緩存清除
現(xiàn)實實際我們總是不可能有足夠的內(nèi)存來緩存所有數(shù)據(jù)的,你總是需要關(guān)注緩存的清除策略。
3.1 基于maximumSize的清除
用于控制緩存的大小,通過CacheBuilder.maximumSize(long)
,當(dāng)緩存的數(shù)據(jù)項解決maximum的數(shù)量時,采用類似LRU的算法過期歷史數(shù)據(jù)。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build(); cache.put("1", 1); cache.put("2", 2); cache.put("3", 3); cache.put("4", 4); System.out.println(cache.asMap().get("1")); // 因為最多緩存3個,get("1")數(shù)據(jù)被清除,返回null System.out.println(cache.asMap().get("2"));
3.2 基于maximumWeight的清除
和maximun類似,只是統(tǒng)計的weight而不是緩存的記錄數(shù)。
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumWeight(10).weigher(new Weigher<String, Integer>() { public int weigh(String s, Integer integer) { return integer; } }).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading from CacheLoader, key:" + s); return Integer.parseInt(s); } });
3.3 基于時間的清除
數(shù)據(jù)寫入指定時間后過期(expireAfterWrite
),也可以指定數(shù)據(jù)一段時間沒有訪問后清除(expireAfterAccess
)。
final long start = System.nanoTime(); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).build(new CacheLoader<String, Integer>() { public Integer load(String s) throws Exception { System.out.println("loading data from CacheLoader, key:" + s); return Integer.parseInt(s); } });
測試基于時間的清除,緩存一個小時,然后我們真的等一個小時后來驗證是不現(xiàn)實的,Guava提供了Ticker類用于提供模擬時鐘,返回的是時間納秒數(shù)。
下面這個實例通過自定義Ticker,讓1s變成10分鐘(*600),緩存一個小時的數(shù)據(jù),實際過6s后數(shù)據(jù)就會過期。
final long start = System.nanoTime(); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).ticker(new Ticker() { public long read() { long current = System.nanoTime(); long diff = current - start; System.out.println("diff:" + (diff / 1000 / 1000 / 1000)); long time = start + (diff * 600); return time; } }).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading data from CacheLoader, key:" + s); return Integer.parseInt(s); } });
3.4 使用WeakReferenct、SoftReference保存Key和Value
Guava允許設(shè)置弱引用(weak reference)和軟銀用(soft reference)來引用實際的Key、Value數(shù)據(jù)。
通過CacheBuilder.weakKeys、CacheBuilder.weakValues、CacheBuilder.softValues來運行JVM的垃圾回收,同時帶來的問題是Cache的Key只用==來比較而不是equals,要想從Cache里取回之前的緩存,必須保存Key的Reference對象。
3.5 顯示的移除緩存
刪除單個Key、批量刪除Key、清空緩存
Cache.invalidate(key) Cache.invalidateAll(keys) Cache.invalidateAll()
3.6 緩存清除監(jiān)聽
不是太實用,并不是Key一過期就會觸發(fā)RemovalListener回調(diào),你需要再次寫入數(shù)據(jù)的時候才會觸發(fā)同一個Segment的過期,Cache.get官網(wǎng)文檔說特定條件下也會觸發(fā)清空過期數(shù)據(jù)。
Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).expireAfterWrite(10, TimeUnit.SECONDS) .removalListener(new RemovalListener<String, Integer>() { public void onRemoval(RemovalNotification<String, Integer> r) { System.out.println("Key:" + r.getKey()); System.out.println("Value:" + r.getValue()); System.out.println("Cause:" + r.getCause()); } }).build();
4. 緩存的清除時機
Cache不會自動的清除緩存,不會在數(shù)據(jù)過期后立即就清除,只有發(fā)生寫入動作(如Cache.put)才會觸發(fā)清除動作(包括LoadingCache.get新加載數(shù)據(jù)也會清除當(dāng)前Segement過期數(shù)據(jù))。
這樣做的目的好處是不用額外維護一個線程做緩存管理動作,如果想要定期清除,開發(fā)者可以自行創(chuàng)建一個線程,定期調(diào)用Cache.cleanUp()
方法。
4.1 通過refresh優(yōu)化讀取性能
LoadingCache.refresh(K)
和清除緩存(eviction)不同,refresh會導(dǎo)致Cache重新加載Key對應(yīng)的值,加載期間,老的值依然可用; 而清除(eviction)之后,其他現(xiàn)在再來取值會阻塞直至新數(shù)據(jù)加載完成。
CacheLoader.reload(K,V)
方法是專門處理refresh提供的方法,refresh調(diào)用后實際會調(diào)用CacheLoader.reload(K,V)
方法,這個方法的第2個入?yún)嶋H是當(dāng)前K的歷史值。
通過CacheBuilder.refreshAfterWrite(long,TimeUnit)
設(shè)定,Key在寫入Cache指定時間區(qū)間后,自動刷新Key的值,而此時歷史數(shù)據(jù)仍然對外提供服務(wù)。
CacheBuilder.refreshAfterWrite(long,TimeUnit)
只會在下次查詢的時候生效,你可以同時指定refreshAfterWrite和expireAfterWrite,這樣在指定的時間段過了之后,如果數(shù)據(jù)還沒有被查詢,數(shù)據(jù)會把清除。
final ScheduledExecutorService es = Executors.newScheduledThreadPool(5); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading from load...s:" + s); return Integer.parseInt(s); } @Override public ListenableFuture<Integer> reload(final String key, final Integer oldValue) throws Exception { if (oldValue > 5) { // 立即返回舊值 System.out.println("loading from reload immediate...key:" + key); return Futures.immediateFuture(oldValue); } else { ListenableFutureTask<Integer> fi = ListenableFutureTask.create(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("loading from reload...key:" + key); return oldValue; } }); es.execute(fi); return fi; } } });
5. 緩存性能指標(biāo)
通過調(diào)用CacheBuilder.recordStats()
可以打開統(tǒng)計功能,打開功能后可以通過Cache.stats()返回統(tǒng)計信息
LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).recordStats().build(new CacheLoader<String, Integer>() { public Integer load(String s) throws Exception { return Integer.parseInt(s); } }); CacheStats stats = cache.stats(); System.out.println(stats.hitRate()); // 緩存命中率 System.out.println(stats.averageLoadPenalty()); // 平均數(shù)加載時間,單位納秒 System.out.println(stats.evictionCount()); // 緩存過期數(shù)據(jù)數(shù)
6. 原理、長處和限制
LocalLoadingCache通過公式Math.min(concurrencyLevel, maxWeight / 20)
計算Segment數(shù)量,數(shù)據(jù)根據(jù)key的Hash值被分散到不同的Segment中。
默認的concurrencyLevel是4,相當(dāng)于默認情況下Segment數(shù)量最大就是4。
LocalLoadingCache指定Capacity,默認是16,Capacity會轉(zhuǎn)換為大于指定Capacity的最小的2冪次方。
SegmentCapacity等于Capacity/SegmentCount
, 轉(zhuǎn)換為大于SegmentCapacity的最小的2冪次方。
SegmentCapacity的值指定了Segment下AtomicReferenceArray的長度,AtomicReferenceArray每一個下標(biāo)對應(yīng)一個鏈表。
SegmentCount和SegmentCapacity決定了緩存數(shù)據(jù)被切分的份數(shù),相當(dāng)于決定了查找效率。
Segment內(nèi)部還維護著writeQueue、accessQueue、recencyQueue每一次讀寫操作都會更新對應(yīng)隊列,后續(xù)expireAfterWrite、expireAfterAccess只需要順著隊列找即可,因為隊列的順序就是操作的順序, writeQueue、accessQueue是特制的隊列,只用簡單的鏈表實現(xiàn),從鏈表移除插入都很高效。
Segement還維護了keyReferenceQueue、valueReferenceQueue,他們是Java里的ReferenceQueue,當(dāng)采用WeakReference、SoftReference做為Key/Value存儲時,自動加入到keyReferenceQueue和valueReferenceQueue中,Guava處理并刪除對應(yīng)的緩存。
7. 測試代碼
package com.hujiang.track.pageview; import com.google.common.base.Ticker; import com.google.common.cache.*; import com.google.common.collect.Lists; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListenableFutureTask; import org.junit.Test; import java.text.SimpleDateFormat; import java.util.Date; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.concurrent.*; public class TestCache { @Test public void testCache() throws ExecutionException { LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(100).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("going to load from data, key:" + s); return s.matches("\\d+") ? Integer.parseInt(s) : -1; } @Override public Map<String, Integer> loadAll(Iterable<? extends String> keys) throws Exception { System.out.println("going to loadAll from data, keys:" + keys); Map<String, Integer> result = new LinkedHashMap<>(); for (String s : keys) { result.put(s, s.matches("\\d+") ? Integer.parseInt(s) : -1); } result.put("99", 99); result.put("WhatIsTheFuck", 100); return result; } }); System.out.println(cache.get("10")); System.out.println(cache.get("20")); System.out.println(cache.get("a0")); List<String> ls = Lists.newArrayList("1", "2", "a"); System.out.println(cache.getAll(ls)); System.out.println(cache.get("WhatIsTheFuck")); } @Test public void testCallable() throws ExecutionException { Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(10).build(); final String key = "2"; Integer value = cache.get(key, new Callable<Integer>() { public Integer call() throws Exception { System.out.println("Callable.call running, key:" + key); return key.matches("\\d+") ? Integer.parseInt(key) : -1; } }); System.out.println(value); System.out.println(value); } @Test public void testPut() { Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).build(); cache.put("1", 1); cache.put("2", 2); cache.put("3", 3); cache.put("4", 4); System.out.println(cache.asMap().get("1")); // 因為最多緩存3個,get("1")數(shù)據(jù)被清除,返回null System.out.println(cache.asMap().get("2")); } @Test public void testWeight() throws ExecutionException { LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumWeight(10).weigher(new Weigher<String, Integer>() { public int weigh(String s, Integer integer) { return integer; } }).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading from CacheLoader, key:" + s); return Integer.parseInt(s); } }); cache.get("1"); cache.get("3"); cache.get("5"); cache.get("1"); cache.get("7"); cache.get("1"); cache.get("3"); } @Test public void testTimeEviction() throws InterruptedException, ExecutionException { System.out.println("nano:" + System.nanoTime()); System.out.println("ms :" + System.currentTimeMillis()); final long start = System.nanoTime(); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().expireAfterWrite(1, TimeUnit.HOURS).ticker(new Ticker() { public long read() { long current = System.nanoTime(); long diff = current - start; System.out.println("diff:" + (diff / 1000 / 1000 / 1000)); long time = start + (diff * 600); return time; } }).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading data from CacheLoader, key:" + s); return Integer.parseInt(s); } }); System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())); System.out.println(cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); TimeUnit.SECONDS.sleep(1); System.out.println("time:" + (new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date())) + "-------" + cache.get("1")); } @Test public void testWeakKeys() { CacheBuilder.newBuilder().weakKeys().weakValues().build(); } @Test public void testRemovalListener() throws InterruptedException { Cache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).expireAfterWrite(10, TimeUnit.SECONDS).removalListener(new RemovalListener<String, Integer>() { public void onRemoval(RemovalNotification<String, Integer> r) { System.out.println("Key:" + r.getKey()); System.out.println("Value:" + r.getValue()); System.out.println("Cause:" + r.getCause()); } }).build(); cache.put("1", 1); cache.put("2", 2); cache.put("3", 3); cache.put("4", 4); TimeUnit.SECONDS.sleep(11); System.out.println("get-from-cache-2:" + cache.getIfPresent("2")); cache.put("2", 3); TimeUnit.SECONDS.sleep(11); } @Test public void testEvict() throws ExecutionException { LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(2).removalListener(new RemovalListener<String, Integer>() { public void onRemoval(RemovalNotification<String, Integer> r) { System.out.println("Key:" + r.getKey() + ", Value:" + r.getValue() + ", Cause:" + r.getCause()); } }).recordStats().build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("CacheLoader.load key:" + s); return Integer.parseInt(s); } }); System.out.println(cache.get("2")); System.out.println(cache.get("5")); System.out.println(cache.get("6")); System.out.println(cache.get("1")); } @Test public void testStatistics() { LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).recordStats().build(new CacheLoader<String, Integer>() { public Integer load(String s) throws Exception { return Integer.parseInt(s); } }); CacheStats stats = cache.stats(); System.out.println(stats.hitRate()); // 緩存命中率 System.out.println(stats.averageLoadPenalty()); // 平均數(shù)加載時間,單位納秒 System.out.println(stats.evictionCount()); // 緩存過期數(shù)據(jù)數(shù) } @Test public void testRefresh() throws ExecutionException, InterruptedException { final ScheduledExecutorService es = Executors.newScheduledThreadPool(5); LoadingCache<String, Integer> cache = CacheBuilder.newBuilder().maximumSize(3).refreshAfterWrite(3, TimeUnit.SECONDS).build(new CacheLoader<String, Integer>() { @Override public Integer load(String s) throws Exception { System.out.println("loading from load...s:" + s); return Integer.parseInt(s); } @Override public ListenableFuture<Integer> reload(final String key, final Integer oldValue) throws Exception { if (oldValue > 5) { // 立即返回舊值 System.out.println("loading from reload immediate...key:" + key); return Futures.immediateFuture(oldValue); } else { ListenableFutureTask<Integer> fi = ListenableFutureTask.create(new Callable<Integer>() { @Override public Integer call() throws Exception { System.out.println("loading from reload...key:" + key); return oldValue; } }); es.execute(fi); return fi; } } }); cache.get("5"); cache.get("6"); TimeUnit.SECONDS.sleep(4); cache.get("5"); cache.get("6"); } }
到此這篇關(guān)于使用Guava Cache做緩存的文章就介紹到這了,更多相關(guān)Guava Cache緩存內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot2底層注解@Configuration配置類詳解
這篇文章主要為大家介紹了SpringBoot2底層注解@Configuration配置類詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2022-05-05Java?數(shù)據(jù)結(jié)構(gòu)與算法系列精講之隊列
這篇文章主要介紹了Java隊列數(shù)據(jù)結(jié)構(gòu)的實現(xiàn),隊列是一種特殊的線性表,只允許在表的隊頭進行刪除操作,在表的后端進行插入操作,隊列是一個有序表先進先出,想了解更多相關(guān)資料的小伙伴可以參考下面文章的詳細內(nèi)容2022-02-02Java如何獲取當(dāng)前進程ID以及所有Java進程的進程ID
本篇文章主要介紹了Java如何獲取當(dāng)前進程ID以及所有Java進程的進程ID,具有一定的參考價值,感興趣的小伙伴們可以參考一下2017-06-06SpringBoot集成Redis向量數(shù)據(jù)庫實現(xiàn)相似性搜索功能
Redis?是一個開源(BSD?許可)的內(nèi)存數(shù)據(jù)結(jié)構(gòu)存儲,用作數(shù)據(jù)庫、緩存、消息代理和流式處理引擎,向量檢索的核心原理是通過將文本或數(shù)據(jù)表示為高維向量,并在查詢時根據(jù)向量的相似度進行搜索,本文給大家介紹了SpringBoot集成Redis向量數(shù)據(jù)庫實現(xiàn)相似性搜索功能2024-09-09