1、ehcahce 什么時候用比較好;
2、問題:當有個消息的key不在guava里面的話,如果大量的消息過來,會同時請求數據庫嗎?還是只有一個請求數據庫,其他的等待第一個把數據從DB加載到Guava中
回答:是的,其他的都會等待load,直到數據加載完畢;
2、recency queue 干嘛用的:
目前沒看出來,但是應該是為了LRU隊列也就是快速刪除算法,因為recency queue的隊列,如果讀的話,會往recency queue和 access queue中寫入數據,如果寫的話,首先要清空recency queue隊列,然后在recency queue中,然后再在access queue中寫入隊列;所以應該會為了快速刪除過期數據准備的queue:
背景:
目前在網安部項目中,會接收到LBS消息 高峰期的QPS大約為5000,目前是直接通過LBS消息的訂單ID查詢 查詢訂單接口的數據,由於涉及到上游部署,或者網絡抖動的問題,當上游積壓時,訂單經常會報警。因此考慮對緩存做一次調研。
在多線程高並發場景中往往是離不開cache的,需要根據不同的應用場景來需要選擇不同的cache,比如分布式緩存如Redis、memcached,還有本地(進程內)緩存如ehcache、GuavaCache
緩存分為本地緩存和分布式緩存, 為什么要使用本地緩存呢?因為本地緩存比IO更高效,比分布式緩存更穩定。。
分布式緩存主要為redis或memcached之類的稱為分布式緩存,
優點:
Redis 容量大,可以持久化,可以實現分布式的緩存,可以處理每秒百萬級的並發,是專業的緩存服務,
redis可單獨部署,多個項目之間可以共享,本地內存無法共享;
在多實例的情況下,各實例共用一份緩存數據,緩存具有一致性。
缺點:
需要保持redis或memcached服務的高可用,整個程序架構上較為復雜,硬件成本較高
本地緩存主要為Ecache和 guava Cache
區別:
- Ehcache支持持久化到本地磁盤,Guava不可以;
- Ehcache有現成的集群解決方案,Guava沒有。不過個人感覺比較雞肋,對JVM級別的緩存來講太重了;
- Ehcache jar包龐大,Guava Cache只是Guava jar包中的工具之一,而且后者遠遠小於Ehcache;
- 兩種緩存當緩存過期或者沒有命中的時候都可以通過load接口重載數據,調用方式略有不同。兩者的主要區別是Ehcache的緩存load的時候,允許用戶返回null,而Guava Cache則不允許返回為null,因為Guava Cache是根據value的值是否為null來判斷是否需要load,所以不允許返回為null,但是使用的時候可以使用空對象替換。不允許返回null是一個很好的考慮;
- Ehcache有內存占用大小統計,Guava Cache沒有,需要自己開發;
- Ehcache在put緩存的時候,對K、V都做了包裝,對GC有一定影響。
適用Ehcache的情況
- 需要使用持久化功能需要,緩存穩定,以免持久化的數據不准確影響結果。
- 有集群解決方案。
適用Guava cache的情況
Guava cache說簡單點就是一個支持LRU的ConCurrentHashMap,它沒有Ehcache那么多的各種特性,只是提供了增、刪、改、查、刷新規則和時效規則設定等最基本的元素。做一個jar包中的一個功能之一,Guava cache極度簡潔並能滿足覺大部分人的要求。
Guava Cache 優點:
- 采用鎖分段技術,鎖粒度減小,加大並發。
- API優雅,簡單可用,支持多種回收方式。
- 自帶統計功能。
- Guava Cache的超時機制不是精確的;
缺點:
- 受內存大小限制不能存儲太多數據
- 單JVM有效,非分布式緩存。多台服務可能有不同效果。
- 不能持久化本地緩存;
使用場景
願意花費一部分內存來提高速度 -- 以空間換時間
期待有些關鍵字會被多次查詢 -- 熱點數據
不需要持久化
緩存中存放的數據總量不會超出內存容量。
總結
Ehcache有着全面的緩存特性,但是略重。Guava cache有最基本的緩存特性,很輕。
兩種類型都是成熟的緩存框架,由於不需要保存到本地磁盤 考慮到Ehcahce 比較重,而Guava 比較輕量,考慮使用Guava
二、Guava Cache緩存數據結構
Guava工程包含了若干被Google的 Java項目廣泛依賴 的核心庫;Google Guava Cache是一種非常優秀本地緩存解決方案,提供了基於容量,時間和引用的緩存回收方式。
Guava Cache 其核心數據結構大體上和 ConcurrentHashMap 一致,具體細節上會有些區別。功能上,ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地, Guava Cache 為了限制內存占用,通常都設定為自動回收元素。在某些場景下,盡管它不回收元素,也是很有用的,因為它會自動加載緩存。
Guava Cache與java1.7的ConcurrentMap很相似,但也不完全一樣。最基本的區別是ConcurrentMap會一直保存所有添加的元素,直到顯式地移除。相對地,Guava Cache為了限制內存占用,通常都設定為自動回收元素。
在這里就會涉及到segement的概念了,我們先把關系理清楚,首先看ConcurrentHashMap的圖示,這樣有助於我們理解:
Guava Cache中的核心類,重點了解。
2.1 LocalCache數據結構
ocalCache為Guava Cache的核心類,先看一個該類的數據結構: LocalCache的數據結構與ConcurrentHashMap很相似,都由多個segment組成,且各segment相對獨立,互不影響,所以能支持並行操作。每個segment由一個table和若干隊列組成。緩存數據存儲在table中,其類型為AtomicReferenceArray。如下圖所示 一個table 還有 5個queue;
對於每一個Segment 放大如下:包含了一個table 和5個隊列;
LocalCache類似ConcurrentHashMap采用了分段策略,通過減小鎖的粒度來提高並發,LocalCache中數據存儲在Segment[]中,每個segment又包含5個隊列和一個table
緩存核心類LocalCache,包含了Segment如下所示:
@GwtCompatible(emulated = true)
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> { final Segment<K, V>[] segments; @Nullable final CacheLoader<? super K, V> defaultLoader;
內部類Segment與jdk1.7及以前的ConcurrentHashMap非常相似,都繼承於ReetrantLock,
static class Segment<K, V> extends ReentrantLock {
@Weak final LocalCache<K, V> map; final ReferenceQueue<K> keyReferenceQueue;//key引用隊列 final ReferenceQueue<V> valueReferenceQueue;//value引用隊列 final Queue<ReferenceEntry<K, V>> recencyQueue;// LRU隊列 @GuardedBy("this") final Queue<ReferenceEntry<K, V>> writeQueue; // 寫隊列 @GuardedBy("this") final Queue<ReferenceEntry<K, V>> accessQueue; //訪問隊列
Segment的構造函數:
Segment( LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) { this.map = map; this.maxSegmentWeight = maxSegmentWeight; this.statsCounter = checkNotNull(statsCounter); initTable(newEntryArray(initialCapacity)); keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; recencyQueue = map.usesAccessQueue() ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); writeQueue = map.usesWriteQueue() ? new WriteQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); accessQueue = map.usesAccessQueue() ? new AccessQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); }
里面有一個table,五個queue,分別表示:讀,寫,最近使用,key,value 的queue;
里面涉及到引用的使用:
在 JDK1.2 之前這點設計的非常簡單:一個對象的狀態只有引用和沒被引用兩種區別。
因此 1.2 之后新增了四種狀態用於更細粒度的划分引用關系:
-
強引用(Strong Reference):這種對象最為常見,比如 `A a = new A();`這就是典型的強引用;這樣的強引用關系是不能被垃圾回收的。
-
軟引用(Soft Reference):這樣的引用表明一些有用但不是必要的對象,在將發生垃圾回收之前是需要將這樣的對象再次回收。
-
弱引用(Weak Reference):這是一種比軟引用還弱的引用關系,也是存放非必須的對象。當垃圾回收時,無論當前內存是否足夠,這樣的對象都會被回收。
-
虛引用(Phantom Reference):這是一種最弱的引用關系,甚至沒法通過引用來獲取對象,它唯一的作用就是在被回收時可以獲得通知。
2.2 GuavaCache的 5個Queue的作用和 table的結構
基於引用的Entry,其實現類有弱引用Entry,強引用Entry等
ReferenceQueue<K> keyReferenceQueue;
已經被GC,需要內部清理的鍵引用隊列。
ReferenceQueue<V> valueReferenceQueue;
已經被GC,需要內部清理的值引用隊列。
Queue<ReferenceEntry<K, V>> recencyQueue;
記錄升級可訪問列表清單時的entries,當segment上達到臨界值或發生寫操作時該隊列會被清空。
Queue<ReferenceEntry<K, V>> writeQueue;
按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部。
Queue<ReferenceEntry<K, V>> accessQueue;
按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部。
table的數據結構 類型為:AtomicReferenceArray
Segment繼承於ReetrantLock,減小鎖粒度,提高並發效率。
AtomicReferenceArray<ReferenceEntry<K, V>> table;
類似於HasmMap中的table一樣,相當於entry的容器。
ReferenceEntry<K, V> referenceEntry;
(a) 這5個隊列,實現了豐富的本地緩存方案。
這些隊列,前2個是key、value引用隊列用以加速GC回收,基於引用回收很好的利用了Java虛擬機的垃圾回收機制。
后3個隊列記錄用戶的寫記錄、訪問記錄、高頻訪問順序隊列用以實現LRU算法。基於容量的方式內部實現采用LRU算法,
(b) AtomicReferenceArray是JUC包下的Doug Lea老李頭設計的類:一組對象引用,其中元素支持原子性更新, 這個table是自定義的一種類數組的結構,每個元素都包含一個ReferenceEntry<k,v>鏈表,指向next entry。 采用了ReferenceEntry的方式,引用數據存儲接口,默認強引用,對應的類圖為:
問題1:為何LRU隊列使用了 recencyQueue 隊列 因為已經有了 accessQueue
keyReferenceQueue = map.usesKeyReferences() ? new ReferenceQueue<K>() : null; valueReferenceQueue = map.usesValueReferences() ? new ReferenceQueue<V>() : null; recencyQueue = map.usesAccessQueue() ? new ConcurrentLinkedQueue<ReferenceEntry<K, V>>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); writeQueue = map.usesWriteQueue() ? new WriteQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue(); accessQueue = map.usesAccessQueue() ? new AccessQueue<K, V>() : LocalCache.<ReferenceEntry<K, V>>discardingQueue();
原因: 因為accessQueue是非線程安全的,get的時候使用並發工具ConcurrentLinkedQueue隊列添加entry,而不用lock(),
ConcurrentLinkedQueue是一個基於鏈接節點的無界非阻塞線程安全隊列,其底層數據結構是使用單向鏈表實現,它采用先進先出的規則對節點進行排序,
recencyQueue 啟用條件和accessQueue一樣。每次訪問操作都會將該entry加入到隊列尾部,並更新accessTime。如果遇到寫入操作,則將該隊列內容排干,如果accessQueue隊列中持有該這些 entry,然后將這些entry add到accessQueue隊列。注意,因為accessQueue是非線程安全的,所以如果每次訪問entry時就將該entry加入到accessQueue隊列中,就會導致並發問題。所以這里每次訪問先將entry臨時加入到並發安全的ConcurrentLinkedQueue隊列中,也就是recencyQueue中。在寫入的時候通過加鎖的方式,將recencyQueue中的數據添加到accessQueue隊列中。 如此看來,recencyQueue是為 accessQueue服務的。以便高效的實現expireAfterAccess功能。 關於使用recencyQueue的好處:get的時候使用並發工具ConcurrentLinkedQueue隊列添加entry,而不用lock(),一個是無阻賽鎖一個是阻塞鎖,
Cache類似於Map,它是存儲鍵值對的集合,然而它和Map不同的是它還需要處理evict、expire、dynamic load等邏輯,需要一些額外信息來實現這些操作。在面向對象思想中,經常使用類對一些關聯性比較強的數據做封裝,同時把操作這些數據相關的操作放到該類中。因而Guava Cache使用ReferenceEntry接口來封裝一個鍵值對,而用ValueReference來封裝Value值。這里之所以用Reference命令,是因為Guava Cache要支持WeakReference Key和SoftReference、WeakReference value。
ValueReference
對於ValueReference,因為Guava Cache支持強引用的Value、SoftReference Value以及WeakReference Value,
因而它對應三個實現類:StrongValueReference、SoftValueReference、WeakValueReference。
為了支持動態加載機制,它還有一個LoadingValueReference,在需要動態加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經在加載了,如果其他線程也要查詢該key對應的值,就能得到該引用,並且等待改值加載完成,從而保證該值只被加載一次(可以在evict以后重新加載)。在該只加載完成后,將LoadingValueReference替換成其他ValueReference類型。對新創建的LoadingValueReference,由於其內部oldValue的初始值是UNSET,它isActive為false,isLoading為false,因而此時的LoadingValueReference的isActive為false,但是isLoading為true。每個ValueReference都紀錄了weight值,所謂weight從字面上理解是“該值的重量”,它由Weighter接口計算而得。weight在Guava Cache中由兩個用途:1. 對weight值為0時,在計算因為size limit而evict是忽略該Entry(它可以通過其他機制evict);2. 如果設置了maximumWeight值,則當Cache中weight和超過了該值時,就會引起evict操作。但是目前還不知道這個設計的用途。最后,Guava Cache還定義了Stength枚舉類型作為ValueReference的factory類,它有三個枚舉值:Strong、Soft、Weak,這三個枚舉值分別創建各自的ValueReference,並且根據傳入的weight值是否為1而決定是否要創建Weight版本的ValueReference。以下是ValueReference的類
這里ValueReference之所以要有對ReferenceEntry的引用是因為在Value因為WeakReference、SoftReference被回收時,需要使用其key將對應的項從Segment的table中移除;copyFor()函數的存在是因為在expand(rehash)重新創建節點時,對WeakReference、SoftReference需要重新創建實例(個人感覺是為了保持對象狀態不會相互影響,但是不確定是否還有其他原因),而對強引用來說,直接使用原來的值即可,這里很好的展示了對彼變化的封裝思想;notifiyNewValue只用於LoadingValueReference,它的存在是為了對LoadingValueReference來說能更加及時的得到CacheLoader加載的值。
ReferenceEntry
ReferenceEntry是Guava Cache中對一個鍵值對節點的抽象。和ConcurrentHashMap一樣,Guava Cache由多個Segment組成,而每個Segment包含一個ReferenceEntry數組,每個ReferenceEntry數組項都是一條ReferenceEntry鏈。並且一個ReferenceEntry包含key、hash、valueReference、next字段。除了在ReferenceEntry數組項中組成的鏈,在一個Segment中,所有ReferenceEntry還組成access鏈(accessQueue)和write鏈(writeQueue),這兩條都是雙向鏈表,分別通過previousAccess、nextAccess和previousWrite、nextWrite字段鏈接而成。在對每個節點的更新操作都會將該節點重新鏈到write鏈和access鏈末尾,並且更新其writeTime和accessTime字段,而沒找到一個節點,都會將該節點重新鏈到access鏈末尾,並更新其accessTime字段。這兩個雙向鏈表的存在都是為了實現采用最近最少使用算法(LRU)的evict操作(expire、size limit引起的evict)。
Guava Cache中的ReferenceEntry可以是強引用類型的key,也可以WeakReference類型的key,為了減少內存使用量,還可以根據是否配置了expireAfterWrite、expireAfterAccess、maximumSize來決定是否需要write鏈和access鏈確定要創建的具體Reference:StrongEntry、StrongWriteEntry、StrongAccessEntry、StrongWriteAccessEntry等。創建不同類型的ReferenceEntry由其枚舉工廠類EntryFactory來實現,它根據key的Strongth類型、是否使用accessQueue、是否使用writeQueue來決定不同的EntryFactry實例,並通過它創建相應的ReferenceEntry實例。ReferenceEntry類圖如下:
WriteQueue和AccessQueue
為了實現最近最少使用算法,Guava Cache在Segment中添加了兩條鏈:write鏈(writeQueue)和access鏈(accessQueue),這兩條鏈都是一個雙向鏈表,通過ReferenceEntry中的previousInWriteQueue、nextInWriteQueue和previousInAccessQueue、nextInAccessQueue鏈接而成,但是以Queue的形式表達。WriteQueue和AccessQueue都是自定義了offer、add(直接調用offer)、remove、poll等操作的邏輯,對於offer(add)操作,如果是新加的節點,則直接加入到該鏈的結尾,如果是已存在的節點,則將該節點鏈接的鏈尾;對remove操作,直接從該鏈中移除該節點;對poll操作,將頭節點的下一個節點移除,並返回。
對於不需要維護WriteQueue和AccessQueue的配置(即沒有expire time或size limit的evict策略)來說,我們可以使用DISCARDING_QUEUE以節省內存:
2.3 Guava Cache數據結構大類
先看一下google cache 核心類如下:
-
CacheBuilder:類,緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。
CacheBuilder在build方法中,會把前面設置的參數,全部傳遞給LocalCache,它自己實際不參與任何計算。采用構造器模式(Builder)使得初始化參數的方法值得借鑒,代碼簡潔易讀。
-
CacheLoader:抽象類。用於從數據源加載數據,定義load、reload、loadAll等操作。
-
Cache:接口,定義get、put、invalidate等操作,這里只有緩存增刪改的操作,沒有數據加載的操作。
-
LoadingCache:接口,繼承自Cache。定義get、getUnchecked、getAll等操作,這些操作都會從數據源load數據。
-
LocalCache:類。整個guava cache的核心類,包含了guava cache的數據結構以及基本的緩存的操作方法。
-
LocalManualCache:LocalCache內部靜態類,實現Cache接口。其內部的增刪改緩存操作全部調用成員變量localCache(LocalCache類型)的相應方法。
-
LocalLoadingCache:LocalCache內部靜態類,繼承自LocalManualCache類,實現LoadingCache接口。其所有操作也是調用成員變量localCache(LocalCache類型)的相應方法。
GuavaCache並不希望我們設置復雜的參數,而讓我們采用建造者模式
創建Cache。GuavaCache分為兩種Cache:Cache
,LoadingCache
。LoadingCache繼承了Cache,他比Cache主要多了get和refresh方法。多這兩個方法能干什么呢?
在第四節高級特性demo中,我們看到builder生成不帶CacheLoader的Cache實例。在類結構圖中其實是生成了LocalManualCache
類實例。而帶CacheLoader的Cache實例生成的是LocalLoadingCache
。他可以定時刷新數據,因為獲取數據的方法已經作為構造參數方法存入了Cache實例中。同樣,在get時,不需要像LocalManualCache還需要傳入一個Callable實例。
實際上,這兩個Cache實現類都繼承自LocalCache
,大部分實現都是父類做的。
LocalManualCache和LocalLoadingCache的選擇
ManualCache
可以在get時動態設置獲取數據的方法,而LoadingCache
可以定時刷新數據。如何取舍?我認為在緩存數據有很多種類的時候采用第一種cache。而數據單一,數據庫數據會定時刷新時采用第二種cache。
先看下cache的類實現定義
class LocalCache<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V> {....}
我們看到了ConcurrentMap,所以我們知道了一點guava cache基於ConcurrentHashMap的基礎上設計。所以ConcurrentHashMap的優點它也具備。既然實現了 ConcurrentMap那再看下guava cache中的Segment的實現是怎樣?
我們看到guava cache 中的Segment本質是一個ReentrantLock。內部定義了table,wirteQueue,accessQueue定義屬性。其中table是一個ReferenceEntry原子類數組,里面就存放了cache的內容。wirteQueue存放的是對table的寫記錄,accessQueue是訪問記錄。guava cache的expireAfterWrite,expireAfterAccess就是借助這個兩個queue來實現的。
2.CacheBuilder構造器
private static final LoadingCache<String, String> cache = CacheBuilder.newBuilder()
.maximumSize(1000) .refreshAfterWrite(1, TimeUnit.SECONDS) //.expireAfterWrite(1, TimeUnit.SECONDS) //.expireAfterAccess(1,TimeUnit.SECONDS) .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { System.out.println(Thread.currentThread().getName() +"==load start=="+",時間=" + new Date()); // 模擬同步重載耗時2秒 Thread.sleep(2000); String value = "load-" + new Random().nextInt(10); System.out.println( Thread.currentThread().getName() + "==load end==同步耗時2秒重載數據-key=" + key + ",value="+value+",時間=" + new Date()); return value; } @Override public ListenableFuture<String> reload(final String key, final String oldValue) throws Exception { System.out.println( Thread.currentThread().getName() + "==reload ==異步重載-key=" + key + ",時間=" + new Date()); return service.submit(new Callable<String>() { @Override public String call() throws Exception { /* 模擬異步重載耗時2秒 */ Thread.sleep(2000); String value = "reload-" + new Random().nextInt(10); System.out.println(Thread.currentThread().getName() + "==reload-callable-result="+value+ ",時間=" + new Date()); return value; } }); } });
如上圖所示:CacheBuilder參數設置完畢后最后調用build(CacheLoader )構造,參數是用戶自定義的CacheLoader緩存加載器,復寫一些方法(load,reload),返回LoadingCache接口(一種面向接口編程的思想,實際返回具體實現類)如下圖:
public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(
CacheLoader<? super K1, V1> loader) { checkWeightWithWeigher(); return new LocalCache.LocalLoadingCache<K1, V1>(this, loader); }
實際是構造了一個LoadingCache接口的實現類:LocalCache的靜態類LocalLoadingCache,本地加載緩存類。
LocalLoadingCache(
CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader)));//LocalLoadingCache構造函數需要一個LocalCache作為參數 } //構造LocalCache LocalCache( CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) { concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS);//默認並發水平是4 keyStrength = builder.getKeyStrength();//key的強引用 valueStrength = builder.getValueStrength(); keyEquivalence = builder.getKeyEquivalence();//key比較器 valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight(); weigher = builder.getWeigher(); expireAfterAccessNanos = builder.getExpireAfterAccessNanos();//讀寫后有效期,超時重載 expireAfterWriteNanos = builder.getExpireAfterWriteNanos();//寫后有效期,超時重載 refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener();//緩存觸發失效 或者 GC回收軟/弱引用,觸發監聽器 removalNotificationQueue =//移除通知隊列 (removalListener == NullListener.INSTANCE) ? LocalCache.<RemovalNotification<K, V>>discardingQueue() : new ConcurrentLinkedQueue<RemovalNotification<K, V>>(); ticker = builder.getTicker(recordsTime()); entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries()); globalStatsCounter = builder.getStatsCounterSupplier().get(); defaultLoader = loader;//緩存加載器 int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY); if (evictsBySize() && !customWeigher()) { initialCapacity = Math.min(initialCapacity, (int) maxWeight); }
三、緩存的加載

Guava Cache為了限制內存占用,通常都設定為自動回收元素。在某些場景下,盡管LoadingCache 不回收元素,它也是很有用的,因為它會自動加載緩存。
guava cache 加載緩存主要有兩種方式:
- cacheLoader
- callable callback
創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception
方法.
cacheLoader方式實現實例:
LoadingCache<Key, Value> cache = CacheBuilder.newBuilder() .build( new CacheLoader<Key, Value>() { public Value load(Key key) throws AnyException { return createValue(key); } }); ... try { return cache.get(key); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
從LoadingCache查詢的正規方式是使用get(K)
方法。這個方法要么返回已經緩存的值,要么使用CacheLoader向緩存原子地加載新值(通過load(String key)
方法加載)。由於CacheLoader可能拋出異常,LoadingCache.get(K)
也聲明拋出ExecutionException異常。如果你定義的CacheLoader沒有聲明任何檢查型異常,則可以通過getUnchecked(K)
查找緩存;但必須注意,一旦CacheLoader聲明了檢查型異常,就不可以調用getUnchecked(K)
。
2、在get的時候:Callable
這種方式不需要在創建的時候指定load方法,但是需要在get的時候實現一個Callable匿名內部類。
Callable方式實現實例:
Cache<Key, Value> cache = CacheBuilder.newBuilder() .build(); // look Ma, no CacheLoader ... try { // If the key wasn't in the "easy to compute" group, we need to // do things the hard way. cache.get(key, new Callable<Value>() { @Override public Value call() throws AnyException { return doThingsTheHardWay(key); } }); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable<V>)
方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有緩存則返回;否則運算、緩存、然后返回"。
CacheBuilder.refreshAfterWrite(long, TimeUnit)
可以為緩存增加自動定時刷新功能。和expireAfterWrite
相反,refreshAfterWrite
通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時才會真正刷新(如果CacheLoader.refresh
實現為異步,那么檢索不會被刷新拖慢)。因此,如果你在緩存上同時聲明expireAfterWrite
和refreshAfterWrite
,緩存並不會因為刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過期時間后也變得可以回收。
統計
guava cache為我們實現統計功能,這在其它緩存工具里面還是很少有的。
7) 統計緩存使用過程中命中率/異常率/未命中率等數據。
緩存命中率
:從緩存中獲取到數據的次數/全部查詢次數,命中率越高說明這個緩存的效率好。由於機器內存的限制,緩存一般只能占據有限的內存大小,緩存需要不定期的刪除一部分數據,從而保證不會占據大量內存導致機器崩潰。
如何提高命中率呢?那就得從刪除一部分數據着手了。目前有三種刪除數據的方式,分別是:FIFO(先進先出)
、LFU(定期淘汰最少使用次數)
、LRU(淘汰最長時間未被使用)
。
guava cache 除了回收還提供一種刷新機制LoadingCache.refresh(K)
,他們的的區別在於,guava cache 在刷新時,其他線程可以繼續獲取它的舊值。這在某些情況是非常友好的。而回收的話就必須等新值加載完成以后才能繼續讀取。而且刷新是可以異步進行的。
如果刷新過程拋出異常,緩存將保留舊值,而異常會在記錄到日志后被丟棄[swallowed]。
重載CacheLoader.reload(K, V)
可以擴展刷新時的行為,這個方法允許開發者在計算新值時使用舊的值
-
CacheBuilder.recordStats()
用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()
方法會返回CacheStats對象以提供如下統計信息: -
hitRate()
:緩存命中率; -
averageLoadPenalty()
:加載新值的平均時間,單位為納秒; -
evictionCount()
:緩存項被回收的總數,不包括顯式清除。
此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據.
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();
常用接口:
/** * 該接口的實現被認為是線程安全的,即可在多線程中調用 * 通過被定義單例使用 */ public interface Cache<K, V> { /** * 通過key獲取緩存中的value,若不存在直接返回null */ V getIfPresent(Object key); /** * 通過key獲取緩存中的value,若不存在就通過valueLoader來加載該value * 整個過程為 "if cached, return; otherwise create, cache and return" * 注意valueLoader要么返回非null值,要么拋出異常,絕對不能返回null */ V get(K key, Callable<? extends V> valueLoader) throws ExecutionException; /** * 添加緩存,若key存在,就覆蓋舊值 */ void put(K key, V value); /** * 刪除該key關聯的緩存 */ void invalidate(Object key); /** * 刪除所有緩存 */ void invalidateAll(); /** * 執行一些維護操作,包括清理緩存 */ void cleanUp(); }
四、緩存回收機制
清理什么時候發生?
緩存清除的時間:
使用CacheBuilder構建的緩存不會"自動"執行清理和回收工作,也不會在某個緩存項過期后馬上清理,也沒有諸如此類的清理機制。GuavaCache的實現代碼中沒有啟動任何線程,Cache中的所有維護操作,包括清除緩存、寫入緩存等,都需要外部調用來實現 ,
相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。(問題2,如何實現的)
這樣做的原因在於:如果要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操作競爭共享鎖。此外,某些環境下線程創建可能受限制,這樣CacheBuilder就不可用了。
相反,我們把選擇權交到你手里。如果你的緩存是高吞吐的,那就無需擔心緩存的維護和清理等工作。如果你的 緩存只會偶爾有寫操作,而你又不想清理工作阻礙了讀操作,那么可以創建自己的維護線程,以固定的時間間隔調用Cache.cleanUp()。ScheduledExecutorService可以幫助你很好地實現這樣的定時調度。
。回收時主要處理四個Queue:1. keyReferenceQueue;2. valueReferenceQueue;3. writeQueue;4. accessQueue。前兩個queue是因為WeakReference、SoftReference被垃圾回收時加入的,清理時只需要遍歷整個queue,將對應的項從LocalCache中移除即可,這里keyReferenceQueue存放ReferenceEntry,而valueReferenceQueue存放的是ValueReference。要從LocalCache中移除需要有key,因而ValueReference需要有對ReferenceEntry的引用。這里的移除通過LocalCache而不是Segment是因為在移除時因為expand(rehash)可能導致原來在某個Segment中的ReferenceEntry后來被移動到另一個Segment中了。
而對后面兩個Queue,只需要檢查是否配置了相應的expire時間,然后從頭開始查找已經expire的Entry,將它們移除即可。
在put的時候,還會清理recencyQueue,即將recencyQueue中的Entry添加到accessEntry中.
guava cache基於ConcurrentHashMap的設計借鑒,在高並發場景支持線程安全,使用Reference引用命令,保證了GC的可回收到相應的數據,有效節省空間;同時write鏈和access鏈的設計,能更靈活、高效的實現多種類型的緩存清理策略,包括基於容量的清理、基於時間的清理、基於引用的清理等;
LRU緩存回收算法
LRU(Least Recently Used,最近最少使用)算法根據數據的歷史訪問記錄來進行淘汰數據,其核心思想是“如果數據最近被訪問過,那么將來被訪問的幾率也更高”。

1.新數據插入到鏈表頭部;
2.每當緩存命中(即緩存數據被訪問),則將數據移到鏈表頭部;
3.當鏈表滿的時候,將鏈表尾部的數據丟棄。
Guava Cache中借助讀寫隊列來實現LRU算法。
Guava Cache提供了四種基本的緩存回收方式:(a)基於容量回收、(b)定時回收 (c)基於引用回收 (d)顯式清除
1. 基於容量回收(size-based eviction)
maximumSize(long):當緩存中的元素數量超過指定值時。
當緩存個數超過CacheBuilder.maximumSize(long)設置的值時,優先淘汰最近沒有使用或者不常用的元素。同理
CacheBuilder.maximumWeight(long)也是一樣邏輯。
如果要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或總體上很少使用的緩存項。——警告:在緩存項的數目達到限定值之前,緩存就可能進行回收操作——通常來說,這種情況發生在緩存項的數目逼近限定值時。
b、定時回收(Timed Eviction)
2. 定時回收
CacheBuilder提供兩種定時回收的方法:
(a)按照寫入時間,最早寫入的最先回收;
expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收一樣。
(b)按照訪問時間,最早訪問的最早回收。
expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(創建或覆蓋),則回收。如果認為緩存數據總是在固定時候后變得陳舊不可用,這種回收方式是可取的。
清理發生時機
使用CacheBuilder構建的緩存不會”自動”執行清理和回收工作,也不會在某個緩存項過期后馬上清理。相反,它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做——如果寫操作實在太少的話。
3. 基於引用回收(Reference-based Eviction)
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照全局最近最少使用的順序回收。
在JDK1.2之后,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Refernce)、虛引用(Phantom Reference)。四種引用強度依次減弱。這四種引用除了強引用(Strong Reference)之外,其它的引用所對應的對象來JVM進行GC時都是可以確保被回收的。所以通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:
通過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache可以把緩存設置為允許垃圾回收:
CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
因為垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項可以被垃圾回收。
因為垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存需要時,才按照LRU(全局最近最少使用)的順序回收。
考慮到使用軟引用的性能影響,我們通常建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值。
這樣的好處就是當內存資源緊張時可以釋放掉到緩存的內存。注意!CacheBuilder如果沒有指明默認是強引用的,GC時如果沒有元素到達指定的過期時間,內存是不能被回收的。
顯式清除
任何時候,你都可以顯式地清除緩存項,而不是等到它被回收:
個別清除:Cache.invalidate(key)
批量清除:Cache.invalidateAll(keys)
清除所有緩存項:Cache.invalidateAll()
這里說一個小技巧,由於guava cache是存在就取不存在就加載的機制,我們可以對緩存數據有修改的地方顯示的把它清除掉,然后再有任務去取的時候就會去數據源重新加載,這樣就可以最大程度上保證獲取緩存的數據跟數據源是一致的。
回答:在緩存訪問或者寫的時候做了清理的操作:
不管是get,還是put每次都會遍歷這五個queue;
1、Get 刪除過期數據: 在getLiveValue中
1、再跟進去之前第 2189 行會發現先要判斷 count 是否大於 0,這個 count 保存的是當前Segment中緩存元素的數量,並用 volatile 修飾保證了可見性。
2、根據方法名稱可以看出是判斷當前的 Entry 是否過期,該 entry 就是通過 key 查詢到的。這里就很明顯的看出是根據根據構建時指定的過期方式來判斷當前 key 是否過期了。
如果過期就往下走,嘗試進行過期刪除(需要加鎖,保證操作此Segment的線程安全)。
獲取當前緩存的總數量
自減一(前面獲取了鎖,所以線程安全)
刪除並將更新的總數賦值到 count。
而在查詢時候順帶做了這些事情,但是如果該緩存遲遲沒有訪問也會存在數據不能被回收的情況,不過這對於一個高吞吐的應用來說也不是問題。
2、Put刪除數據:
刪除包含了兩部分:(a)回收弱,軟引用queue(b) 刪除 access和write隊列 中過期時間的數據
(a)回收keyReference 和 valueReference 隊列 弱,軟引用queue
(b)刪除 access和write隊列 中過期時間的數據
五、GuavaCache 的實現
GuavaCache的工作流程:獲取數據->如果存在,返回數據->計算獲取數據->存儲返回
。由於特定的工作流程,使用者必須在創建Cache或者獲取數據時指定不存在數據時應當怎么獲取數據。GuavaCache采用LRU的工作原理,使用者必須指定緩存數據的大小,當超過緩存大小時,必定引發數據刪除。GuavaCache還可以讓用戶指定緩存數據的過期時間,刷新時間等等很多有用的功能。
創建本地緩存
a.CacheLoader
/** * CacheLoader 當檢索不存在的時候,會自動的加載信息的! */ private static LoadingCache<String, String> loadingCache = CacheBuilder .newBuilder() .maximumSize(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .recordStats() .build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { String value = map.get(key); log.info(" load value by key; key:{},value:{}", key, value); return value; } }); public static String getValue(String key) { try { return loadingCache.get(key); } catch (Exception e) { log.warn(" get key error ", e); return null; } }
b.Callable
private static Cache<String, String> cacheCallable = CacheBuilder .newBuilder() .maximumSize(2) .expireAfterWrite(10, TimeUnit.SECONDS) .concurrencyLevel(2) .recordStats() .build(); /** * Callable 如果有緩存則返回;否則運算、緩存、然后返回 */ public static String getValue1(String key) { try { return cacheCallable.get(key, new Callable<String>() { @Override public String call() throws Exception { String value = map.get(key); log.info(" load value by key; key:{},value:{}", key, value); return value; } }); } catch (Exception e) { log.warn(" get key error ", e); return null; } }
5.1、Guava Cache使用了 builder模式
使用構造器重載我們需要定義很多構造器,為了應對使用者不同的需求(有些可能只需要id,有些需要id和name,有些只需要name,......),理論上我們需要定義2^4 = 16個構造器,這只是4個參數,如果參數更多的話,那將是指數級增長,肯定是不合理的。要么你定義一個全部參數的構造器,使用者只能多傳入一些不需要的屬性值來匹配你的構造器。很明顯這種構造器重載的方式對於多屬性的情況是不完美的。 (問題3 當構造函數的屬性比較多,時候可以使用)
六、CacheBuilder有3種失效重載模式
這里面有幾個參數expireAfterWrite、expireAfterAccess、maximumSize其實這幾個定義的都是過期策略。expireAfterWrite適用於一段時間cache可能會發先變化場景。expireAfterAccess是包括expireAfterWrite在內的,因為read和write操作都被定義的access操作。另外expireAfterAccess,expireAfterAccess都是受到maximumSize的限制。當緩存的數量超過了maximumSize時,guava cache會要據LRU算法淘汰掉最近沒有寫入或訪問的數據。這
里的maximumSize指的是緩存的個數並不是緩存占據內存的大小。 如果想限制緩存占據內存的大小可以配置maximumWeight參數。
看代碼:
CacheBuilder.newBuilder().weigher(new Weigher<String, Object>() {
@Override
public int weigh(String key, Object value) { return 0; //the value.size() } }).expireAfterWrite(10, TimeUnit.SECONDS).maximumWeight(500).build();
weigher返回每個cache value占據內存的大小,這個大小是由使用者自身定義的,並且put進內存時就已經確定后面就再不會發生變動。maximumWeight定義了所有cache value加起的weigher的總和不能超過的上限。
注意一點就是maximumWeight與maximumSize兩者只能生效一個是不能同時使用的!
1.expireAfterWrite
當 創建 或 寫之后的 固定 有效期到達時,數據會被自動從緩存中移除,
2.expireAfterAccess
指明每個數據實體:當 創建 或 寫 或 讀 之后的 固定值的有效期到達時,數據會被自動從緩存中移除。讀寫操作都會重置訪問時間,但asMap方法不會。
3.refreshAfterWrite
指明每個數據實體:當 創建 或 寫 之后的 固定值的有效期到達時,且新請求過來時,數據會被自動刷新(注意不是刪除是異步刷新,不會阻塞讀取,先返回舊值,異步重載到數據返回后復寫新值)。
3.數據過期重載
數據過期不會自動重載,而是通過get操作時執行過期重載。具體就是上面追蹤到了CacheBuilder構造的LocalLoadingCache,類圖如下:
返回LocalCache.LocalLoadingCache后
就可以調用如下方法:
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> { LocalLoadingCache( CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader))); } // LoadingCache methods @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e.getCause()); } } @Override public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { return localCache.getAll(keys); } @Override public void refresh(K key) { localCache.refresh(key); } @Override public final V apply(K key) { return getUnchecked(key); } // Serialization Support private static final long serialVersionUID = 1; @Override Object writeReplace() { return new LoadingSerializationProxy<K, V>(localCache); } }
刷新:
V scheduleRefresh( ReferenceEntry<K, V> entry, K key, int hash, V oldValue, long now, CacheLoader<? super K, V> loader) { if (map.refreshes() && (now - entry.getWriteTime() > map.refreshNanos) && !entry.getValueReference().isLoading()) { V newValue = refresh(key, hash, loader, true);//重載數據 if (newValue != null) {//重載數據成功,直接返回 return newValue; } }//否則返回舊值 return oldValue; }
刷新核心方法:
V refresh(K key, int hash, CacheLoader<? super K, V> loader, boolean checkTime) { final LoadingValueReference<K, V> loadingValueReference = insertLoadingValueReference(key, hash, checkTime); if (loadingValueReference == null) { return null; } //異步重載數據 ListenableFuture<V> result = loadAsync(key, hash, loadingValueReference, loader); if (result.isDone()) { try { return Uninterruptibles.getUninterruptibly(result); } catch (Throwable t) { // don't let refresh exceptions propagate; error was already logged } } return null; } ListenableFuture<V> loadAsync( final K key, final int hash, final LoadingValueReference<K, V> loadingValueReference, CacheLoader<? super K, V> loader) { final ListenableFuture<V> loadingFuture = loadingValueReference.loadFuture(key, loader); loadingFuture.addListener( new Runnable() { @Override public void run() { try { getAndRecordStats(key, hash, loadingValueReference, loadingFuture); } catch (Throwable t) { logger.log(Level.WARNING, "Exception thrown during refresh", t); loadingValueReference.setException(t); } } }, directExecutor()); return loadingFuture; } public ListenableFuture<V> loadFuture(K key, CacheLoader<? super K, V> loader) { try { stopwatch.start(); V previousValue = oldValue.get(); if (previousValue == null) { V newValue = loader.load(key); return set(newValue) ? futureValue : Futures.immediateFuture(newValue); } ListenableFuture<V> newValue = loader.reload(key, previousValue); if (newValue == null) { return Futures.immediateFuture(null); } // To avoid a race, make sure the refreshed value is set into loadingValueReference // *before* returning newValue from the cache query. return transform( newValue, new com.google.common.base.Function<V, V>() { @Override public V apply(V newValue) { LoadingValueReference.this.set(newValue); return newValue; } }, directExecutor()); } catch (Throwable t) { ListenableFuture<V> result = setException(t) ? futureValue : fullyFailedFuture(t); if (t instanceof InterruptedException) { Thread.currentThread().interrupt(); } return result; } }
如上圖,最終刷新調用的是CacheBuilder中預先設置好的CacheLoader接口實現類的reload方法實現的異步刷新。
返回get主方法,如果當前segment中找不到key對應的實體,同步阻塞重載數據:
V lockedGetOrLoad(K key, int hash, CacheLoader<? super K, V> loader) throws ExecutionException { ReferenceEntry<K, V> e; ValueReference<K, V> valueReference = null; LoadingValueReference<K, V> loadingValueReference = null; boolean createNewEntry = true; lock(); try { // re-read ticker once inside the lock long now = map.ticker.read(); preWriteCleanup(now); int newCount = this.count - 1; AtomicReferenceArray<ReferenceEntry<K, V>> table = this.table; int index = hash & (table.length() - 1); ReferenceEntry<K, V> first = table.get(index); for (e = first; e != null; e = e.getNext()) { K entryKey = e.getKey(); if (e.getHash() == hash && entryKey != null && map.keyEquivalence.equivalent(key, entryKey)) { valueReference = e.getValueReference(); if (valueReference.isLoading()) {//如果正在重載,那么不需要重新再新建實體對象 createNewEntry = false; } else { V value = valueReference.get(); if (value == null) {//如果被GC回收,添加進移除隊列,等待remove監聽器執行 enqueueNotification( entryKey, hash, value, valueReference.getWeight(), RemovalCause.COLLECTED); } else if (map.isExpired(e, now)) {//如果緩存過期,添加進移除隊列,等待remove監聽器執行 // This is a duplicate check, as preWriteCleanup already purged expired // entries, but let's accomodate an incorrect expiration queue. enqueueNotification( entryKey, hash, value, valueReference.getWeight(), RemovalCause.EXPIRED); } else {//不在重載,直接返回value recordLockedRead(e, now); statsCounter.recordHits(1); // we were concurrent with loading; don't consider refresh return value; } // immediately reuse invalid entries writeQueue.remove(e); accessQueue.remove(e); this.count = newCount; // write-volatile } break; } } //需要新建實體對象 if (createNewEntry) { loadingValueReference = new LoadingValueReference<K, V>(); if (e == null) { e = newEntry(key, hash, first); e.setValueReference(loadingValueReference); table.set(index, e);//把新的ReferenceEntry<K, V>引用實體對象添加進table } else { e.setValueReference(loadingValueReference); } } } finally { unlock(); postWriteCleanup(); } //需要新建實體對象 if (createNewEntry) { try { // Synchronizes on the entry to allow failing fast when a recursive load is // detected. This may be circumvented when an entry is copied, but will fail fast most // of the time. synchronized (e) {//同步重載數據 return loadSync(key, hash, loadingValueReference, loader); } } finally { statsCounter.recordMisses(1); } } else { // 重載中,說明實體已存在,等待重載完畢 return waitForLoadingValue(e, key, valueReference); } }
七、GuavaCache使用
首先定義一個需要存儲的Bean,對象Man:
/** * @author jiangmitiao * @version V1.0 * @Title: 標題 * @Description: Bean * @date 2016/10/27 10:01 */ public class Man { //身份證號 private String id; //姓名 private String name; public String getId() { return id; } public void setId(String id) { this.id = id; } public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return "Man{" + "id='" + id + '\'' + ", name='" + name + '\'' + '}'; } }
接下來我們寫一個Demo:
import com.google.common.cache.*; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.concurrent.*; /** * @author jiangmitiao * @version V1.0 * @Description: Demo * @date 2016/10/27 10:00 */ public class GuavaCachDemo { private LoadingCache<String,Man> loadingCache; //loadingCache public void InitLoadingCache() { //指定一個如果數據不存在獲取數據的方法 CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() { @Override public Man load(String key) throws Exception { //模擬mysql操作 Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("LoadingCache測試 從mysql加載緩存ing...(2s)"); Thread.sleep(2000); logger.info("LoadingCache測試 從mysql加載緩存成功"); Man tmpman = new Man(); tmpman.setId(key); tmpman.setName("其他人"); if (key.equals("001")) { tmpman.setName("張三"); return tmpman; } if (key.equals("002")) { tmpman.setName("李四"); return tmpman; } return tmpman; } }; //緩存數量為1,為了展示緩存刪除效果 loadingCache = CacheBuilder.newBuilder().maximumSize(1).build(cacheLoader); } //獲取數據,如果不存在返回null public Man getIfPresentloadingCache(String key){ return loadingCache.getIfPresent(key); } //獲取數據,如果數據不存在則通過cacheLoader獲取數據,緩存並返回 public Man getCacheKeyloadingCache(String key){ try { return loadingCache.get(key); } catch (ExecutionException e) { e.printStackTrace(); } return null; } //直接向緩存put數據 public void putloadingCache(String key,Man value){ Logger logger = LoggerFactory.getLogger("LoadingCache"); logger.info("put key :{} value : {}",key,value.getName()); loadingCache.put(key,value); } }
接下來,我們寫一些測試方法,檢測一下
public class Test { public static void main(String[] args){ GuavaCachDemo cachDemo = new GuavaCachDemo() System.out.println("使用loadingCache"); cachDemo.InitLoadingCache(); System.out.println("使用loadingCache get方法 第一次加載"); Man man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 第一次加載"); man = cachDemo.getIfPresentloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 第一次加載"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加載過"); man = cachDemo.getCacheKeyloadingCache("002"); System.out.println(man); System.out.println("\n使用loadingCache get方法 已加載過,但是已經被剔除掉,驗證重新加載"); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache getIfPresent方法 已加載過"); man = cachDemo.getIfPresentloadingCache("001"); System.out.println(man); System.out.println("\n使用loadingCache put方法 再次get"); Man newMan = new Man(); newMan.setId("001"); newMan.setName("額外添加"); cachDemo.putloadingCache("001",newMan); man = cachDemo.getCacheKeyloadingCache("001"); System.out.println(man); } }
guava cache使用簡介
guava cache 是利用CacheBuilder類用builder模式構造出兩種不同的cache加載方式CacheLoader,Callable,共同邏輯都是根據key是加載value。不同的地方在於CacheLoader的定義比較寬泛,是針對整個cache定義的,可以認為是統一的根據key值load value的方法,而Callable的方式較為靈活,允許你在get的時候指定load方法。看以下代碼
Cache<String,Object> cache = CacheBuilder.newBuilder() .expireAfterWrite(10, TimeUnit.SECONDS).maximumSize(500).build(); cache.get("key", new Callable<Object>() { //Callable 加載 @Override public Object call() throws Exception { return "value"; } }); LoadingCache<String, Object> loadingCache = CacheBuilder.newBuilder() .expireAfterAccess(30, TimeUnit.SECONDS).maximumSize(5) .build(new CacheLoader<String, Object>() { @Override public Object load(String key) throws Exception { return "value"; } });
加載
CacheLoader
如果有合理的默認方法來加載或計算與鍵關聯的值。
LoadingCache是附帶CacheLoader構建而成的緩存實現。創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception方法。
從LoadingCache查詢的正規方式是使用get(K)方法。這個方法要么返回已經緩存的值,要么使用CacheLoader向緩存原子地加載新值。由於CacheLoader可能拋出異常,LoadingCache.get(K)也聲明為拋出ExecutionException異常。
Callable
如果沒有合理的默認方法來加載或計算與鍵關聯的值,或者想要覆蓋默認的加載運算,同時保留“獲取緩存-如果沒有-則計算”[get-if-absent-compute]的原子語義。
所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable<V>)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式"如果有緩存則返回;否則運算、緩存、然后返回"。
但自動加載是首選的,因為它可以更容易地推斷所有緩存內容的一致性。
使用cache.put(key, value)方法可以直接向緩存中插入值,這會直接覆蓋掉給定鍵之前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中。進一步說,asMap視圖的原子運算在Guava Cache的原子加載范疇之外,所以相比於Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 應該總是優先使用。
統計
CacheBuilder.recordStats():用來開啟Guava Cache的統計功能。統計打開后,Cache.stats()方法會返回CacheS tats 對象以提供如下統計信息:
hitRate():緩存命中率;
averageLoadPenalty():加載新值的平均時間,單位為納秒;
evictionCount():緩存項被回收的總數,不包括顯式清除。
此外,還有其他很多統計信息。這些統計信息對於調整緩存設置是至關重要的,在性能要求高的應用中我們建議密切關注這些數據。
Demo3:
public class GuavaCacheDemo3 { static Cache<String, Object> testCache = CacheBuilder.newBuilder() .weakValues() .recordStats() .build(); public static void main(String[] args){ Object obj1 = new Object(); testCache.put("1234",obj1); obj1 = new String("123"); System.gc(); System.out.println(testCache.getIfPresent("1234")); System.out.println(testCache.stats()); } }
運行結果

3、Guava Cache 主要的類圖:

CacheBuilder
緩存構建器。構建緩存的入口,指定緩存配置參數並初始化本地緩存。
主要采用builder的模式,CacheBuilder的每一個方法都返回這個CacheBuilder知道build方法的調用。
注意build方法有重載,帶有參數的為構建一個具有數據加載功能的緩存,不帶參數的構建一個沒有數據加載功能的緩存。
LocalManualCache
作為LocalCache的一個內部類,在構造方法里面會把LocalCache類型的變量傳入,並且調用方法時都直接或者間接調用LocalCache里面的方法。
LocalLoadingCache
可以看到該類繼承了LocalManualCache並實現接口LoadingCache。
覆蓋了get,getUnchecked等方法。
2、localManualCache如何實現的
上一篇文章講了LocalCache是如何通過Builder構建出來的,這篇文章重點是講localCache的原理,首先通過類圖理清涉及到相關類的關系,如下圖我們可以看到,guava Cache的核心就是LocalCache,LocalCache實現了ConcurrentMap,並繼承了抽象的map,關於ConcurrentMap的實現可以看這篇文章,講的是並發hashmap的實現,對理解這篇文章有幫助。對於構造LocalCache最直接的兩個相關類是LocalManualCache和LocalLoadingCache。
LocalManualCache和LocalLoadingCache
那么這個LoadingCache到底是什么作用呢,其實就是LocalCache對外暴露了實現的方法,所有暴露的方法都是實現了這個接口,LocalLoadingCache就是實現了這個接口,
特殊的是它是LocalCache的內部靜態類,這個LocalLoadingCache內部靜態類只是降低了LocalCache的復雜度,它是完全獨立於LocalCache的。下邊是我們使用的方法都是LocalCache接口的方法
說完了LocalLoadingCache我們看下LocalManualCache的作用,LocalManualCache是LocalLoadingCache的父類,LocalManualCache實現了Cache,所以LocalManualCache具有了所有Cache的方法,LocalLoadingCache是繼承自LocalManualCache同樣獲得了Cache的所有方法,但是LocalLoadingCache可以選擇的重載LocalManualCache中的方法,這樣的設計有很大的靈活性;guava cache的內部實現用的LocalCache,但是對外暴露的是LocalLoadingCache,很好隱藏了細節,總結來說
1、LocalManualCache實現了Cache,具有了所有cache方法。
2、LocalLoadingCache實現了LoadingCache,具有了所有LoadingCache方法。
3、LocalLoadingCache繼承了LocalManualCache,那么對外暴露的LocalLoadingCache的方法既有自身需要的,又有cache應該具有的。
4、通過LocalLoadingCache和LocalManualCache的父子關系實現了LocalCache的細節。
Guava Cache到底是如何進行緩存的
我們現在通過類圖和源碼的各種繼承關系理清了這兩個LocalLoadingCache和LocalManualCache的重要關系,下邊我們再繼續深入,通過我們常用的get方法進入:
/** * LocalLoadingCache中的get方法,localCache是父類LocalManualCache的 */ @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } /** * 這個get和getOrLoad是AccessQueue中的方法,AccessQueue是何方神聖呢,我們通過類圖梳理一下他們的關系 */ V get(K key, CacheLoader<? super K, V> loader) throws ExecutionException { int hash = hash(checkNotNull(key)); return segmentFor(hash).get(key, hash, loader); } V getOrLoad(K key) throws ExecutionException { return get(key, defaultLoader); }
很明顯這是隊列,這兩個隊列的作用如下
WriteQueue:按照寫入時間進行排序的元素隊列,寫入一個元素時會把它加入到隊列尾部。
AccessQueue:按照訪問時間進行排序的元素隊列,訪問(包括寫入)一個元素時會把它加入到隊列尾部。
我們來看下ReferenceEntry接口的代碼,具備了一個Entry所需要的元素
interface ReferenceEntry<K, V> { /** * Returns the value reference from this entry. */ ValueReference<K, V> getValueReference(); /** * Sets the value reference for this entry. */ void setValueReference(ValueReference<K, V> valueReference); /** * Returns the next entry in the chain. */ @Nullable ReferenceEntry<K, V> getNext(); /** * Returns the entry's hash. */ int getHash(); /** * Returns the key for this entry. */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. * New entries are added at the tail of the list at write time; stale entries are expired from * the head of the list. */ /** * Returns the time that this entry was last accessed, in ns. */ long getAccessTime();
5、Guava Cache 如何加載數據
不管性能,還是可用性來說, Guava Cache 絕對是本地緩存類庫中首要推薦的工具類。其提供的 Builder模式 的CacheBuilder生成器來創建緩存的方式,十分方便,並且各個緩存參數的配置設置,類似於函數式編程的寫法,也特別棒。
在官方文檔中,提到三種方式加載 <key,value> 到緩存中。分別是:
LoadingCache 在構建緩存的時候,使用build方法內部調用 CacheLoader 方法加載數據;
在使用get方法的時候,如果緩存不存在該key或者key過期等,則調用 get(K, Callable<V>) 方式加載數據;
使用粗暴直接的方式,直接想緩存中put數據。
需要說明的是,如果不能通過key快速計算出value時,則還是不要在初始化的時候直接調用 CacheLoader 加載數據到緩存中。
加載
在使用緩存前,首先問自己一個問題:有沒有合理的默認方法來加載或計算與鍵關聯的值?如果有的話,你應當使用CacheLoader。如果沒有,或者你想要覆蓋默認的加載運算,同時保留”獲取緩存-如果沒有-則計算”[get-if-absent-compute]的原子語義,你應該在調用get時傳入一個Callable實例。緩存元素也可以通過Cache.put方法直接插入,但自動加載是首選的,因為它可以更容易地推斷所有緩存內容的一致性。自動加載就是createCacheLoader中的,當cache.get(key)不存在的時候,會主動的去加載值的信息並放進緩存中去。
Guava Cache有以下兩種創建方式:
創建 CacheLoader
LoadingCache是附帶CacheLoader構建而成的緩存實現。創建自己的CacheLoader通常只需要簡單地實現V load(K key) throws Exception方法。例如,你可以用下面的代碼構建LoadingCache:
CacheLoader: 當檢索不存在的時候,會自動的加載信息的!
public static com.google.common.cache.CacheLoader<String, Employee> createCacheLoader() {
return new com.google.common.cache.CacheLoader<String, Employee>() { @Override public Employee load(String key) throws Exception { log.info("加載創建key:" + key); return new Employee(key, key + "dept", key + "id"); } }; } LoadingCache<String, Employee> cache = CacheBuilder.newBuilder() .maximumSize(1000) .expireAfterAccess(30L, TimeUnit.MILLISECONDS) .build(createCacheLoader());
創建 Callable
所有類型的Guava Cache,不管有沒有自動加載功能,都支持get(K, Callable)方法。這個方法返回緩存中相應的值,或者用給定的Callable運算並把結果加入到緩存中。在整個加載方法完成前,緩存項相關的可觀察狀態都不會更改。這個方法簡便地實現了模式”如果有緩存則返回;否則運算、緩存、然后返回”。
Cache<Key, Value> cache = CacheBuilder.newBuilder()
.maximumSize(1000) .build(); // look Ma, no CacheLoader ... try { // If the key wasn't in the "easy to compute" group, we need to // do things the hard way. cache.get(key, new Callable<Value>() { @Override public Value call() throws AnyException { return doThingsTheHardWay(key); } }); } catch (ExecutionException e) { throw new OtherException(e.getCause()); }
7、刷新
刷新和回收不太一樣。正如LoadingCache.refresh(K)所聲明,刷新表示為鍵加載新值,這個過程可以是異步的。在刷新操作進行時,緩存仍然可以向其他線程返回舊值,而不像回收操作,讀緩存的線程必須等待新值加載完成。
CacheBuilder.refreshAfterWrite(long, TimeUnit)可以為緩存增加自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite通過定時刷新可以讓緩存項保持可用,但請注意:緩存項只有在被檢索時才會真正刷新(如果CacheLoader.refresh實現為異步,那么檢索不會被刷新拖慢)。因此,如果你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存並不會因為刷新盲目地定時重置,如果緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過期時間后也變得可以回收。
2.1 Guava Cache使用示例
import java.util.Date; import java.util.concurrent.Callable; import java.util.concurrent.TimeUnit; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.google.common.cache.Cache; import com.google.common.cache.CacheBuilder; import com.google.common.cache.CacheLoader; import com.google.common.cache.LoadingCache; import com.google.common.cache.RemovalListener; import com.google.common.cache.RemovalNotification; /** * @author tao.ke Date: 14-12-20 Time: 下午1:55 * @version \$Id$ */ public class CacheSample { private static final Logger logger = LoggerFactory.getLogger(CacheSample.class); // Callable形式的Cache private static final Cache<String, String> CALLABLE_CACHE = CacheBuilder.newBuilder() .expireAfterWrite(1, TimeUnit.SECONDS).maximumSize(1000).recordStats() .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { logger.info("Remove a map entry which key is {},value is {},cause is {}.", notification.getKey(), notification.getValue(), notification.getCause().name()); } }).build(); // CacheLoader形式的Cache private static final LoadingCache<String, String> LOADER_CACHE = CacheBuilder.newBuilder() .expireAfterAccess(1, TimeUnit.SECONDS).maximumSize(1000).recordStats().build(new CacheLoader<String, String>() { @Override public String load(String key) throws Exception { return key + new Date(); } }); public static void main(String[] args) throws Exception { int times = 4; while (times-- > 0) { Thread.sleep(900); String valueCallable = CALLABLE_CACHE.get("key", new Callable<String>() { @Override public String call() throws Exception { return "key" + new Date(); } }); logger.info("Callable Cache ----->>>>> key is {},value is {}", "key", valueCallable); logger.info("Callable Cache ----->>>>> stat miss:{},stat hit:{}",CALLABLE_CACHE.stats().missRate(),CALLABLE_CACHE.stats().hitRate()); String valueLoader = LOADER_CACHE.get("key"); logger.info("Loader Cache ----->>>>> key is {},value is {}", "key", valueLoader); logger.info("Loader Cache ----->>>>> stat miss:{},stat hit:{}",LOADER_CACHE.stats().missRate(),LOADER_CACHE.stats().hitRate()); } } }
上述代碼,簡單的介紹了 Guava Cache 的使用,給了兩種加載構建Cache的方式。在 Guava Cache 對外提供的方法中, recordStats 和 removalListener 是兩個很有趣的接口,可以很好的幫我們完成統計功能和Entry移除引起的監聽觸發功能。
此外,雖然在 Guava Cache 對外方法接口中提供了豐富的特性,但是如果我們在實際的代碼中不是很有需要的話,建議不要設置這些屬性,因為會額外占用內存並且會多一些處理計算工作,不值得。
Guava Cache 分析前置知識
Guava Cache 就是借鑒Java的 ConcurrentHashMap 的思想來實現一個本地緩存,但是它內部代碼實現的時候,還是有很多非常精彩的設計實現,並且如果對 ConcurrentHashMap 內部具體實現不是很清楚的話,通過閱讀 Cache 的實現,對 ConcurrentHashMap 的實現基本上會有個全面的了解。
3.1 Builder模式
設計模式之 Builder模式 在Guava中很多地方得到的使用。 Builder模式 是將一個復雜對象的構造與其對應配置屬性表示的分離,也就是可以使用基本相同的構造過程去創建不同的具體對象。
Builder模式典型的結構圖如:
Builder:為創建一個Product對象的各個部件制定抽象接口;
ConcreteBuilder:具體的建造者,它負責真正的生產;
Director:導演, 建造的執行者,它負責發布命令;
Product:最終消費的產品
Builder模式 的關鍵是其中的Director對象並不直接返回對象,而是通過(BuildPartA,BuildPartB,BuildPartC)來一步步進行對象的創建。當然這里Director可以提供一個默認的返回對象的接口(即返回通用的復雜對象的創建,即不指定或者特定唯一指定BuildPart中的參數)。
Tips:在 Effective Java 第二版中, Josh Bloch 在第二章中就提到使用Builder模式處理需要很多參數的構造函數。他不僅展示了Builder的使用,也描述了相這種方法相對使用帶很多參數的構造函數帶來的好處。
下面給出一個使用Builder模式來構造對象,這種方式優點和不足(代碼量增加)非常明顯。
import org.apache.commons.lang3.builder.ToStringBuilder; import org.apache.commons.lang3.builder.ToStringStyle; /** * @author tao.ke Date: 14-12-22 Time: 下午8:57 * @version \$Id$ */ public class BuilderPattern { /** * 姓名 */ private String name; /** * 年齡 */ private int age; /** * 性別 */ private Gender gender; public static BuilderPattern newBuilder() { return new BuilderPattern(); } public BuilderPattern setName(String name) { this.name = name; return this; } public BuilderPattern setAge(int age) { this.age = age; return this; } public BuilderPattern setGender(Gender gender) { this.gender = gender; return this; } @Override public String toString() { return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE); } enum Gender { MALE, FEMALE } public static void main(String[] args) { BuilderPattern bp = BuilderPattern.newBuilder().setAge(10).setName("zhangsan").setGender(Gender.FEMALE); system.out.println(bp.toString()); } }
3.6 Guava ListenableFuture接口
我們強烈地建議你在代碼中多使用 ListenableFuture 來代替JDK的 Future, 因為:
大多數Futures 方法中需要它。
轉到 ListenableFuture 編程比較容易。
Guava提供的通用公共類封裝了公共的操作方方法,不需要提供Future和 ListenableFuture 的擴展方法。
創建ListenableFuture實例
首先需要創建 ListeningExecutorService 實例,Guava 提供了專門的方法把JDK中提供 ExecutorService對象轉換為 ListeningExecutorService 。然后通過submit方法就可以創建一個ListenableFuture實例了。
代碼片段如下:
ListeningExecutorService service = MoreExecutors.listeningDecorator(Executors.newFixedThreadPool(10)); ListenableFuture explosion = service.submit(new Callable() { public Explosion call() { return pushBigRedButton(); } }); Futures.addCallback(explosion, new FutureCallback() { // we want this handler to run immediately after we push the big red button! public void onSuccess(Explosion explosion) { walkAwayFrom(explosion); } public void onFailure(Throwable thrown) { battleArchNemesis(); // escaped the explosion! } });
也就是說,對於異步的方法,我可以通過監聽器來根據執行結果來判斷接下來的處理行為。
ListenableFuture 鏈式操作
使用ListenableFuture 最重要的理由是它可以進行一系列的復雜鏈式的異步操作。
一般,使用AsyncFunction來完成鏈式異步操作。不同的操作可以在不同的Executors中執行,單獨的ListenableFuture 可以有多個操作等待。
>
Tips: AsyncFunction接口常被用於當我們想要異步的執行轉換而不造成線程阻塞時,盡管Future.get()方法會在任務沒有完成時造成阻塞,但是AsyncFunction接口並不被建議用來異步的執行轉換,它常被用於返回Future實例。
下面給出這個鏈式操作完成一個簡單的異步字符串轉換操作:
import java.util.concurrent.Callable; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import com.google.common.util.concurrent.AsyncFunction; import com.google.common.util.concurrent.FutureCallback; import com.google.common.util.concurrent.Futures; import com.google.common.util.concurrent.ListenableFuture; import com.google.common.util.concurrent.ListeningExecutorService; import com.google.common.util.concurrent.MoreExecutors; /** * @author tao.ke Date: 14-12-26 Time: 下午5:28 * @version \$Id$ */ public class ListenerFutureChain { private static final ExecutorService executor = Executors.newFixedThreadPool(2); private static final ListeningExecutorService executorService = MoreExecutors.listeningDecorator(executor); public void executeChain() { AsyncFunction<String, String> asyncFunction = new AsyncFunction<String, String>() { @Override public ListenableFuture<String> apply(final String input) throws Exception { ListenableFuture<String> future = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("STEP1 >>>" + Thread.currentThread().getName()); return input + "|||step 1 ===--===||| "; } }); return future; } }; AsyncFunction<String, String> asyncFunction2 = new AsyncFunction<String, String>() { @Override public ListenableFuture<String> apply(final String input) throws Exception { ListenableFuture<String> future = executorService.submit(new Callable<String>() { @Override public String call() throws Exception { System.out.println("STEP2 >>>" + Thread.currentThread().getName()); return input + "|||step 2 ===--===---||| "; } }); return future; } }; ListenableFuture startFuture = executorService.submit(new Callable() { @Override public Object call() throws Exception { System.out.println("BEGIN >>>" + Thread.currentThread().getName()); return "BEGIN--->"; } }); ListenableFuture future = Futures.transform(startFuture, asyncFunction, executor); ListenableFuture endFuture = Futures.transform(future, asyncFunction2, executor); Futures.addCallback(endFuture, new FutureCallback() { @Override public void onSuccess(Object result) { System.out.println(result); System.out.println("=======OK======="); } @Override public void onFailure(Throwable t) { t.printStackTrace(); } }); } public static void main(String[] args) { System.out.println("========START======="); System.out.println("MAIN >>>" + Thread.currentThread().getName()); ListenerFutureChain chain = new ListenerFutureChain(); chain.executeChain(); System.out.println("========END======="); executor.shutdown(); // System.exit(0); } } 輸出: ========START======= MAIN >>>main BEGIN >>>pool-2-thread-1 ========END======= STEP1 >>>pool-2-thread-2 STEP2 >>>pool-2-thread-1 BEGIN--->|||step 1 ===--===||| |||step 2 ===--===---||| =======OK=======
從輸出可以看出,代碼是異步完成字符串操作的。
CacheBuilder實現
寫過Cache的,或者其他一些工具類的同學知道,為了讓工具類更靈活,我們需要對外提供大量的參數配置給使用者設置,雖然這帶有一些好處,但是由於參數太多,使用者開發構造對象的時候過於繁雜。
上面提到過參數配置過多,可以使用Builder模式。Guava Cache也一樣,它為我們提供了CacheBuilder工具類來構造不同配置的Cache實例。但是,和本文上面提到的構造器實現有點不一樣,它構造器返回的是另外一個對象,因此,這意味着在實現的時候,對象構造函數需要有Builder參數提供配置屬性。
4.1 CacheBuilder構造LocalCache實現
首先,我們先看看Cache的構造函數:
/** * 從builder中獲取相應的配置參數。 */ LocalCache(CacheBuilder<? super K, ? super V> builder, @Nullable CacheLoader<? super K, V> loader) { concurrencyLevel = Math.min(builder.getConcurrencyLevel(), MAX_SEGMENTS); keyStrength = builder.getKeyStrength(); valueStrength = builder.getValueStrength(); keyEquivalence = builder.getKeyEquivalence(); valueEquivalence = builder.getValueEquivalence(); maxWeight = builder.getMaximumWeight(); weigher = builder.getWeigher(); expireAfterAccessNanos = builder.getExpireAfterAccessNanos(); expireAfterWriteNanos = builder.getExpireAfterWriteNanos(); refreshNanos = builder.getRefreshNanos(); removalListener = builder.getRemovalListener(); removalNotificationQueue = (removalListener == NullListener.INSTANCE) ? LocalCache .<RemovalNotification<K, V>> discardingQueue() : new ConcurrentLinkedQueue<RemovalNotification<K, V>>(); ticker = builder.getTicker(recordsTime()); entryFactory = EntryFactory.getFactory(keyStrength, usesAccessEntries(), usesWriteEntries()); globalStatsCounter = builder.getStatsCounterSupplier().get(); defaultLoader = loader; int initialCapacity = Math.min(builder.getInitialCapacity(), MAXIMUM_CAPACITY); if (evictsBySize() && !customWeigher()) { initialCapacity = Math.min(initialCapacity, (int) maxWeight); } //....... }
從構造函數可以看到,Cache的所有參數配置都是從Builder對象中獲取的,Builder完成了作為該模式最典型的應用,多配置參數構建對象。
在Cache中只提供一個構造函數,但是在上面代碼示例中,我們演示了兩種構建緩存的方式:自動加載;手動加載。那么,一般會存在一個完成兩者之間的過渡 adapter 組件,接下來看看Builder在內部是如何完成創建緩存對象過程的。
OK,你猜到了。在 LocalCache 中確實提供了兩種過渡類,一個是支持自動加載value的 LocalLoadingCache 和只能在鍵值找不到的時候手動調用獲取值方法的 LocalManualCache 。
LocalManualCache實現
static class LocalManualCache<K, V> implements Cache<K, V>, Serializable { final LocalCache<K, V> localCache; LocalManualCache(CacheBuilder<? super K, ? super V> builder) { this(new LocalCache<K, V>(builder, null)); } private LocalManualCache(LocalCache<K, V> localCache) { this.localCache = localCache; } // Cache methods @Override @Nullable public V getIfPresent(Object key) { return localCache.getIfPresent(key); } @Override public V get(K key, final Callable<? extends V> valueLoader) throws ExecutionException { checkNotNull(valueLoader); return localCache.get(key, new CacheLoader<Object, V>() { @Override public V load(Object key) throws Exception { return valueLoader.call(); } }); } //...... @Override public CacheStats stats() { SimpleStatsCounter aggregator = new SimpleStatsCounter(); aggregator.incrementBy(localCache.globalStatsCounter); for (Segment<K, V> segment : localCache.segments) { aggregator.incrementBy(segment.statsCounter); } return aggregator.snapshot(); } // Serialization Support private static final long serialVersionUID = 1; Object writeReplace() { return new ManualSerializationProxy<K, V>(localCache); } }
從代碼實現看出實際上是一個adapter組件,並且絕大部分實現都是直接調用LocalCache的方法,或者加一些參數判斷和聚合。在它核心的構造函數中,就是直接調用LocalCache構造函數,對於loader對象直接設null值。
LocalLoadingCache實現
LocalLoadingCache 實現繼承了``類,其主要對get相關方法做了重寫。
static class LocalLoadingCache<K, V> extends LocalManualCache<K, V> implements LoadingCache<K, V> { LocalLoadingCache(CacheBuilder<? super K, ? super V> builder, CacheLoader<? super K, V> loader) { super(new LocalCache<K, V>(builder, checkNotNull(loader))); } // LoadingCache methods @Override public V get(K key) throws ExecutionException { return localCache.getOrLoad(key); } @Override public V getUnchecked(K key) { try { return get(key); } catch (ExecutionException e) { throw new UncheckedExecutionException(e.getCause()); } } @Override public ImmutableMap<K, V> getAll(Iterable<? extends K> keys) throws ExecutionException { return localCache.getAll(keys); } @Override public void refresh(K key) { localCache.refresh(key); } @Override public final V apply(K key) { return getUnchecked(key); } // Serialization Support private static final long serialVersionUID = 1; @Override Object writeReplace() { return new LoadingSerializationProxy<K, V>(localCache); } } } 提供了這些adapter類之后,builder類就可以創建 LocalCache ,如下: // 獲取value可以通過key計算出 public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build(CacheLoader<? super K1, V1> loader) { checkWeightWithWeigher(); return new LocalCache.LocalLoadingCache<K1, V1>(this, loader); } // 手動加載 public <K1 extends K, V1 extends V> Cache<K1, V1> build() { checkWeightWithWeigher(); checkNonLoadingCache(); return new LocalCache.LocalManualCache<K1, V1>(this); }
4.2 CacheBuilder參數設置
CacheBuilder 在為我們提供了構造一個Cache對象時,會構造各個成員對象的初始值(默認值)。了解這些默認值,對於我們分析Cache源碼實現時,一些判斷條件的設置原因,還是很有用的。
初始參數值設置
在 ConcurrentHashMap 中,我們知道有個並發水平(CONCURRENCY_LEVEL),這個參數決定了其允許多少個線程並發操作修改該數據結構。這是因為這個參數是最后map使用的segment個數,而每個segment對應一個鎖,因此,對於一個map來說,並發環境下,理論上最大可以有segment個數的線程同時安全地操作修改數據結構。那么是不是segment的值可以設置很大呢?顯然不是,要記住維護一個鎖的成本還是挺高的,此外如果涉及全表操作,那么性能就會非常不好了。
其他一些初始參數值的設置如下所示:
private static final int DEFAULT_INITIAL_CAPACITY = 16; // 默認的初始化Map大小 private static final int DEFAULT_CONCURRENCY_LEVEL = 4; // 默認並發水平 private static final int DEFAULT_EXPIRATION_NANOS = 0; // 默認超時 private static final int DEFAULT_REFRESH_NANOS = 0; // 默認刷新時間 static final int UNSET_INT = -1; boolean strictParsing = true; int initialCapacity = UNSET_INT; int concurrencyLevel = UNSET_INT; long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; long expireAfterWriteNanos = UNSET_INT; long expireAfterAccessNanos = UNSET_INT; long refreshNanos = UNSET_INT;
初始對象引用設置
在Cache中,我們除了超時時間,鍵值引用屬性等設置外,還關注命中統計情況,這就需要統計對象來工作。CacheBuilder提供了初始的null 統計對象和空統計對象。
此外,還會設置到默認的引用類型等設置,代碼如下:
/** * 默認空的緩存命中統計類 */ static final Supplier<? extends StatsCounter> NULL_STATS_COUNTER = Suppliers.ofInstance(new StatsCounter() { //......省略空override @Override public CacheStats snapshot() { return EMPTY_STATS; } }); static final CacheStats EMPTY_STATS = new CacheStats(0, 0, 0, 0, 0, 0);// 初始狀態的統計對象 /** * 系統實現的簡單的緩存狀態統計類 */ static final Supplier<StatsCounter> CACHE_STATS_COUNTER = new Supplier<StatsCounter>() { @Override public StatsCounter get() { return new SimpleStatsCounter();//這里構造簡單地統計類實現 } }; /** * 自定義的空RemovalListener,監聽到移除通知,默認空處理。 */ enum NullListener implements RemovalListener<Object, Object> { INSTANCE; @Override public void onRemoval(RemovalNotification<Object, Object> notification) { } } /** * 默認權重類,任何對象的權重均為1 */ enum OneWeigher implements Weigher<Object, Object> { INSTANCE; @Override public int weigh(Object key, Object value) { return 1; } } static final Ticker NULL_TICKER = new Ticker() { @Override public long read() { return 0; } }; /** * 默認的key等同判斷 * @return */ Equivalence<Object> getKeyEquivalence() { return firstNonNull(keyEquivalence, getKeyStrength().defaultEquivalence()); } /** * 默認value的等同判斷 * @return */ Equivalence<Object> getValueEquivalence() { return firstNonNull(valueEquivalence, getValueStrength().defaultEquivalence()); } /** * 默認的key引用 * @return */ Strength getKeyStrength() { return firstNonNull(keyStrength, Strength.STRONG); } /** * 默認為Strong 屬性的引用 * @return */ Strength getValueStrength() { return firstNonNull(valueStrength, Strength.STRONG); } <K1 extends K, V1 extends V> Weigher<K1, V1> getWeigher() { return (Weigher<K1, V1>) Objects.firstNonNull(weigher, OneWeigher.INSTANCE); }
其中,在我們不設置緩存中鍵值引用的情況下,默認都是采用強引用及相對應的屬性策略來初始化的。此外,在上面代碼中還可以看到,統計類 SimpleStatsCounter 是一個簡單的實現。里面主要是簡單地緩存累加,此外由於多線程下Long類型的線程非安全性,所以也進行了一下封裝,下面給出命中率的實現:
public static final class SimpleStatsCounter implements StatsCounter { private final LongAddable hitCount = LongAddables.create(); private final LongAddable missCount = LongAddables.create(); private final LongAddable loadSuccessCount = LongAddables.create(); private final LongAddable loadExceptionCount = LongAddables.create(); private final LongAddable totalLoadTime = LongAddables.create(); private final LongAddable evictionCount = LongAddables.create(); public SimpleStatsCounter() {} @Override public void recordHits(int count) { hitCount.add(count); } @Override public CacheStats snapshot() { return new CacheStats( hitCount.sum()); } /** * Increments all counters by the values in {@code other}. */ public void incrementBy(StatsCounter other) { CacheStats otherStats = other.snapshot(); hitCount.add(otherStats.hitCount()); } }
因此,CacheBuilder的一些參數對象等得初始化就完成了。可以看到這些默認的初始化,有兩套引用:Null對象和Empty對象,顯然Null會更省空間,但我們在創建的時候將決定不使用某特性的時候,就會使用Null來創建,否則使用Empty來完成初始化工作。在分析Cache的時候,寫后超時隊列和讀后超時隊列也存在兩個版本。
LocalCache實現
在設計實現上, LocalCache 的並發策略和 concurrentHashMap 的並發策略是一致的,也是根據分段鎖來提高並發能力,分段鎖可以很好的保證 並發讀寫的效率。因此,該map支持非阻塞讀和不同段之間並發寫。
如果最大的大小指定了,那么基於段來執行操作是最好的。使用頁面替換算法來決定當map大小超過指定值時,哪些entries需要被驅趕出去。頁面替換算法的數據結構保證Map臨時一致性:對一個segment寫排序是一致的;但是對map進行更新和讀不能直接立刻 反應在數據結構上。 雖然這些數據結構被lock鎖保護,但是其結構決定了批量操作可以避免鎖競爭出現。在線程之間傳播的批量操作導致分攤成本比不強制大小限制的操作要稍微高一點。
此外, LoacalCache 使用LRU頁面替換算法,是因為該算法簡單,並且有很高的命中率,以及O(1)的時間復雜度。需要說明的是, LRU算法是基於頁面而不是全局實現的,所以可能在命中率上不如全局LRU算法,但是應該基本相似。
最后,要說明一點,在代碼實現上,頁面其實就是一個段segment。之所以說page頁,是因為在計算機專業課程上,CPU,操作系統,算法上,基本上都介紹過分頁導致優化效果的提升。
從圖中可以直觀看到cache是以segment粒度來控制並發get和put等操作的,接下來首先看我們的 LocalCache 是如何構造這些segment段的,繼續上面初始化localCache構造函數的代碼:
// 找到大於並發水平的最小2的次方的值,作為segment數量 int segmentShift = 0; int segmentCount = 1; while (segmentCount < concurrencyLevel && (!evictsBySize() || segmentCount * 20 <= maxWeight)) { ++segmentShift; segmentCount <<= 1; } this.segmentShift = 32 - segmentShift;//位 偏移數 segmentMask = segmentCount - 1;//mask碼 this.segments = newSegmentArray(segmentCount);// 構造數據數組,如上圖所示 //獲取每個segment初始化容量,並且保證大於等於map初始容量 int segmentCapacity = initialCapacity / segmentCount; if (segmentCapacity * segmentCount < initialCapacity) { ++segmentCapacity; } //段Size 必須為2的次數,並且剛剛大於段初始容量 int segmentSize = 1; while (segmentSize < segmentCapacity) { segmentSize <<= 1; } // 權重設置,確保權重和==map權重 if (evictsBySize()) { // Ensure sum of segment max weights = overall max weights long maxSegmentWeight = maxWeight / segmentCount + 1; long remainder = maxWeight % segmentCount; for (int i = 0; i < this.segments.length; ++i) { if (i == remainder) { maxSegmentWeight--; } //構造每個段結構 this.segments[i] = createSegment(segmentSize, maxSegmentWeight, builder.getStatsCounterSupplier().get()); } } else { for (int i = 0; i < this.segments.length; ++i) { //構造每個段結構 this.segments[i] = createSegment(segmentSize, UNSET_INT, builder.getStatsCounterSupplier().get()); } } Notes:基本上都是基於2的次數來設置大小的,顯然基於移位操作比普通計算操作速度要快。此外,對於最大權重分配到段權重的設計上,很特殊。為什么呢?為了保證兩者能夠相等(maxWeight==sumAll(maxSegmentWeight)),對於remainder前面的segment maxSegmentWeight的值比remainder后面的權重值大1,這樣保證最后值相等。 map get 方法 @Override @Nullable public V get(@Nullable Object key) { if (key == null) { return null; } int hash = hash(key); return segmentFor(hash).get(key, hash); } Notes:代碼很簡單,首先check key是否為null,然后計算hash值,定位到對應的segment,執行segment實例擁有的get方法獲取對應的value值 map put 方法 @Override public V put(K key, V value) { checkNotNull(key); checkNotNull(value); int hash = hash(key); return segmentFor(hash).put(key, hash, value, false); } Notes:和get方法一樣,也是先check值,然后計算key的hash值,然后定位到對應的segment段,執行段實例的put方法,將鍵值存入map中。 map isEmpty 方法 @Override public boolean isEmpty() { long sum = 0L; Segment<K, V>[] segments = this.segments; for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) { return false; } sum += segments[i].modCount; } if (sum != 0L) { // recheck unless no modifications for (int i = 0; i < segments.length; ++i) { if (segments[i].count != 0) { return false; } sum -= segments[i].modCount; } if (sum != 0L) { return false; } } return true; }
Notes:判斷Cache是否為空,就是分別判斷每個段segment是否都為空,但是由於整體是在並發環境下進行的,也就是說存在對一個segment並發的增加和移除元素的時候,而我們此時正在check其他segment段。
上面這種情況,決定了我們不能夠獲得任何一個時間點真實段狀態的情況。因此,上面的代碼引入了sum變量來計算段modCount變更情況。modCount表示改變segment大小size的更新次數,這個在批量讀取方法期間保證它們可以看到一致性的快照。 需要注意,這里先獲取count,該值是volatile,因此modCount通常都可以在不需要一致性控制下,獲得當前segment最新的值.
在判斷如果在第一次check的時候,發現segment發生了數據結構級別變更,則會進行recheck,就是在每個modCount下,段仍然是空的,則判斷該map為空。如果發現這期間數據結構發生變化,則返回非空判斷。
map 其他方法
在Cache數據結構中,還有很多方法,和上面列出來的方法一樣,其底層核心實現都是依賴segment類實例中實現的對應方法。
此外,在總的數據結構中,還提供了一些根據builder配制制定相應地緩存策略方法。比如:
expiresAfterAccess:是否執行訪問后超時過期策略;
expiresAfterWrite:是否執行寫后超時過期策略;
usesAccessQueue:根據上面的配置決定是否需要new一個訪問隊列;
usesWriteQueue:根據上面的配置決定是否需要new一個寫隊列;
usesKeyReferences/usesValueReferences:是否需要使用特別的引用策略(非Strong引用).
等等……
5.2 引用數據結構
在介紹Segment數據結構之前,先講講Cache中引用的設計。
關於Reference引用的一些說明,在博文的上面已經介紹了,這里就不贅述。在Guava Cache 中,主要使用三種引用類型,分別是: STRONG引用 , SOFT引用 , WEAK引用 。和Map不同,在Cache中,使用 ReferenceEntry 來封裝鍵值對,並且對於值來說,還額外實現了 ValueReference 引用對象來封裝對應Value對象。
ReferenceEntry節點結構
為了支持各種不同類型的引用,以及不同過期策略,這里構造了一個ReferenceEntry節點結構。通過下面的節點數據結構,可以清晰的看到緩存大致操作流程。
/** * 引用map中一個entry節點。 * * 在map中得entries節點有下面幾種狀態: * valid:-live:設置了有效的key/value;-loading:加載正在處理中.... * invalid:-expired:時間過期(但是key/value可能仍然設置了);Collected:key/value部分被垃圾收集了,但是還沒有被清除; * -unset:標記為unset,表示等待清除或者重新使用。 * */ interface ReferenceEntry<K, V> { /** * 從entry中返回value引用 */ ValueReference<K, V> getValueReference(); /** * 為entry設置value引用 */ void setValueReference(ValueReference<K, V> valueReference); /** * 返回鏈中下一個entry(解決hash碰撞存在鏈表) */ @Nullable ReferenceEntry<K, V> getNext(); /** * 返回entry的hash */ int getHash(); /** * 返回entry的key */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. New entries are * added at the tail of the list at write time; stale entries are expired from the head of the list. */ /** * 返回該entry最近一次被訪問的時間ns */ long getAccessTime(); /** * 設置entry訪問時間ns. */ void setAccessTime(long time); /** * 返回訪問隊列中下一個entry */ ReferenceEntry<K, V> getNextInAccessQueue(); /** * Sets the next entry in the access queue. */ void setNextInAccessQueue(ReferenceEntry<K, V> next); /** * Returns the previous entry in the access queue. */ ReferenceEntry<K, V> getPreviousInAccessQueue(); /** * Sets the previous entry in the access queue. */ void setPreviousInAccessQueue(ReferenceEntry<K, V> previous); // ...... 省略write隊列相關方法,和access一樣 }
Notes:從上面代碼可以看到除了和Map一樣,有key、value、hash和next四個屬性之外,還有訪問和寫更新兩個雙向鏈表隊列,以及entry的最近訪問時間和最近更新時間。顯然,多出來的屬性就是為了支持緩存必須要有的過期機制。
此外,從上面的代碼可以看出 cache支持的LRU機制實際上是建立在segment上的,也就是基於頁的替換機制。
關於訪問隊列數據結構,其實質就是一個雙向的鏈表。當節點被訪問的時候,這個節點將會移除,然后把這個節點添加到鏈表的結尾。關於具體實現,將在segment中介紹。
創建不同類型的ReferenceEntry由其枚舉工廠類EntryFactory來實現,它根據key的Strength類型、是否使用accessQueue、是否使用writeQueue來決定不同的EntryFactry實例,並通過它創建相應的ReferenceEntry實例
ValueReference結構
同樣為了支持Cache中各個不同類型的引用,其對Value類型進行再封裝,支持引用。看看其內部數據屬性:
/** * A reference to a value. */ interface ValueReference<K, V> { /** * Returns the value. Does not block or throw exceptions. */ @Nullable V get(); /** * Waits for a value that may still be loading. Unlike get(), this method can block (in the case of * FutureValueReference). * * @throws ExecutionException if the loading thread throws an exception * @throws ExecutionError if the loading thread throws an error */ V waitForValue() throws ExecutionException; /** * Returns the weight of this entry. This is assumed to be static between calls to setValue. */ int getWeight(); /** * Returns the entry associated with this value reference, or {@code null} if this value reference is * independent of any entry. */ @Nullable ReferenceEntry<K, V> getEntry(); /** * 為一個指定的entry創建一個該引用的副本 * <p> * {@code value} may be null only for a loading reference. */ ValueReference<K, V> copyFor(ReferenceQueue<V> queue, @Nullable V value, ReferenceEntry<K, V> entry); /** * 告知一個新的值正在加載中。這個只會關聯到加載值引用。 */ void notifyNewValue(@Nullable V newValue); /** * 當一個新的value正在被加載的時候,返回true。不管是否已經有存在的值。這里加鎖方法返回的值對於給定的ValueReference實例來說是常量。 * */ boolean isLoading(); /** * 返回true,如果該reference包含一個活躍的值,意味着在cache里仍然有一個值存在。活躍的值包含:cache查找返回的,等待被移除的要被驅趕的值; 非激活的包含:正在加載的值, */ boolean isActive(); }
Notes:value引用接口對象中包含了不同狀態的標記,以及一些加載方法和獲取具體value值對象。
為了減少不必須的load加載,在value引用中增加了loading標識和wait方法等待加載獲取值。這樣,就可以等待上一個調用loader方法獲取值,而不是重復去調用loader方法加重系統負擔,而且可以更快的獲取對應的值。
此外,介紹下 ReferenceQueue 引用隊列,這個隊列是JDK提供的,在檢測到適當的可到達性更改后,垃圾回收器將已注冊的引用對象添加到該隊列中。因為Cache使用了各種引用,而通過ReferenceQueue這個“監聽器”就可以優雅的實現自動刪除那些引用不可達的key了,是不是很吊,哈哈。
在Cache分別實現了基於Strong,Soft,Weak三種形式的ValueReference實現。
這里ValueReference之所以要有對ReferenceEntry的引用是因為在Value因為WeakReference、SoftReference被回收時,需要使用其key將對應的項從Segment段中移除;
copyFor()函數的存在是因為在expand(rehash)重新創建節點時,對WeakReference、SoftReference需要重新創建實例(C++中的深度復制思想,就是為了保持對象狀態不會相互影響),而對強引用來說,直接使用原來的值即可,這里很好的展示了對彼變化的封裝思想;
notifiyNewValue只用於LoadingValueReference,它的存在是為了對LoadingValueReference來說能更加及時的得到CacheLoader加載的值。
5.3 Segment 數據結構
Segment 數據結構,是ConcurrentHashMap的核心實現,也是該結構保證了其算法的高效性。在 Guava Cache 中也一樣, segment 數據結構保證了緩存在線程安全的前提下可以高效地更新,插入,獲取對應value。
實際上,segment就是一個特殊版本的hash table實現。其內部也是對應一個鎖,不同的是,對於get和put操作做了一些優化處理。因此,在代碼實現的時候,為了快速開發和利用已有鎖特性,直接 extends ReentrantLock 。
在segment中,其主要的類屬性就是一個 LoacalCache 類型的map變量。關於segment實現說明,如下:
/** * segments 維護一個entry列表的table,確保一致性狀態。所以可以不加鎖去讀。節點的next field是不可修改的final,因為所有list的增加操作 * 是執行在每個容器的頭部。因此,這樣子很容易去檢查變化,也可以快速遍歷。此外,當節點被改變的時候,新的節點將被創建然后替換它們。 由於容器的list一般都比較短(平均長度小於2),所以對於hash * tables來說,可以工作的很好。雖然說讀操作因此可以不需要鎖進行,但是是依賴 * 使用volatile確保其他線程完成寫操作。對於絕大多數目的而言,count變量,跟蹤元素的數量,其作為一個volatile變量確保可見性(其內部原理可以參考其他相關博文)。 * 這樣一下子變得方便的很多,因為這個變量在很多讀操作的時候都會被獲取:所有非同步的(unsynchronized)讀操作必須首先讀取這個count值,並且如果count為0則不會 查找table * 的entries元素;所有的同步(synchronized)操作必須在結構性的改變任務bin容器之后,才會寫操作這個count值。 * 這些操作必須在並發讀操作看到不一致的數據的時候,不采取任務動作。在map中讀操作性質可以更容易實現這個限制。例如:沒有操作可以顯示出 當table * 增長了,但是threshold值沒有更新,所以考慮讀的時候不要求原子性。作為一個原則,所有危險的volatile讀和寫count變量都必須在代碼中標記。 */ final LocalCache<K, V> map; /** * 該segment區域內所有存活的元素個數 */ volatile int count; /** * 改變table大小size的更新次數。這個在批量讀取方法期間保證它們可以看到一致性的快照: * 如果modCount在我們遍歷段加載大小或者核對containsValue期間被改變了,然后我們會看到一個不一致的狀態視圖,以至於必須去重試。 * count+modCount 保證內存一致性 * * 感覺這里有點像是版本控制,比如數據庫里的version字段來控制數據一致性 */ int modCount; /** * 每個段表,使用樂觀鎖的Array來保存entry The per-segment table. */ volatile AtomicReferenceArray<ReferenceEntry<K, V>> table; // 這里和concurrentHashMap不一致,原因是這邊元素是引用,直接使用不會線程安全 /** * A queue of elements currently in the map, ordered by write time. Elements are added to the tail of the queue * on write. */ @GuardedBy("Segment.this") final Queue<ReferenceEntry<K, V>> writeQueue; /** * A queue of elements currently in the map, ordered by access time. Elements are added to the tail of the queue * on access (note that writes count as accesses). */ @GuardedBy("Segment.this") final Queue<ReferenceEntry<K, V>> accessQueue;
Notes:
在segment實現中,很多地方使用count變量和modCount變量來保持線程安全,從而省掉lock開銷。
在本文上面的圖中說明了每個segment就是一個節點table,和jdk實現不一致,這里為了GC,內部維護的是一個 AtomicReferenceArray<ReferenceEntry<K, V>> 類型的列表,可以保證安全性。
最后, LocalCache 作為一個緩存,其必須具有訪問和寫超時特性,因為其內部維護了訪問隊列和寫隊列,隊列中的元素按照訪問或者寫時間排序,新的元素會被添加到隊列尾部。如果,在隊列中已經存在了該元素,則會先delete掉,然后再尾部add該節點,新的時間。這也就是為什么,對於 LocalCache 而言,其LRU是針對segment的,而不是全Cache范圍的。
在本文的 5.2節中知道,cache會根據初始化實例時配置來創建多個segment( createSegment ),然后該方法最終調用segment類的構造函數創建一個段。對於參數set,就不展示,看看構造方法中其主要操作:
// 構造函數 Segment(LocalCache<K, V> map, int initialCapacity, long maxSegmentWeight, StatsCounter statsCounter) { initTable(newEntryArray(initialCapacity)); } AtomicReferenceArray<ReferenceEntry<K, V>> newEntryArray(int size) { return new AtomicReferenceArray<ReferenceEntry<K, V>>(size); } void initTable(AtomicReferenceArray<ReferenceEntry<K, V>> newTable) { this.threshold = newTable.length() * 3 / 4; // 0.75 if (!map.customWeigher() && this.threshold == maxSegmentWeight) { // prevent spurious expansion before eviction this.threshold++; } this.table = newTable; }
OK,這里我們已經構造好了整個localCache對象了,包括其內部每個segment中對應的節點表。這些節點table,決定了最后所有核心操作的具體實現和操作結果。
接下來,需要看看最核心的幾個方法。
Tips:本文把這幾個方法單獨作為幾節來說明,這也表示這幾個方法的重要性。
Notes:上面從緩存中直接獲取key對應value,是完全沒有加鎖來完成的。
scheduleRefresh方法
如果配置refresh特性,到了配置的刷新間隔時間,而且節點也沒有正在加載,則應該進行refresh操作。refresh操作比較復雜。
六、Guava Cache中數據的定位 兩次Hash 類似於ConcurrentHashMap
其實 Guava Cache 為了滿足並發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這里也是一個 key 定位到一個具體位置的過程。
先找到 Segment,再找具體的位置,等於是做了兩次 Hash 定位。
主要的類:
CacheBuilder 設置參數,構建Cache
LocalCache 是核心實現,雖然builder構建的是LocalLoadingCache(帶refresh功能)和LocalManualCache(不帶refresh功能),但其實那兩個只是個殼子
1、CacheBuilder 構建器
提要:
記錄所需參數
public final class CacheBuilder<K, V> { public <K1 extends K, V1 extends V> LoadingCache<K1, V1> build( CacheLoader<? super K1, V1> loader) { // loader是用來自動刷新的 checkWeightWithWeigher(); return new LocalCache.LocalLoadingCache<>(this, loader); } public <K1 extends K, V1 extends V> Cache<K1, V1> build() { // 這個沒有loader,就不會自動刷新 checkWeightWithWeigher(); checkNonLoadingCache(); return new LocalCache.LocalManualCache<>(this); } int initialCapacity = UNSET_INT; // 初始map大小 int concurrencyLevel = UNSET_INT; // 並發度 long maximumSize = UNSET_INT; long maximumWeight = UNSET_INT; Weigher<? super K, ? super V> weigher; Strength keyStrength; // key強、弱、軟引,默認為強 Strength valueStrength; // value強、弱、軟引,默認為強 long expireAfterWriteNanos = UNSET_INT; // 寫過期 long expireAfterAccessNanos = UNSET_INT; // long refreshNanos = UNSET_INT; // Equivalence<Object> keyEquivalence; // 強引時為equals,否則為== Equivalence<Object> valueEquivalence; // 強引時為equals,否則為== RemovalListener<? super K, ? super V> removalListener; // 刪除時的監聽 Ticker ticker; // 時間鍾,用來獲得當前時間的 Supplier<? extends StatsCounter> statsCounterSupplier = NULL_STATS_COUNTER; // 計數器,用來記錄get或者miss之類的數據 }
2、LocalCache
在上文的分析中可以看出 Cache 中的 ReferenceEntry
是類似於 HashMap 的 Entry 存放數據的。
來看看 ReferenceEntry 的定義:
interface ReferenceEntry<K, V> { allBackListener { /** * Returns the value reference from this entry. */ ValueReference<K, V> getValueReference(); /** ify(String msg) ; * Sets the value reference for this entry. */ void setValueReference(ValueReference<K, V> valueReference); /** * Returns the next entry in the chain. */ @Nullable ReferenceEntry<K, V> getNext(); /** * Returns the entry's hash. */ int getHash(); /** * Returns the key for this entry. */ @Nullable K getKey(); /* * Used by entries that use access order. Access entries are maintained in a doubly-linked list. * New entries are added at the tail of the list at write time; stale entries are expired from * the head of the list. */ /** * Returns the time that this entry was last accessed, in ns. */ long getAccessTime(); /** * Sets the entry access time in ns. */ void setAccessTime(long time); }
包含了很多常用的操作,如值引用、鍵引用、訪問時間等。
根據 ValueReference<K, V> getValueReference();
的實現:
具有強引用和弱引用的不同實現。
key 也是相同的道理:
當使用這樣的構造方式時,弱引用的 key 和 value 都會被垃圾回收。
當然我們也可以顯式的回收:
/** * Discards any cached value for key {@code key}. * 單個回收 */ void invalidate(Object key); /** * Discards any cached values for keys {@code keys}. * * @since 11.0 */ void invalidateAll(Iterable<?> keys); /** * Discards all entries in the cache. */ void invalidateAll();
回調
改造了之前的例子:
loadingCache = CacheBuilder.newBuilder() .expireAfterWrite(2, TimeUnit.SECONDS) .removalListener(new RemovalListener<Object, Object>() { @Override public void onRemoval(RemovalNotification<Object, Object> notification) { LOGGER.info("刪除原因={},刪除 key={},刪除 value={}",notification.getCause(),notification.getKey(),notification.getValue()); } }) .build(new CacheLoader<Integer, AtomicLong>() { @Override public AtomicLong load(Integer key) throws Exception { return new AtomicLong(0); } });
執行結果:
12018-07-15 20:41:07.433 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 當前緩存值=0,緩存大小=1 22018-07-15 20:41:07.442 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 緩存的所有內容={1000=0} 32018-07-15 20:41:07.443 [main] INFO c.crossoverjie.guava.CacheLoaderTest - job running times=10 42018-07-15 20:41:10.461 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 刪除原因=EXPIRED,刪除 key=1000,刪除 value=1 52018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 當前緩存值=0,緩存大小=1 62018-07-15 20:41:10.462 [main] INFO c.crossoverjie.guava.CacheLoaderTest - 緩存的所有內容={1000=0}
可以看出當緩存被刪除的時候會回調我們自定義的函數,並告知刪除原因。
那么 Guava 是如何實現的呢?
根據 LocalCache 中的 getLiveValue()
中判斷緩存過期時,跟着這里的調用關系就會一直跟到:
removeValueFromChain()
中的:
enqueueNotification()
方法會將回收的緩存(包含了 key,value)以及回收原因包裝成之前定義的事件接口加入到一個本地隊列中。
這樣一看也沒有回調我們初始化時候的事件啊。
不過用過隊列的同學應該能猜出,既然這里寫入隊列,那就肯定就有消費。
我們回到獲取緩存的地方:
在 finally 中執行了 postReadCleanup()
方法;其實在這里面就是對剛才的隊列進行了消費:
一直跟進來就會發現這里消費了隊列,將之前包裝好的移除消息調用了我們自定義的事件,這樣就完成了一次事件回調。
8、Guava Cache特性:對於同一個key,只讓一個請求回源load數據,其他線程阻塞等待結果 (問題4 如何做到的)
KeyReferenceQueue: 基於引用的Entry,其實現類有弱引用Entry,強引用Entry等 ,已經被GC,需要內部清理的鍵引用隊列。
ValueReference
對於ValueReference,因為Guava Cache支持強引用的Value、SoftReference Value以及WeakReference Value,因而它對應三個實現類:StrongValueReference、SoftValueReference、WeakValueReference。為了支持動態加載機制,它還有一個LoadingValueReference,在需要動態加載一個key的值時,先把該值封裝在LoadingValueReference中,以表達該key對應的值已經在加載了,如果其他線程也要查詢該key對應的值,就能得到該引用,並且等待改值加載完成,從而保證該值只被加載一次(可以在evict以后重新加載)。
在該只加載完成后,將LoadingValueReference替換成其他ValueReference類型。對新創建的LoadingValueReference,由於其內部oldValue的初始值是UNSET,它isActive為false,isLoading為false,因而此時的LoadingValueReference的isActive為false,但是isLoading為true。每個ValueReference都紀錄了weight值,所謂weight從字面上理解是“該值的重量”,它由Weighter接口計算而得。weight在Guava Cache中由兩個用途:1. 對weight值為0時,在計算因為size limit而evict是忽略該Entry(它可以通過其他機制evict);2. 如果設置了maximumWeight值,則當Cache中weight和超過了該值時,就會引起evict操作。但是目前還不知道這個設計的用途。最后,Guava Cache還定義了Stength枚舉類型作為ValueReference的factory類,它有三個枚舉值:Strong、Soft、Weak,這三個枚舉值分別創建各自的ValueReference,並且根據傳入的weight值是否為1而決定是否要創建Weight版本的ValueReference。
設想高並發下的一種場景:假設我們將name=aty存放到緩存中,並設置的有過期時間。當緩存過期后,恰好有10個客戶端發起請求,需要讀取name的值。使用Guava Cache可以保證只讓一個線程去加載數據(比如從數據庫中),而其他線程則等待這個線程的返回結果。這樣就能避免大量用戶請求穿透緩存。
九、總結
1、guava使用的場景:
在平常開發過程中,很多情況需要使用緩存來避免頻繁SQL查詢或者其他耗時操作,會采取緩存這些操作結果給下一次請求使用。如果我們的操作結果是一直不改變的,其實我們可以使用 ConcurrentHashMap 來存儲這些數據;但是如果這些結果在隨后時間內會改變或者我們希望存放的數據所占用的內存空間可控,這樣就需要自己來實現這種數據結構了。
顯然,對於這種十分常見的需求, Guava 提供了自己的工具類實現。 Guava Cache 提供了一般我們使用緩存所需要的幾乎所有的功能,主要有:
(1) 自動將entry節點加載進緩存結構中;
(2)當緩存的數據已經超過預先設置的最大值時,使用LRU算法移除一些數據;
(3)具備根據entry節點上次被訪問或者寫入的時間來計算過期機制;
(4)緩存的key被封裝在`WeakReference`引用內;
(5)緩存的value被封裝在`WeakReference`或者`SoftReference`引用內;
(6)移除entry節點,可以觸發監聽器通知事件;
緩存加載:CacheLoader、Callable、顯示插入(put)
緩存回收:LRU,定時(expireAfterAccess
,expireAfterWrite
),軟弱引用,顯示刪除(Cache接口方法invalidate
,invalidateAll
)
監聽器:CacheBuilder.removalListener(RemovalListener)
清理緩存時間:只有在獲取數據時才或清理緩存LRU,使用者可以單起線程采用Cache.cleanUp()
方法主動清理。
刷新:主動刷新方法LoadingCache.referesh(K)
信息統計:CacheBuilder.recordStats()
開啟Guava Cache的統計功能。Cache.stats()
返回CacheStats對象。(其中包括命中率等相關信息)
獲取當前緩存所有數據:cache.asMap()
,cache.asMap().get(Object)會刷新數據的訪問時間(影響的是:創建時設置的在多久沒訪問后刪除數據)
對於Guava Cache 對於其核心實現我會做如下的設計:
- 定義一個CacheConfig類用於紀錄所有的配置,如CacheLoader,maximum size、expire time、key reference level、value reference level、eviction listener等。
- 定義一個Cache接口,該接口類似Map(或ConcurrentMap),但是為了和Map區別開來,因而重新定義一個Cache接口。
- 定義一個實現Cache接口的類CacheImpl,它接收CacheConfig作為參數的構造函數,並將CacheConfig實例保存在字段中。
- 在實現上模仿ConcurrentHashMap的實現方式,有一個Segment數組,其長度由配置的concurrencyLevel值決定。為了實現最近最少使用算法(LRU),添加AccessQueue和WriteQueue字段,這兩個Queue內部采用雙鏈表,每次新創建一個Entry,就將這個Entry加入到這兩個Queue的末尾,而每讀取一個Entry就將其添加到AccessQueue的末尾,沒更新一個Entry將該Entry添加到WriteQueue末尾。為了實現key和value上的WeakReference、SoftReference,添加ReferenceQueue<K>類型的keyReferenceQueue和valueReferenceQueue字段。
- 在每次調用方法之前都遍歷AccessQueue和WriteQueue,如果發現有Entry已經expire,就將該Entry從這兩個Queue上和Cache中移除。然后遍歷keyReferenceQueue和valueReference,如果發現有項存在,同樣將它們移除。在移除時如果有EvictionListener注冊着,則調用該listener。
- 對Segment實現,它時一個CacheEntry數組,CacheEntry是一個鏈節點,它包含hash、key、vlaue、next。CacheEntry根據是否需要包裝在WeakReference中創建WeakEntry或StrongEntry,而對value根據是否需要包裝在WeakReference、SoftReference中創建WeakValueReference、SoftValueReference、StrongValueReference。在get操作中對於需要使用CacheLoader加載的值先添加一個具有LoadingValueReference值的Entry,這樣可以保證同一個Key只加載依次。在加載成功后將LoadingValueReference根據配置替換成其他Weak、Soft、Strong ValueReference。
- 對於cache access statistics,只需要有一個類在需要的地方做一些統計計數即可。
源碼參考:guava源碼
參考:Guava Cache特性:對於同一個key,只讓一個請求回源load數據,其他線程阻塞等待結果