適用性
緩存在很多場景下都是相當有用的。例如,計算或檢索一個值的代價很高,並且對同樣的輸入需要不止一次獲取值的時候,就應當考慮使用緩存
Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache為了限制內存占用,通常都設定為自動回收元素。在某些場景下,盡管LoadingCache 不回收元素,它也是很有用的,因為它會自動加載緩存
通常來說,Guava Cache適用於:
-
- 你願意消耗一些內存空間來提升速度。
- 你預料到某些鍵會被查詢一次以上。
- 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。如果這不符合你的需求,請嘗試Redis這類工具)
如果你的場景符合上述的每一條,Guava Cache就適合你。
如果你不需要Cache中的特性,使用ConcurrentHashMap有更好的內存效率——但Cache的大多數特性都很難基於舊有的ConcurrentMap復制,甚至根本不可能做到
代碼詳解
1 /** 2 * @author LiuHuan 3 * @date 2020-06-17 15:52 4 * @desc Guava Cache學習 5 */ 6 public class GuavaCacheTest { 7 8 public static void main(String[] args) throws ExecutionException { 9 GuavaCacheTest test = new GuavaCacheTest(); 10 Cache<String, String> cache = test.getGuavaCache(); 11 // 放入/覆蓋一個緩存 12 cache.put("key", "value"); 13 // 獲取一個緩存,如果該緩存不存在則返回一個null值 14 cache.getIfPresent(""); 15 // 獲取緩存,當緩存不存在時,則通Callable進行加載並返回。該操作是原子 16 cache.get("key", () -> loadingValue("key")); 17 // 回收key為k1的緩存 18 cache.invalidate("key"); 19 // 使用Map的put方法進行覆蓋刷新 20 cache.asMap().put("key", "value"); 21 // 使用ConcurrentMap的replace方法進行覆蓋刷新 22 cache.asMap().replace("key", "value1"); 23 // 使用Map的putAll方法進行批量覆蓋刷新 24 Map<String,String> needRefresh = ImmutableMap.of("key1","value1", "key2", "value2"); 25 cache.asMap().putAll(needRefresh); 26 // 批量回收key為key1、key2的緩存 27 List<String> needInvalidateKeys = Arrays.asList("key1", "key2"); 28 cache.invalidateAll(needInvalidateKeys); 29 // 回收所有緩存 30 cache.invalidateAll(); 31 32 // 用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()方法會返回CacheStats對象 33 CacheStats stats = cache.stats(); 34 // 緩存命中率 35 double hitRate = stats.hitRate(); 36 // 加載新值的平均時間,單位為納秒 37 double averageLoadPenalty = stats.averageLoadPenalty(); 38 // 緩存項被回收的總數,不包括顯式清除 39 long evictionCount = stats.evictionCount(); 40 41 LoadingCache<String, String> loadingCache = test.getGuavaLoadingCache(); 42 // loadingCache 在進行刷新時無需顯式的傳入value 43 loadingCache.refresh("key"); 44 } 45 46 /** 47 * 獲取GuavaCache實例 48 * @return 49 */ 50 public Cache<String, String> getGuavaCache(){ 51 // 異步觸發監聽器 52 RemovalListener<Object, Object> removalListener = RemovalListeners.asynchronous(removal -> { 53 // 如果被顯示移除這里為true 54 boolean wasEvicted = removal.wasEvicted(); 55 // 移除的原因 56 RemovalCause cause = removal.getCause(); 57 }, Executors.newSingleThreadExecutor()); 58 59 // 通過CacheBuilder構建一個緩存實例 60 Cache<String, String> cache = CacheBuilder.newBuilder() 61 // 由於Guava的緩存使用了分離鎖的機制,擴容的代價非常昂貴,所以合理的初識容量能夠減少擴容次數 62 .initialCapacity(100) 63 // 設置緩存的最大條數 64 .maximumSize(100) 65 // maximumWeight邏輯上用來表示一種“權重”,這里與maximumSize沖突,設置一個即可 66 // 這里我們將key和value所占的字節數,作為weight,當cache中所有的“weight”總和達到maximumWeight時,將會觸發“剔除策略” 67 .maximumWeight(1024 * 1024) 68 .weigher((Weigher<String, String>)(key, value) -> key.getBytes().length + value.getBytes().length) 69 // 使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,該緩存可能會被回收 70 .weakKeys() 71 // 使用弱引用存儲值。當值沒有其它(強或軟)引用時,該緩存可能會被回收 72 .weakValues() 73 // 使用軟引用存儲值。當內存不足並且該值其它強引用引用時,該緩存就會被回收 74 // 通過軟/弱引用的回收方式,相當於將緩存回收任務交給了GC,使得緩存的命中率變得十分的不穩定,在非必要的情況下,還是推薦基於數量和容量的回收 75 .softValues() 76 // 設置緩存在寫入一分鍾后失效 77 .expireAfterWrite(1, TimeUnit.MINUTES) 78 // 設置緩存最后一次訪問10分鍾之后失效,與session類似。與expireAfterWrite沖突,設置一個即可 79 .expireAfterAccess(Duration.ofMinutes(10)) 80 // 開啟緩存統計 81 .recordStats() 82 // 設置並發級別為CPU核心數 83 .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 84 // 移除監聽器,這里是移除緩存時同步調用,會拖慢正常的請求 85 .removalListener(removal -> { 86 // 如果被顯示移除這里為true 87 boolean wasEvicted = removal.wasEvicted(); 88 // 移除的原因 89 RemovalCause cause = removal.getCause(); 90 }) 91 // 異步觸發監聽器 92 .removalListener(removalListener) 93 .build(); 94 return cache; 95 } 96 97 /** 98 * 獲取GuavaLoadingCache實例,會有默認的緩存加載策略 99 * @return 100 */ 101 public LoadingCache<String, String> getGuavaLoadingCache(){ 102 // 通過CacheBuilder構建一個緩存實例 103 LoadingCache<String, String> cache = CacheBuilder.newBuilder() 104 // 設置緩存在寫入10分鍾后,通過CacheLoader的load方法進行刷新 105 .refreshAfterWrite(Duration.ofMinutes(10)) 106 .build(new CacheLoader<String, String>() { 107 @Override 108 public String load(String key) throws Exception { 109 // 緩存加載策略 110 return loadingValue(key); 111 } 112 113 }); 114 return cache; 115 } 116 117 /** 118 * 緩存加載策略 119 * @param key 120 * @return 121 */ 122 private static String loadingValue(String key){ 123 return null; 124 }; 125 126 }
緩存清理
使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護工作,如果寫操作實在太少的話,偶爾在讀操作時做這個操作。這樣做的原因在於:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。
如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度