解析高性能進程緩存-caffeine


1.簡介

    對於用戶來說,響應的快慢是判斷一個系統的重要指標,緩存就是必不可少的優化工具,在一個高並發的場景中往往占有着非常重要的角色,所以開發人員需要根據不同的應用場景來選擇不同的緩存框架,比如分布式緩存redis,或者進程緩存GuavaCache。

    進程緩存與Map之間的本質區別就是能自動的回收存儲的元素,而GuavaCache是一款非常優秀的進程緩存框架,很好的提供了讀寫和自動失效的功能。而今天要介紹的進程緩存Caffeine,在設計上參考了GuavaCache的經驗,也進行了大量的改進優化,以下數據圖片均來源於Caffeine GitHub地址:caffeine,首先是讀寫性能的比較:

 
8個線程同時從緩存中讀取

 

 
8個線程同時從緩存中寫入

 

 
6個線程讀取,2個線程寫入

可以看到caffeine在讀寫方面明顯優與其他框架,在緩存命中率上Caffeine也不同於Guava,采用了更為優秀的Window TinyLfu算法,該算法是在LRU的基礎上改進的版本。


2.填充策略

(1)手動填充

 
手動填充

    newBuilder方法只是Caffeine類的一個空的構造函數,類屬性的實例化是在build方法中進行的,put方法就是手動填充緩存。newBuilder方法后面還能跟很多配置方法,比如

 
 

我們也可以使用 get 方法獲取值,該方法將一個參數為 key 的 Function 作為參數傳入。如果緩存中不存在該 key,則該函數將用於提供默認值,該值在計算后插入緩存中。
Caffeine類是Caffeine的基礎類,里面提供了很多配置方法和參數:

maximumSize:設置緩存最大條目數,超過條目則觸發回收。 
maximumWeight:設置緩存最大權重,設置權重是通過weigher方法, 需要注意的是權重也是限制緩存大小的參數,並不會影響緩存淘汰策略,也不能和maximumSize方法一起使用。 
weakKeys:將key設置為弱引用,在GC時可以直接淘汰
weakValues:將value設置為弱引用,在GC時可以直接淘汰
softValues:將value設置為軟引用,在內存溢出前可以直接淘汰
expireAfterWrite:寫入后隔段時間過期
expireAfterAccess:訪問后隔斷時間過期
refreshAfterWrite:寫入后隔斷時間刷新
removalListener:緩存淘汰監聽器,配置監聽器后,每個條目淘汰時都會調用該監聽器
writer:writer監聽器其實提供了兩個監聽,一個是緩存寫入或更新是的write,一個是緩存淘汰時的delete,每個條目淘汰時都會調用該監聽器

手動填充表示任何數據都需要手動put到cache中,沒有任何自動加載策略。put方法會覆蓋相同key的條目

(2)同步填充   

 
同步填充

通過在build方法中傳入一個CacheLoader的實現來進行同步填充,CacheLoader中的load方法制定了對key的計算,也可以重寫loadAll來進行批量計算。

 
 

還有種方法是通過在build方法中傳入一個參數為 key 的 Function來進行同步填充,這種方法類似於手動填充中的get方法。

(3)異步填充

 
異步填充

異步填充於同步填充大致相似,區別是傳入一個執行器進行異步執行,並且返回一個CompletableFuture對象,可以通過CompletableFuture.get來獲取數據並設置超時時間。


3.回收策略

    條目的自動淘汰回收是map於cache最大的區別,Caffeine同樣包含了3中緩存回收機制,分別是基於大小,基於時間,基於引用類型。

(1)基於大小

 
基於大小
 
 

    設置了maximumSize屬性大小為1,cache實例化是緩存size為0,執行了第一個put方法后緩存到達上限,第二個put執行后會回收第一個緩存。調用cleanUp方法是因為緩存回收是異步執行,cleanUp可以等待異步執行完成。

 
基於權重
 
執行結果

除了設置maximumSize外,設置maximumWeight也可以進行基於大小的緩存回收,weigher簡單的設定了每個條目的權重為5,進行2次put后權重達到上限,所以第三次put執行時會進行回收。

(2)基於時間

 
基於時間

基於時間的方式主要是三種配置:
    expireAfterWrite:上次寫入后開始計時
    expireAfterAccess:上次訪問后開始計時,包括讀和寫
    expireAfter:自定義的時間計時器

(3)基於引用

 

 
基於引用

我們可以顯式的定義key或value為弱引用,或者value單獨定義為軟引用,這樣就會啟用基於引用的回收策略了,主要用到Java的GC進行回收。
    軟引用:在內存溢出前回收
    弱引用:在下次GC時回收
使用到的回收策略時LRU算法

RemovalCause

RemovalCause是一個enum,記錄了緩存失效的原因,並且通過wasEvicted方法定義是否是自動淘汰。
EXPLICIT    //手動調用invalidate或remove等方法
REPLACED        //調用put等方法進行修改
COLLECTED    //設置了key或value的引用方式
EXPIRED    //設置了過期時間
SIZE    //設置了大小


4.刷新

cache除了會自動淘汰,也能進行自動刷新操作

 
自動刷新

refreshAfterWrite就是設置寫入后多就會刷新,expireAfterWrite和refreshAfterWrite的區別是,當緩存過期后,配置了expireAfterWrite,則調用時會阻塞,等待緩存計算完成,返回新的值並進行緩存,refreshAfterWrite則是返回一個舊值,並異步計算新值並緩存。


5.源碼解析

    說完了基本的功能,接下來我們簡單的解析一下Caffeine內部的實現,因為Caffeine設計復雜,功能強大,所以本篇先進行粗力度的解析。如有錯誤歡迎指正。
    首先我們看看在構建cache的時候用來區分填充方式的build方法:

 
build

可以看到build方法都伴隨這一個三目運算符,並且最后會實例化兩個子類返回,buildAsync方法內部也是這樣的實現。那么這些實現類是干什么用的呢,我們先要明白Caffeine內部接口的一個大致關系。

Cache

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

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

LoadingCache

LoadingCache類繼承自Cache,同時也定義了一些接口

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

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

LocalManualCache

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

UnboundedLocalManualCache

如果我們沒有配置任何的回收策略,則會默認使用UnboundedLocalManualCache。

 
UnboundedLocalManualCache

    該實現類最低限度的提供了緩存的功能,初始化時提供了一個默認大小為16的ConcurrentHashMap用來存儲數據,也提供了基本的狀態計數器,刪除監聽器,編寫器等。由於沒有任何主動的回收策略,UnboundedLocalManualCache的本質就是對Map的操作。

BoundedLocalManualCache

BoundedLocalManualCache是有回收策略的,所有Caffeine對於設置的每種回收策略都有一個對應的實現類,所以就有了LocalCacheFactory類來構建響應的實現類。

 
LocalCacheFactory

newBoundedLocalCache針對我們配置的每種情況都拼接了一個字符,最終得到一個對應的實現類名,這樣窮舉性的寫法也是因為Caffeine對每種情況都作出了優化。

 
LocalCacheFactory的實現類

newBoundedLocalCache方法最后返回一個BoundedLocalCache,也是我們最終用到的實現類。


6.緩存過期策略解析

我們知道了Caffeine有三種過期策略,接下來我們來大致分析下Caffeine是怎么主動的進行緩存回收的。從源碼中我們找到了這樣兩個方法:

 
讀后操作
 
寫后操作

這是在讀寫時分別調用的兩個方法,進行一些讀寫的后續操作,其中都調用了一個scheduleDrainBuffers方法,這個方法就是用來進行過期任務調度的。

 
scheduleDrainBuffers

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

 
PerformCleanupTask
 
 

PerformCleanupTask的performCleanUp方法會再次加鎖

 
maintenance

進到maintenance方法中,在這里我們看到很多的方法,都是用來進行回收的。
drainReadBuffer:讀緩存用盡
drainWriteBuffer:寫緩存用盡
drainKeyReferences:key引用隊列耗盡
drainValueReferences:value引用耗盡
expireEntries:達到過期時間
evictEntries:達到大小限制

 
 
 
 

獲取到當前時間后對expireAfterAccess進行淘汰。

 
 

之后淘汰expireAfterWrite。

 
 

對於自定義時間通過時間輪來進行淘汰。


最后

    本篇文章大致介紹了Caffeine的使用方法,填充策略,回收策略以及粗略的進行了源碼的解析,Caffeine是一款非常優秀的緩存框架,使用的設計理念和代碼實現都讓我受益良多,之后有機會我會繼續進行深入的理解和學習,謝謝大家的瀏覽。

 
 
鏈接:https://www.jianshu.com/p/6eb8b0a16c12


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM