https://my.oschina.net/u/2270476/blog/1805749
http://www.cnblogs.com/parryyang/p/5777019.html
https://www.jianshu.com/p/5299f5a11bd5
https://www.cnblogs.com/vikde/p/8045226.html
https://blog.csdn.net/fly910905/article/details/80976161
https://www.cnblogs.com/yuxiang1/p/9282952.html 解析Java分布式系統中的緩存架構(上)
google guava cache使用方法
1.簡介
guava cache是google guava中的一個內存緩存模塊,用於將數據緩存到JVM內存中.實際項目開發中經常將一些比較公共或者常用的數據緩存起來方便快速訪問.
內存緩存最常見的就是基於HashMap實現的緩存,為了解決並發問題也可能也會用到ConcurrentHashMap等並發集合,但是內存緩存需要考慮很多問題,包括並發問題、緩存過期機制、緩存移除機制、緩存命中統計率等.
guava cache已經考慮到這些問題,可以上手即用.通過CacheBuilder創建緩存、然后設置緩存的相關參數、設置緩存的加載方法等.在多線程高並發場景中往往是離不開cache的,需要根據不同的應用場景來需要選擇不同的cache,比如分布式緩存如Redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCache。Guava Cache與ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache為了限制內存占用,通常都設定為自動回收元素。
cache初始化
final static Cache<Integer, String> cache =
CacheBuilder.newBuilder()
//設置cache的初始大小為10,要合理設置該值
.initialCapacity(10)
//設置並發數為5,即同一時間最多只能有5個線程往cache執行寫入操作
.concurrencyLevel(5)
//設置cache中的數據在寫入之后的存活時間為10秒
.expireAfterWrite(10, TimeUnit.SECONDS)
//構建cache實例
.build();
// 放入緩存 cache.put("key", "value");
// 獲取緩存 String value = cache.getIfPresent("key");
LoadingCache
LoadingCache繼承自Cache,在構建LoadingCache時,需要通過CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法構建:
LoadingCache,顧名思義,它能夠通過CacheLoader自發的加載緩存:如果有緩存則返回;否則運算、緩存、然后返回 CacheBuilder.newBuilder() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 緩存加載邏輯 ... } });
2.緩存回收
Guava Cache提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。
基於容量的回收(size-based eviction)
如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。
定時回收(Timed Eviction)
CacheBuilder提供兩種定時回收的方法:
expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。
expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
定時回收周期性地在寫操作中執行,偶爾在讀操作中執行。
基於引用的回收(Reference-based Eviction)
通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。因為垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。
顯式清除
任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有緩存項:Cache.invalidateAll()
移除監聽器
通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個監聽器,以便緩存項被移除時做一些額外操作。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值
清理什么時候發生?
使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。
這樣做的原因在於:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權交到你手里。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的 緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度。
緩存刷新
在Guava cache中支持定時刷新和顯式刷新兩種方式,其中只有LoadingCache能夠進行定時刷新。
在進行緩存定時刷新時,我們需要指定緩存的刷新間隔,和一個用來加載緩存的CacheLoader,當達到刷新時間間隔后,下一次獲取緩存時,會調用CacheLoader的load方法刷新緩存。例如構建個刷新頻率為10分鍾的緩存:
CacheBuilder.newBuilder() // 設置緩存在寫入10分鍾后,通過CacheLoader的load方法進行刷新 .refreshAfterWrite(10, TimeUnit.SECONDS) // jdk8以后可以使用 Duration // .refreshAfterWrite(Duration.ofMinutes(10)) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { // 緩存加載邏輯 ... } });
Guava Cache緩存回收:
- 基於容量和定時的回收
-
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .expireAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
- 如代碼所示,新建了名為caches的一個緩存對象,maximumSize定義了緩存的容量大小,當緩存數量即將到達容量上線時,則會進行緩存回收,回收最近沒有使用或總體上很少使用的緩存項。需要注意的是在接近這個容量上限時就會發生,所以在定義這個值的時候需要視情況適量地增大一點。
- 另外通過expireAfterWrite這個方法定義了緩存的過期時間,寫入十分鍾之后過期。
-
CacheBuilder提供兩種定時回收的方法:
-
-
expireAfterAccess( long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。
-
請注意這種緩存的回收順序和基於大小回收一樣。
-
-
expireAfterWrite( long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。
-
如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
-
- 在build方法里,傳入了一個CacheLoader對象,重寫了其中的load方法。當獲取的緩存值不存在或已過期時,則會調用此load方法,進行緩存值的計算。
- 這就是最簡單也是我們平常最常用的一種使用方法。定義了緩存大小、過期時間及緩存值生成方法。
- 如果用其他的緩存方式,如redis,我們知道上面這種“如果有緩存則返回;否則運算、緩存、然后返回”的緩存模式是有很大弊端的。當高並發條件下同時進行get操作,而此時緩存值已過期時,會導致大量線程都調用生成緩存值的方法,比如從數據庫讀取。這時候就容易造成數據庫雪崩。這也就是我們常說的“緩存穿透”。
- 而Guava cache則對此種情況有一定控制。當大量線程用相同的key獲取緩存值時,只會有一個線程進入load方法,而其他線程則等待,直到緩存值被生成。這樣也就避免了緩存穿透的危險。
Guava Cache定時刷新:
如上的使用方法,雖然不會有緩存穿透的情況,但是每當某個緩存值過期時,老是會導致大量的請求線程被阻塞。而Guava則提供了另一種緩存策略,緩存值定時刷新:更新線程調用load方法更新該緩存,其他請求線程返回該緩存的舊值。這樣對於某個key的緩存來說,只會有一個線程被阻塞,用來生成緩存值,而其他的線程都返回舊的緩存值,不會被阻塞。
這里就需要用到Guava cache的refreshAfterWrite方法。
LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
如代碼所示,每隔十分鍾緩存值則會被刷新。
此外需要注意一個點,這里的定時並不是真正意義上的定時。Guava cache的刷新需要依靠用戶請求線程,讓該線程去進行load方法的調用,所以如果一直沒有用戶嘗試獲取該緩存值,則該緩存也並不會刷新。
Guava Cache異步刷新:
如上的使用方法,解決了同一個key的緩存過期時會讓多個線程阻塞的問題,只會讓用來執行刷新緩存操作的一個用戶線程會被阻塞。由此可以想到另一個問題,當緩存的key很多時,高並發條件下大量線程同時獲取不同key對應的緩存,此時依然會造成大量線程阻塞,並且給數據庫帶來很大壓力。這個問題的解決辦法就是將刷新緩存值的任務交給后台線程,所有的用戶請求線程均返回舊的緩存值,這樣就不會有用戶線程被阻塞了。
詳細做法如下:
ListeningExecutorService backgroundRefreshPools = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(20)); LoadingCache<String, Object> caches = CacheBuilder.newBuilder() .maximumSize(100) .refreshAfterWrite(10, TimeUnit.MINUTES) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return generateValueByKey(key); } @Override public ListenableFuture<Object> reload(String key, Object oldValue) throws Exception { return backgroundRefreshPools.submit(new Callable<Object>() { @Override public Object call() throws Exception { return generateValueByKey(key); } }); } }); try { System.out.println(caches.get("key-zorro")); } catch (ExecutionException e) { e.printStackTrace(); }
在上面的代碼中,我們新建了一個線程池,用來執行緩存刷新任務。並且重寫了CacheLoader的reload方法,在該方法中建立緩存刷新的任務並提交到線程池。
注意此時緩存的刷新依然需要靠用戶線程來驅動,只不過和上面不同之處在於該用戶線程觸發刷新操作之后,會立馬返回舊的緩存值。