本地缓存之王——Caffeine 组件最强讲解!
點(diǎn)擊關(guān)注公眾號(hào),實(shí)用技術(shù)文章及時(shí)了解
結(jié)論:Caffeine 是目前性能最好的本地緩存,因此,在考慮使用本地緩存時(shí),直接選擇 Caffeine 即可。
先看一個(gè)小例子,明白如何創(chuàng)建一個(gè) Caffeine 緩存實(shí)例。
Caffeine?caffeine?=?Caffeine.newBuilder().initialCapacity(3).maximumSize(4); Cache?cache?=?caffeine.build(); cache.put("aa",?13); System.out.println(cache.getIfPresent("aa"));Caffeine 相當(dāng)于一個(gè)緩存工廠,可以創(chuàng)建出多個(gè)緩存實(shí)例 Cache。這些緩存實(shí)例都繼承了 Caffeine 的參數(shù)配置,Caffeine 是如何配置的,這些緩存實(shí)例就具有什么樣的特性和功能。
1. Caffeine 可以設(shè)置哪些緩存屬性呢?
1. 緩存初始容量
initialCapacity:整數(shù),表示能存儲(chǔ)多少個(gè)緩存對(duì)象。
為什么要設(shè)置初始容量呢?因?yàn)槿绻崆澳茴A(yù)估緩存的使用大小,那么可以設(shè)置緩存的初始容量,以免緩存不斷地進(jìn)行擴(kuò)容,致使效率不高。
2. 最大容量 最大權(quán)重
maximumSize:最大容量,如果緩存中的數(shù)據(jù)量超過這個(gè)數(shù)值,Caffeine 會(huì)有一個(gè)異步線程來專門負(fù)責(zé)清除緩存,按照指定的清除策略來清除掉多余的緩存。
注意:比如最大容量是 2,此時(shí)已經(jīng)存入了2個(gè)數(shù)據(jù)了,此時(shí)存入第3個(gè)數(shù)據(jù),觸發(fā)異步線程清除緩存,在清除操作沒有完成之前,緩存中仍然有3個(gè)數(shù)據(jù),且 3 個(gè)數(shù)據(jù)均可讀,緩存的大小也是 3,只有當(dāng)緩存操作完成了,緩存中才只剩 2 個(gè)數(shù)據(jù),至于清除掉了哪個(gè)數(shù)據(jù),這就要看清除策略了。
maximumWeight:最大權(quán)重,存入緩存的每個(gè)元素都要有一個(gè)權(quán)重值,當(dāng)緩存中所有元素的權(quán)重值超過最大權(quán)重時(shí),就會(huì)觸發(fā)異步清除。
下面給個(gè)例子:
class?Person{Integer?age;String?name; }Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); cache.put("three",?new?Person(1,?"three")); Thread.sleep(10); System.out.println(cache.estimatedSize()); System.out.println(cache.getIfPresent("two"));運(yùn)行結(jié)果:
2 null要使用權(quán)重來衡量的話,就要規(guī)定權(quán)重是什么,每個(gè)元素的權(quán)重怎么計(jì)算,weigher 方法就是設(shè)置權(quán)重規(guī)則的,它的參數(shù)是一個(gè)函數(shù),函數(shù)的參數(shù)是 key 和 value,函數(shù)的返回值就是元素的權(quán)重,比如上述代碼中,caffeine 設(shè)置了最大權(quán)重值為 30,然后將每個(gè) Person 對(duì)象的 age 年齡作為權(quán)重值,所以整個(gè)意思就是:緩存中存儲(chǔ)的是 Person 對(duì)象,但是限制所有對(duì)象的 age 總和不能超過 30,否則就觸發(fā)異步清除緩存。
特別要注意一點(diǎn):最大容量 和 最大權(quán)重 只能二選一作為緩存空間的限制。
3. 緩存狀態(tài)
3.1 默認(rèn)的緩存狀態(tài)收集器 CacheStats
默認(rèn)情況下,緩存的狀態(tài)會(huì)用一個(gè) CacheStats 對(duì)象記錄下來,通過訪問 CacheStats 對(duì)象就可以知道當(dāng)前緩存的各種狀態(tài)指標(biāo),那究竟有哪些指標(biāo)呢?
先說一下什么是“加載”,當(dāng)查詢緩存時(shí),緩存未命中,那就需要去第三方數(shù)據(jù)庫中查詢,然后將查詢出的數(shù)據(jù)先存入緩存,再返回給查詢者,這個(gè)過程就是加載。
totalLoadTime:總共加載時(shí)間。
loadFailureRate:加載失敗率,= 總共加載失敗次數(shù) / 總共加載次數(shù)
averageLoadPenalty:平均加載時(shí)間,單位-納秒
evictionCount:被淘汰出緩存的數(shù)據(jù)總個(gè)數(shù)
evictionWeight:被淘汰出緩存的那些數(shù)據(jù)的總權(quán)重
hitCount:命中緩存的次數(shù)
hitRate:命中緩存率
loadCount:加載次數(shù)
loadFailureCount:加載失敗次數(shù)
loadSuccessCount:加載成功次數(shù)
missCount:未命中次數(shù)
missRate:未命中率
requestCount:用戶請(qǐng)求查詢總次數(shù)
CacheStats 類包含了 2 個(gè)方法,了解一下:
CacheStats minus(@Nonnull CacheStats other):當(dāng)前 CacheStats 對(duì)象的各項(xiàng)指標(biāo)減去參數(shù) other 的各項(xiàng)指標(biāo),差值形成一個(gè)新的 CacheStats 對(duì)象。
CacheStats plus(@Nonnull CacheStats other):當(dāng)前 CacheStats 對(duì)象的各項(xiàng)指標(biāo)加上參數(shù) other 的各項(xiàng)指標(biāo),和值形成一個(gè)新的 CacheStats 對(duì)象。
舉個(gè)例子說明:
Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).recordStats().weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); cache.put("three",?new?Person(1,?"three")); CacheStats?stats?=?cache.stats();System.out.println(stats.hitCount());3.2 自定義的緩存狀態(tài)收集器
自定義的緩存狀態(tài)收集器的作用:每當(dāng)緩存有操作發(fā)生時(shí),不管是查詢,加載,存入,都會(huì)使得緩存的某些狀態(tài)指標(biāo)發(fā)生改變,哪些狀態(tài)指標(biāo)發(fā)生了改變,就會(huì)自動(dòng)觸發(fā)收集器中對(duì)應(yīng)的方法執(zhí)行,如果我們?cè)诜椒ㄖ凶远x的代碼是收集代碼,比如將指標(biāo)數(shù)值發(fā)送到 kafka,那么其它程序從kafka讀取到數(shù)值,再進(jìn)行分析與可視化展示,就能實(shí)現(xiàn)對(duì)緩存的實(shí)時(shí)監(jiān)控了。
收集器接口為 StatsCounter ,我們只需實(shí)現(xiàn)這個(gè)接口的所有抽象方法即可。下面舉例說明。
public?class?MyStatsCounter?implements?StatsCounter?{@Overridepublic?void?recordHits(int?i)?{System.out.println("命中次數(shù):"?+?i);}@Overridepublic?void?recordMisses(int?i)?{System.out.println("未命中次數(shù):"?+?i);}@Overridepublic?void?recordLoadSuccess(long?l)?{System.out.println("加載成功次數(shù):"?+?l);}@Overridepublic?void?recordLoadFailure(long?l)?{System.out.println("加載失敗次數(shù):"?+?l);}@Overridepublic?void?recordEviction()?{System.out.println("因?yàn)榫彺娲笮∠拗?#xff0c;執(zhí)行了一次緩存清除工作");}@Overridepublic?void?recordEviction(int?weight)?{System.out.println("因?yàn)榫彺鏅?quán)重限制,執(zhí)行了一次緩存清除工作,清除的數(shù)據(jù)的權(quán)重為:"?+?weight);}@Overridepublic?CacheStats?snapshot()?{return?null;} }上述代碼為自定義的緩存狀態(tài)收集器,收集到的狀態(tài)指標(biāo)只是簡(jiǎn)單地打印出來,snapshot 方法有什么作用,暫時(shí)不清楚。
特別需要注意的是:收集器中那些方法得到的狀態(tài)值,只是當(dāng)前緩存操作所產(chǎn)生的結(jié)果,比如當(dāng)前 cache.getIfPresent() 查詢一個(gè)值,查詢到了,說明命中了,但是 recordHits(int i) 方法的參數(shù) i = 1,因?yàn)楸敬尾僮髅辛?1 次。
再將收集器與某個(gè)緩存掛鉤,如下:
MyStatsCounter?myStatsCounter?=?new?MyStatsCounter(); Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).recordStats(()->myStatsCounter).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); cache.put("three",?new?Person(1,?"three")); cache.getIfPresent("ww"); CacheStats?stats?=?myStatsCounter.snapshot(); Thread.sleep(1000);最后的執(zhí)行結(jié)果為:
未命中次數(shù):1 因?yàn)榫彺鏅?quán)重限制,執(zhí)行了一次緩存清除工作,清除的數(shù)據(jù)的權(quán)重為:184. 線程池
Caffeine 緩沖池總有一些異步任務(wù)要執(zhí)行,所以它包含了一個(gè)線程池,用于執(zhí)行這些異步任務(wù),默認(rèn)使用的是 ForkJoinPool.commonPool() 線程池,個(gè)人覺得沒有必要去自定義線程池,或者使用其它的線程池,因?yàn)?Caffeine 的作者在設(shè)計(jì)的時(shí)候就考慮了線程池的選擇,既然別人選擇了,就有一定道理。
如果一定要用其它的線程池,可以通過 executor() 方法設(shè)置,方法參數(shù)是一個(gè) 線程池對(duì)象。
5. 數(shù)據(jù)過期策略
5.1 expireAfterAccess
最后一次訪問之后,隔多久沒有被再次訪問的話,就過期。訪問包括了 讀 和 寫。舉個(gè)例子:
Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).expireAfterAccess(2,?TimeUnit.SECONDS).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); Thread.sleep(3000); System.out.println(cache.getIfPresent("one")); System.out.println(cache.getIfPresent("two"));運(yùn)行結(jié)果:
null nullexpireAfterAccess 包含兩個(gè)參數(shù),第二個(gè)參數(shù)是時(shí)間單位,第一個(gè)參數(shù)是時(shí)間大小,比如上述代碼中設(shè)置過期時(shí)間為 2 秒,在過了 3 秒之后,再次訪問數(shù)據(jù),發(fā)現(xiàn)數(shù)據(jù)不存在了,即觸發(fā)過期清除了。
5.2 expireAfterWrite
某個(gè)數(shù)據(jù)在多久沒有被更新后,就過期。舉個(gè)例子
Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).expireAfterWrite(2,?TimeUnit.SECONDS).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); Thread.sleep(1000); System.out.println(cache.getIfPresent("one").getName()); Thread.sleep(2000); System.out.println(cache.getIfPresent("one"));運(yùn)行結(jié)果:
one null只能是被更新,才能延續(xù)數(shù)據(jù)的生命,即便是數(shù)據(jù)被讀取了,也不行,時(shí)間一到,也會(huì)過期。
5.2 expireAfter
實(shí)話實(shí)說,關(guān)于這個(gè)設(shè)置項(xiàng),官網(wǎng)沒有說明白,網(wǎng)上其它博客更是千篇一律,沒有一個(gè)講明白的。此處簡(jiǎn)單講講我個(gè)人的測(cè)試用例與理解,如果有誤,歡迎評(píng)論指正。
Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).expireAfter(new?Expiry<String,?Person>()?{@Overridepublic?long?expireAfterCreate(String?s,?Person?person,?long?l)?{if(person.getAge()?>?60){?//首次存入緩存后,年齡大于?60?的,過期時(shí)間為?4?秒return?4000000000L;}return?2000000000L;?//?否則為?2?秒}@Overridepublic?long?expireAfterUpdate(String?s,?Person?person,?long?l,?long?l1)?{if(person.getName().equals("one")){?//?更新?one?這個(gè)人之后,過期時(shí)間為?8?秒return?8000000000L;}return?4000000000L;?//?更新其它人后,過期時(shí)間為?4?秒}@Overridepublic?long?expireAfterRead(String?s,?Person?person,?long?l,?long?l1)?{return?3000000000L;?//?每次被讀取后,過期時(shí)間為?3?秒}}).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build();expireAfter 方法的參數(shù)是一個(gè) Expiry 對(duì)象,Expiry 是一個(gè)接口,上述代碼用了匿名類。需要實(shí)現(xiàn) Expiry 的三個(gè)方法。
expireAfterCreate(String s, Person person, long l) :此方法為數(shù)據(jù)<s , person> 創(chuàng)建之后,過期時(shí)間是多久(可以理解為生命周期),單位為納秒,方法的返回值就是過期時(shí)間,這個(gè)時(shí)間設(shè)置為多久,怎么設(shè)置,可以自定義的,比如上述代碼,60 歲以上的過期時(shí)間為 4 秒,如果 4 秒內(nèi)數(shù)據(jù)沒有被操作,就過期。另外還有一個(gè)參數(shù) long l,l 表示創(chuàng)建時(shí)間的系統(tǒng)時(shí)間戳,單位為納秒。
expireAfterUpdate(String s, Person person, long l, long l1):此方法表示更新某個(gè)數(shù)據(jù)后,過期時(shí)間是多久(刷新生命周期),個(gè)人認(rèn)為:參數(shù) l 表示更新前的系統(tǒng)時(shí)間戳,l1 表示更新成功后的系統(tǒng)時(shí)間戳,因?yàn)樵诙嗑€程下,更新操作可能會(huì)阻塞。
expireAfterRead(String s, Person person, long l, long l1) : 與 expireAfterUpdate 同理。
6. refreshAfterWrite 延遲刷新
refreshAfterWrite(long?duration,?TimeUnit?unit)寫操作完成后多久才將數(shù)據(jù)刷新進(jìn)緩存中,兩個(gè)參數(shù)只是用于設(shè)置時(shí)間長(zhǎng)短的。
只適用于 LoadingCache 和 AsyncLoadingCache,如果刷新操作沒有完成,讀取的數(shù)據(jù)只是舊數(shù)據(jù)。 同理,不想寫了。
7. removalListener 清除、更新監(jiān)聽
當(dāng)緩存中的數(shù)據(jù)發(fā)送更新,或者被清除時(shí),就會(huì)觸發(fā)監(jiān)聽器,在監(jiān)聽器里可以自定義一些處理手段,比如打印出哪個(gè)數(shù)據(jù)被清除,原因是什么。這個(gè)觸發(fā)和監(jiān)聽的過程是異步的,就是說可能數(shù)據(jù)都被刪除一小會(huì)兒了,監(jiān)聽器才監(jiān)聽到。
舉個(gè)例子:
MyStatsCounter?myStatsCounter?=?new?MyStatsCounter(); Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).removalListener((String?key,?Person?value,?RemovalCause?cause)->{System.out.println("被清除人的年齡:"?+?value.getAge()?+?";??清除的原因是:"?+?cause);}).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); cache.put("one",?new?Person(14,?"one")); cache.invalidate("one"); cache.put("three",?new?Person(31,?"three")); Thread.sleep(2000);運(yùn)行結(jié)果:
被清除人的年齡:12;??清除的原因是:REPLACED 被清除人的年齡:14;??清除的原因是:EXPLICIT 被清除人的年齡:18;??清除的原因是:SIZEremovalListener 方法的參數(shù)是一個(gè) RemovalListener 對(duì)象,但是可以函數(shù)式傳參,如上述代碼,當(dāng)數(shù)據(jù)被更新或者清除時(shí),會(huì)給監(jiān)聽器提供三個(gè)內(nèi)容,(鍵,值,原因)分別對(duì)應(yīng)代碼中的三個(gè)參數(shù),(鍵,值)都是更新前,清除前的舊值, 這樣可以了解到清除的詳細(xì)了。
清除的原因有 5 個(gè),存儲(chǔ)在枚舉類 RemovalCause 中:
EXPLICIT : 表示顯式地調(diào)用刪除操作,直接將某個(gè)數(shù)據(jù)刪除。
REPLACED:表示某個(gè)數(shù)據(jù)被更新。
EXPIRED:表示因?yàn)樯芷诮Y(jié)束(過期時(shí)間到了),而被清除。
SIZE:表示因?yàn)榫彺婵臻g大小受限,總權(quán)重受限,而被清除。
COLLECTED : 這個(gè)不明白。
8. 緩存的數(shù)據(jù)使用弱引用,軟引用
AsyncCache 緩存不支持軟引用和弱引用。
weakKeys():將緩存的 key 使用弱引用包裝起來,只要 GC 的時(shí)候,就能被回收。
weakValues():將緩存的 value 使用弱引用包裝起來,只要 GC 的時(shí)候,就能被回收。
softValues():將緩存的 value使用軟引用包裝起來,只要 GC 的時(shí)候,有必要,就能被回收。
關(guān)于軟引用,弱引用,強(qiáng)引用,虛引用,可以參考:
https://blog.csdn.net/dgh112233/article/details/107288545
因此,弱引用 ,軟引用的設(shè)置,只是為了方便回收空間,節(jié)省空間,但是使用的時(shí)候注意一點(diǎn),緩存查詢時(shí),是用 == 來判斷兩個(gè) key 是否相等,比較的是地址,不是 key 本身的內(nèi)容,很容易造成一種現(xiàn)象:命名 key 是對(duì)的,但就是無法命中,因?yàn)?key 的內(nèi)容相等,但是地址卻不同,會(huì)被認(rèn)為是兩個(gè) key。
9. 時(shí)間源 ticker
不了解,感覺默認(rèn)用系統(tǒng)的時(shí)鐘就好了。
10. 同步監(jiān)聽器
之前的 removalListener 是異步監(jiān)聽,此處的 writer 方法可以設(shè)置同步監(jiān)聽器,同步監(jiān)聽器一個(gè)實(shí)現(xiàn)了接口 CacheWriter 的實(shí)例化對(duì)象,我們需要自定義接口的實(shí)現(xiàn)類,比如:
public?class?MyCacheWriter?implements?CacheWriter<String,?Application.Person>?{@Overridepublic?void?write(String?s,?Application.Person?person)?{System.out.println("新增/更新了一個(gè)新數(shù)據(jù):"?+?person.getName());}@Overridepublic?void?delete(String?s,?Application.Person?person,?RemovalCause?removalCause)?{System.out.println("刪除了一個(gè)數(shù)據(jù):"?+?person.getName());} }關(guān)鍵是要實(shí)現(xiàn) CacheWriter 接口的兩個(gè)方法,當(dāng)新增,更新某個(gè)數(shù)據(jù)時(shí),會(huì)同步觸發(fā) write 方法的執(zhí)行。當(dāng)刪除某個(gè)數(shù)據(jù)時(shí),會(huì)觸發(fā) delete 方法的執(zhí)行。
Caffeine<String,?Person>?caffeine?=?Caffeine.newBuilder().maximumWeight(30).writer(new?MyCacheWriter()).weigher((String?key,?Person?value)->?value.getAge()); Cache<String,?Person>?cache?=?caffeine.build(); cache.put("one",?new?Person(12,?"one")); cache.put("two",?new?Person(18,?"two")); cache.invalidate("two");運(yùn)行結(jié)果:
新增/更新了一個(gè)新數(shù)據(jù):one 新增/更新了一個(gè)新數(shù)據(jù):two 刪除了一個(gè)數(shù)據(jù):two2. Cache 可以有的操作
V getIfPresent(K key) :如果緩存中 key 存在,則獲取 value,否則返回 null。
void put( K key, V value):存入一對(duì)數(shù)據(jù) <key, value>。
Map<K, V> getAllPresent(Iterable<?> var1) :參數(shù)是一個(gè)迭代器,表示可以批量查詢緩存。
void putAll( Map<? extends K, ? extends V> var1): 批量存入緩存。
void invalidate(K var1):刪除某個(gè) key 對(duì)應(yīng)的數(shù)據(jù)。
void invalidateAll(Iterable<?> var1):批量刪除數(shù)據(jù)。
void invalidateAll():清空緩存。
long estimatedSize():返回緩存中數(shù)據(jù)的個(gè)數(shù)。
CacheStats stats():返回緩存當(dāng)前的狀態(tài)指標(biāo)集。
ConcurrentMap<K, V> asMap():將緩存中所有的數(shù)據(jù)構(gòu)成一個(gè) map。
void cleanUp():會(huì)對(duì)緩存進(jìn)行整體的清理,比如有一些數(shù)據(jù)過期了,但是并不會(huì)立馬被清除,所以執(zhí)行一次 cleanUp 方法,會(huì)對(duì)緩存進(jìn)行一次檢查,清除那些應(yīng)該清除的數(shù)據(jù)。
V get( K var1, Function<? super K, ? extends V> var2):第一個(gè)參數(shù)是想要獲取的 key,第二個(gè)參數(shù)是函數(shù),例子如下:
可以著重考慮一下第二個(gè)參數(shù)的寫法,如果寫成從數(shù)據(jù)庫查詢的話,那就很完整了。
還有另外兩種緩存:LoadingCache, AsyncLoadingCache。
來源:blog.csdn.net/dgh112233/article/
details/118915259
推薦
Java面試題寶典
技術(shù)內(nèi)卷群,一起來學(xué)習(xí)!!
PS:因?yàn)楣娞?hào)平臺(tái)更改了推送規(guī)則,如果不想錯(cuò)過內(nèi)容,記得讀完點(diǎn)一下“在看”,加個(gè)“星標(biāo)”,這樣每次新文章推送才會(huì)第一時(shí)間出現(xiàn)在你的訂閱列表里。點(diǎn)“在看”支持我們吧!
總結(jié)
以上是生活随笔為你收集整理的本地缓存之王——Caffeine 组件最强讲解!的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: HR专家训练营-X版本 成为HR专家系列
- 下一篇: 关于对Caffe适用场景的思考