解析高性能進(jìn)程緩存-caffeine

1.簡(jiǎn)介

? ? 對(duì)于用戶(hù)來(lái)說(shuō),響應(yīng)的快慢是判斷一個(gè)系統(tǒng)的重要指標(biāo),緩存就是必不可少的優(yōu)化工具,在一個(gè)高并發(fā)的場(chǎng)景中往往占有著非常重要的角色,所以開(kāi)發(fā)人員需要根據(jù)不同的應(yīng)用場(chǎng)景來(lái)選擇不同的緩存框架,比如分布式緩存redis,或者進(jìn)程緩存GuavaCache。

? ? 進(jìn)程緩存與Map之間的本質(zhì)區(qū)別就是能自動(dòng)的回收存儲(chǔ)的元素,而GuavaCache是一款非常優(yōu)秀的進(jìn)程緩存框架,很好的提供了讀寫(xiě)和自動(dòng)失效的功能。而今天要介紹的進(jìn)程緩存Caffeine,在設(shè)計(jì)上參考了GuavaCache的經(jīng)驗(yàn),也進(jìn)行了大量的改進(jìn)優(yōu)化,以下數(shù)據(jù)圖片均來(lái)源于Caffeine GitHub地址:caffeine,首先是讀寫(xiě)性能的比較:

8個(gè)線(xiàn)程同時(shí)從緩存中讀取


8個(gè)線(xiàn)程同時(shí)從緩存中寫(xiě)入


6個(gè)線(xiàn)程讀取,2個(gè)線(xiàn)程寫(xiě)入

可以看到caffeine在讀寫(xiě)方面明顯優(yōu)與其他框架,在緩存命中率上Caffeine也不同于Guava,采用了更為優(yōu)秀的Window TinyLfu算法,該算法是在LRU的基礎(chǔ)上改進(jìn)的版本。


2.填充策略

(1)手動(dòng)填充

手動(dòng)填充

? ? newBuilder方法只是Caffeine類(lèi)的一個(gè)空的構(gòu)造函數(shù),類(lèi)屬性的實(shí)例化是在build方法中進(jìn)行的,put方法就是手動(dòng)填充緩存。newBuilder方法后面還能跟很多配置方法,比如

我們也可以使用 get 方法獲取值,該方法將一個(gè)參數(shù)為 key 的 Function 作為參數(shù)傳入。如果緩存中不存在該 key,則該函數(shù)將用于提供默認(rèn)值,該值在計(jì)算后插入緩存中。
Caffeine類(lèi)是Caffeine的基礎(chǔ)類(lèi),里面提供了很多配置方法和參數(shù):

maximumSize:設(shè)置緩存最大條目數(shù),超過(guò)條目則觸發(fā)回收。?
maximumWeight:設(shè)置緩存最大權(quán)重,設(shè)置權(quán)重是通過(guò)weigher方法, 需要注意的是權(quán)重也是限制緩存大小的參數(shù),并不會(huì)影響緩存淘汰策略,也不能和maximumSize方法一起使用。?
weakKeys:將key設(shè)置為弱引用,在GC時(shí)可以直接淘汰
weakValues:將value設(shè)置為弱引用,在GC時(shí)可以直接淘汰
softValues:將value設(shè)置為軟引用,在內(nèi)存溢出前可以直接淘汰
expireAfterWrite:寫(xiě)入后隔段時(shí)間過(guò)期
expireAfterAccess:訪(fǎng)問(wèn)后隔斷時(shí)間過(guò)期
refreshAfterWrite:寫(xiě)入后隔斷時(shí)間刷新
removalListener:緩存淘汰監(jiān)聽(tīng)器,配置監(jiān)聽(tīng)器后,每個(gè)條目淘汰時(shí)都會(huì)調(diào)用該監(jiān)聽(tīng)器
writer:writer監(jiān)聽(tīng)器其實(shí)提供了兩個(gè)監(jiān)聽(tīng),一個(gè)是緩存寫(xiě)入或更新是的write,一個(gè)是緩存淘汰時(shí)的delete,每個(gè)條目淘汰時(shí)都會(huì)調(diào)用該監(jiān)聽(tīng)器

手動(dòng)填充表示任何數(shù)據(jù)都需要手動(dòng)put到cache中,沒(méi)有任何自動(dòng)加載策略。put方法會(huì)覆蓋相同key的條目

(2)同步填充? ?

同步填充

通過(guò)在build方法中傳入一個(gè)CacheLoader的實(shí)現(xiàn)來(lái)進(jìn)行同步填充,CacheLoader中的load方法制定了對(duì)key的計(jì)算,也可以重寫(xiě)loadAll來(lái)進(jìn)行批量計(jì)算。

還有種方法是通過(guò)在build方法中傳入一個(gè)參數(shù)為 key 的 Function來(lái)進(jìn)行同步填充,這種方法類(lèi)似于手動(dòng)填充中的get方法。

(3)異步填充

異步填充

異步填充于同步填充大致相似,區(qū)別是傳入一個(gè)執(zhí)行器進(jìn)行異步執(zhí)行,并且返回一個(gè)CompletableFuture對(duì)象,可以通過(guò)CompletableFuture.get來(lái)獲取數(shù)據(jù)并設(shè)置超時(shí)時(shí)間。


3.回收策略

? ? 條目的自動(dòng)淘汰回收是map于cache最大的區(qū)別,Caffeine同樣包含了3中緩存回收機(jī)制,分別是基于大小,基于時(shí)間,基于引用類(lèi)型。

(1)基于大小

基于大小

? ? 設(shè)置了maximumSize屬性大小為1,cache實(shí)例化是緩存size為0,執(zhí)行了第一個(gè)put方法后緩存到達(dá)上限,第二個(gè)put執(zhí)行后會(huì)回收第一個(gè)緩存。調(diào)用cleanUp方法是因?yàn)榫彺婊厥帐钱惒綀?zhí)行,cleanUp可以等待異步執(zhí)行完成。

基于權(quán)重
執(zhí)行結(jié)果

除了設(shè)置maximumSize外,設(shè)置maximumWeight也可以進(jìn)行基于大小的緩存回收,weigher簡(jiǎn)單的設(shè)定了每個(gè)條目的權(quán)重為5,進(jìn)行2次put后權(quán)重達(dá)到上限,所以第三次put執(zhí)行時(shí)會(huì)進(jìn)行回收。

(2)基于時(shí)間

基于時(shí)間

基于時(shí)間的方式主要是三種配置:
? ? expireAfterWrite:上次寫(xiě)入后開(kāi)始計(jì)時(shí)
? ? expireAfterAccess:上次訪(fǎng)問(wèn)后開(kāi)始計(jì)時(shí),包括讀和寫(xiě)
? ? expireAfter:自定義的時(shí)間計(jì)時(shí)器

(3)基于引用


基于引用

我們可以顯式的定義key或value為弱引用,或者value單獨(dú)定義為軟引用,這樣就會(huì)啟用基于引用的回收策略了,主要用到Java的GC進(jìn)行回收。
? ? 軟引用:在內(nèi)存溢出前回收
? ? 弱引用:在下次GC時(shí)回收
使用到的回收策略時(shí)LRU算法

RemovalCause

RemovalCause是一個(gè)enum,記錄了緩存失效的原因,并且通過(guò)wasEvicted方法定義是否是自動(dòng)淘汰。
EXPLICIT? ? //手動(dòng)調(diào)用invalidate或remove等方法
REPLACED? ? ? ? //調(diào)用put等方法進(jìn)行修改
COLLECTED? ? //設(shè)置了key或value的引用方式
EXPIRED? ? //設(shè)置了過(guò)期時(shí)間
SIZE? ? //設(shè)置了大小


4.刷新

cache除了會(huì)自動(dòng)淘汰,也能進(jìn)行自動(dòng)刷新操作

自動(dòng)刷新

refreshAfterWrite就是設(shè)置寫(xiě)入后多就會(huì)刷新,expireAfterWrite和refreshAfterWrite的區(qū)別是,當(dāng)緩存過(guò)期后,配置了expireAfterWrite,則調(diào)用時(shí)會(huì)阻塞,等待緩存計(jì)算完成,返回新的值并進(jìn)行緩存,refreshAfterWrite則是返回一個(gè)舊值,并異步計(jì)算新值并緩存。


5.源碼解析

? ? 說(shuō)完了基本的功能,接下來(lái)我們簡(jiǎn)單的解析一下Caffeine內(nèi)部的實(shí)現(xiàn),因?yàn)镃affeine設(shè)計(jì)復(fù)雜,功能強(qiáng)大,所以本篇先進(jìn)行粗力度的解析。如有錯(cuò)誤歡迎指正。
? ? 首先我們看看在構(gòu)建cache的時(shí)候用來(lái)區(qū)分填充方式的build方法:

build

可以看到build方法都伴隨這一個(gè)三目運(yùn)算符,并且最后會(huì)實(shí)例化兩個(gè)子類(lèi)返回,buildAsync方法內(nèi)部也是這樣的實(shí)現(xiàn)。那么這些實(shí)現(xiàn)類(lèi)是干什么用的呢,我們先要明白Caffeine內(nèi)部接口的一個(gè)大致關(guān)系。

Cache

首先是Caffeine的Cache接口,這個(gè)接口是Caffeine最底層的一個(gè)接口,主要提供了一些方法定義:

V getIfPresent(@Nonnull Object key);? ? ? ? ? ? ? ? ? ? //獲取緩存條目,不存在則返回NULL
V get(@Nonnull K key, @Nonnull Function<? super K, ? extends V> mappingFunction);? ? //獲取緩存條目,不存在則執(zhí)行mappingFunction進(jìn)行計(jì)算,并存入緩存
Map<K, V> getAllPresent(@Nonnull Iterable<?> keys);? ? //批量獲取條目,返回一個(gè)Map
void put(@Nonnull K key, @Nonnull V value);? ? //插入一個(gè)條目到緩存中
void putAll(@Nonnull Map<? extends K,? extends V> map);? ? //批量緩存數(shù)據(jù)
void invalidate(@Nonnull Object key);? ? //回收一個(gè)條目
void invalidateAll(@Nonnull Iterable<?> keys);? ? 批量回收條目
void invalidateAll();? ? //回收全部條目
long estimatedSize();? ? //獲取緩存大小
CacheStats stats();? ? //獲取緩存狀態(tài)
ConcurrentMap<K, V> asMap();? ? //轉(zhuǎn)換為ConcurrentMap
void cleanUp();? ? //觸發(fā)清除緩存
Policy<K, V> policy();? ? //設(shè)定策略

LoadingCache

LoadingCache類(lèi)繼承自Cache,同時(shí)也定義了一些接口

V get(@Nonnull K key);? ? //獲取條目,沒(méi)有function參數(shù),但是為空會(huì)調(diào)用CacheLoader的loadMap<K, V> getAll(@Nonnull Iterable<? extends K> keys);? ? //獲取條目,為空會(huì)調(diào)用CacheLoader的loadAllvoid refresh(@Nonnull K key);? ? //會(huì)異步的通過(guò)CacheLoader的load更新緩存

可以看到Cache接口更像是Map,用來(lái)存放key-value,而LoadingCache定義了加載和更新的機(jī)制,通過(guò)build方法中傳入的CacheLoader來(lái)操作條目。

LocalManualCache

LocalManualCache也繼承自Cache,這個(gè)接口有兩個(gè)主要的實(shí)現(xiàn)類(lèi),就是上文提到的BoundedLocalManualCache和UnboundedLocalManualCache。這些是實(shí)現(xiàn)類(lèi)提供了Cache的具體實(shí)現(xiàn),并且UnboundedLocalManualCache也最低限度的提供了LocalCache的功能。而卻分使用這兩個(gè)實(shí)現(xiàn)的方式就是看我們是否配置了回收策略。

UnboundedLocalManualCache

如果我們沒(méi)有配置任何的回收策略,則會(huì)默認(rèn)使用UnboundedLocalManualCache。

UnboundedLocalManualCache

? ? 該實(shí)現(xiàn)類(lèi)最低限度的提供了緩存的功能,初始化時(shí)提供了一個(gè)默認(rèn)大小為16的ConcurrentHashMap用來(lái)存儲(chǔ)數(shù)據(jù),也提供了基本的狀態(tài)計(jì)數(shù)器,刪除監(jiān)聽(tīng)器,編寫(xiě)器等。由于沒(méi)有任何主動(dòng)的回收策略,UnboundedLocalManualCache的本質(zhì)就是對(duì)Map的操作。

BoundedLocalManualCache

BoundedLocalManualCache是有回收策略的,所有Caffeine對(duì)于設(shè)置的每種回收策略都有一個(gè)對(duì)應(yīng)的實(shí)現(xiàn)類(lèi),所以就有了LocalCacheFactory類(lèi)來(lái)構(gòu)建響應(yīng)的實(shí)現(xiàn)類(lèi)。

LocalCacheFactory

newBoundedLocalCache針對(duì)我們配置的每種情況都拼接了一個(gè)字符,最終得到一個(gè)對(duì)應(yīng)的實(shí)現(xiàn)類(lèi)名,這樣窮舉性的寫(xiě)法也是因?yàn)镃affeine對(duì)每種情況都作出了優(yōu)化。

LocalCacheFactory的實(shí)現(xiàn)類(lèi)

newBoundedLocalCache方法最后返回一個(gè)BoundedLocalCache,也是我們最終用到的實(shí)現(xiàn)類(lèi)。


6.緩存過(guò)期策略解析

我們知道了Caffeine有三種過(guò)期策略,接下來(lái)我們來(lái)大致分析下Caffeine是怎么主動(dòng)的進(jìn)行緩存回收的。從源碼中我們找到了這樣兩個(gè)方法:

讀后操作
寫(xiě)后操作

這是在讀寫(xiě)時(shí)分別調(diào)用的兩個(gè)方法,進(jìn)行一些讀寫(xiě)的后續(xù)操作,其中都調(diào)用了一個(gè)scheduleDrainBuffers方法,這個(gè)方法就是用來(lái)進(jìn)行過(guò)期任務(wù)調(diào)度的。

scheduleDrainBuffers

首先嘗試加鎖,如果鎖失敗表明其他線(xiàn)程正在進(jìn)行操作。鎖成功后會(huì)執(zhí)行drainBuffersTask,也就是Caffeine的PerformCleanupTask異步回收。

PerformCleanupTask

PerformCleanupTask的performCleanUp方法會(huì)再次加鎖

maintenance

進(jìn)到maintenance方法中,在這里我們看到很多的方法,都是用來(lái)進(jìn)行回收的。
drainReadBuffer:讀緩存用盡
drainWriteBuffer:寫(xiě)緩存用盡
drainKeyReferences:key引用隊(duì)列耗盡
drainValueReferences:value引用耗盡
expireEntries:達(dá)到過(guò)期時(shí)間
evictEntries:達(dá)到大小限制

獲取到當(dāng)前時(shí)間后對(duì)expireAfterAccess進(jìn)行淘汰。

之后淘汰expireAfterWrite。

對(duì)于自定義時(shí)間通過(guò)時(shí)間輪來(lái)進(jìn)行淘汰。


最后

? ? 本篇文章大致介紹了Caffeine的使用方法,填充策略,回收策略以及粗略的進(jìn)行了源碼的解析,Caffeine是一款非常優(yōu)秀的緩存框架,使用的設(shè)計(jì)理念和代碼實(shí)現(xiàn)都讓我受益良多,之后有機(jī)會(huì)我會(huì)繼續(xù)進(jìn)行深入的理解和學(xué)習(xí),謝謝大家的瀏覽。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 簡(jiǎn)介 在本文中,我們來(lái)看看 Caffeine — 一個(gè)高性能的 Java 緩存庫(kù)。 緩存和 Map 之間的一個(gè)根本...
    xiaolyuh閱讀 71,597評(píng)論 5 48
  • 原文:http://www.baeldung.com/java-caching-caffeine作者:baeldu...
    Oopsguy閱讀 11,222評(píng)論 0 6
  • 緩存和 Map 之間的一個(gè)根本區(qū)別在于緩存可以回收存儲(chǔ)的 item?;厥詹呗詾樵谥付〞r(shí)間刪除哪些對(duì)象。此策略直接影...
    tracy_668閱讀 12,274評(píng)論 4 6
  • 1. 前言 互聯(lián)網(wǎng)軟件神速發(fā)展,用戶(hù)的體驗(yàn)度是判斷一個(gè)軟件好壞的重要原因,所以緩存就是必不可少的一個(gè)神器。在多線(xiàn)程...
    不知名的蛋撻閱讀 50,025評(píng)論 2 16
  • 大前天晚上放了兩任務(wù)到服務(wù)器上跑,今天早上查看發(fā)現(xiàn)任務(wù)已經(jīng)算完了。起初還高興“怎么這次這么快”,進(jìn)去一看才發(fā)現(xiàn)媽蛋...
    lxt閱讀 442評(píng)論 6 1

友情鏈接更多精彩內(nèi)容