高性能緩存?Caffeine?原理及實(shí)戰(zhàn)
時(shí)間:2021-08-19 15:51:26
手機(jī)看文章
掃描二維碼
隨時(shí)隨地手機(jī)看文章
[導(dǎo)讀]一、簡(jiǎn)介Caffeine是基于Java8開發(fā)的、提供了近乎最佳命中率的高性能本地緩存組件,Spring5開始不再支持GuavaCache,改為使用Caffeine。下面是Caffeine官方測(cè)試報(bào)告。由上面三幅圖可見:不管在并發(fā)讀、并發(fā)寫還是并發(fā)讀寫的場(chǎng)景下,Caffeine的性...
一、簡(jiǎn)介
Caffeine 是基于Java 8 開發(fā)的、提供了近乎最佳命中率的高性能本地緩存組件,Spring5 開始不再支持 Guava Cache,改為使用 Caffeine。
下面是 Caffeine 官方測(cè)試報(bào)告。



由上面三幅圖可見:不管在并發(fā)讀、并發(fā)寫還是并發(fā)讀寫的場(chǎng)景下,Caffeine 的性能都大幅領(lǐng)先于其他本地開源緩存組件。
本文先介紹 Caffeine 實(shí)現(xiàn)原理,再講解如何在項(xiàng)目中使用 Caffeine 。
?二、Caffeine 原理
2.1 淘汰算法
2.1.1 常見算法
對(duì)于 Java 進(jìn)程內(nèi)緩存我們可以通過 HashMap 來實(shí)現(xiàn)。不過,Java 進(jìn)程內(nèi)存是有限的,不可能無限地往里面放緩存對(duì)象。這就需要有合適的算法輔助我們淘汰掉使用價(jià)值相對(duì)不高的對(duì)象,為新進(jìn)的對(duì)象留有空間。常見的緩存淘汰算法有 FIFO、LRU、LFU。
FIFO(First In First Out):先進(jìn)先出。它是優(yōu)先淘汰掉最先緩存的數(shù)據(jù)、是最簡(jiǎn)單的淘汰算法。缺點(diǎn)是如果先緩存的數(shù)據(jù)使用頻率比較高的話,那么該數(shù)據(jù)就不停地進(jìn)進(jìn)出出,因此它的緩存命中率比較低。
LRU(Least Recently Used):最近最久未使用。它是優(yōu)先淘汰掉最久未訪問到的數(shù)據(jù)。缺點(diǎn)是不能很好地應(yīng)對(duì)偶然的突發(fā)流量。比如一個(gè)數(shù)據(jù)在一分鐘內(nèi)的前59秒訪問很多次,而在最后1秒沒有訪問,但是有一批冷門數(shù)據(jù)在最后一秒進(jìn)入緩存,那么熱點(diǎn)數(shù)據(jù)就會(huì)被沖刷掉。
LFU(Least Frequently Used):最近最少頻率使用。它是優(yōu)先淘汰掉最不經(jīng)常使用的數(shù)據(jù),需要維護(hù)一個(gè)表示使用頻率的字段。主要有兩個(gè)缺點(diǎn):一、如果訪問頻率比較高的話,頻率字段會(huì)占據(jù)一定的空間;二、無法合理更新新上的熱點(diǎn)數(shù)據(jù),比如某個(gè)歌手的老歌播放歷史較多,新出的歌如果和老歌一起排序的話,就永無出頭之日。
2.1.2 W-TinyLFU 算法
Caffeine 使用了 W-TinyLFU 算法,解決了 LRU 和LFU上述的缺點(diǎn)。W-TinyLFU 算法由論文《TinyLFU: A Highly Efficient Cache Admission Policy》提出。
它主要干了兩件事:一、采用 Count–Min Sketch 算法降低頻率信息帶來的內(nèi)存消耗;二、維護(hù)一個(gè)PK機(jī)制保障新上的熱點(diǎn)數(shù)據(jù)能夠緩存。
如下圖所示,Count–Min Sketch 算法類似布隆過濾器 (Bloom filter)思想,對(duì)于頻率統(tǒng)計(jì)我們其實(shí)不需要一個(gè)精確值。存儲(chǔ)數(shù)據(jù)時(shí),對(duì)key進(jìn)行多次 hash 函數(shù)運(yùn)算后,二維數(shù)組不同位置存儲(chǔ)頻率(Caffeine 實(shí)際實(shí)現(xiàn)的時(shí)候是用一維 long 型數(shù)組,每個(gè) long 型數(shù)字切分成16份,每份4bit,默認(rèn)15次為最高訪問頻率,每個(gè)key實(shí)際 hash 了四次,落在不同 long 型數(shù)字的16份中某個(gè)位置)。讀取某個(gè)key的訪問次數(shù)時(shí),會(huì)比較所有位置上的頻率值,取最小值返回。對(duì)于所有key的訪問頻率之和有個(gè)最大值,當(dāng)達(dá)到最大值時(shí),會(huì)進(jìn)行reset即對(duì)各個(gè)緩存key的頻率除以2。

如下圖緩存訪問頻率存儲(chǔ)主要分為兩大部分,即 LRU 和 Segmented LRU 。新訪問的數(shù)據(jù)會(huì)進(jìn)入第一個(gè) LRU,在 Caffeine 里叫 WindowDeque。當(dāng) WindowDeque 滿時(shí),會(huì)進(jìn)入 Segmented LRU 中的 ProbationDeque,在后續(xù)被訪問到時(shí),它會(huì)被提升到 ProtectedDeque。當(dāng) ProtectedDeque 滿時(shí),會(huì)有數(shù)據(jù)降級(jí)到 ProbationDeque 。數(shù)據(jù)需要淘汰的時(shí)候,對(duì) ProbationDeque 中的數(shù)據(jù)進(jìn)行淘汰。具體淘汰機(jī)制:取ProbationDeque 中的隊(duì)首和隊(duì)尾進(jìn)行 PK,隊(duì)首數(shù)據(jù)是最先進(jìn)入隊(duì)列的,稱為受害者,隊(duì)尾的數(shù)據(jù)稱為攻擊者,比較兩者 頻率大小,大勝小汰。

總的來說,通過 reset 衰減,避免歷史熱點(diǎn)數(shù)據(jù)由于頻率值比較高一直淘汰不掉,并且通過對(duì)訪問隊(duì)列分成三段,這樣避免了新加入的熱點(diǎn)數(shù)據(jù)早早地被淘汰掉。
2.2 高性能讀寫
Caffeine 認(rèn)為讀操作是頻繁的,寫操作是偶爾的,讀寫都是異步線程更新頻率信息。
2.2.1 讀緩沖
傳統(tǒng)的緩存實(shí)現(xiàn)將會(huì)為每個(gè)操作加鎖,以便能夠安全的對(duì)每個(gè)訪問隊(duì)列的元素進(jìn)行排序。一種優(yōu)化方案是將每個(gè)操作按序加入到緩沖區(qū)中進(jìn)行批處理操作。讀完把數(shù)據(jù)放到環(huán)形隊(duì)列 RingBuffer 中,為了減少讀并發(fā),采用多個(gè) RingBuffer,每個(gè)線程都有對(duì)應(yīng)的 RingBuffer。環(huán)形隊(duì)列是一個(gè)定長(zhǎng)數(shù)組,提供高性能的能力并最大程度上減少了 GC所帶來的性能開銷。數(shù)據(jù)丟到隊(duì)列之后就返回讀取結(jié)果,類似于數(shù)據(jù)庫的WAL機(jī)制,和ConcurrentHashMap 讀取數(shù)據(jù)相比,僅僅多了把數(shù)據(jù)放到隊(duì)列這一步。異步線程并發(fā)讀取 RingBuffer 數(shù)組,更新訪問信息,這邊的線程池使用的是下文實(shí)戰(zhàn)小節(jié)講的 Caffeine 配置參數(shù)中的 executor。
2.2.2 寫緩沖
與讀緩沖類似,寫緩沖是為了儲(chǔ)存寫事件。讀緩沖中的事件主要是為了優(yōu)化驅(qū)逐策略的命中率,因此讀緩沖中的事件完整程度允許一定程度的有損。但是寫緩沖并不允許數(shù)據(jù)的丟失,因此其必須實(shí)現(xiàn)為一個(gè)安全的隊(duì)列。Caffeine 寫是把數(shù)據(jù)放入MpscGrowableArrayQueue 阻塞隊(duì)列中,它參考了JCTools里的MpscGrowableArrayQueue ,是針對(duì) MPSC- 多生產(chǎn)者單消費(fèi)者(Multi-Producer
Caffeine 是基于Java 8 開發(fā)的、提供了近乎最佳命中率的高性能本地緩存組件,Spring5 開始不再支持 Guava Cache,改為使用 Caffeine。
下面是 Caffeine 官方測(cè)試報(bào)告。



由上面三幅圖可見:不管在并發(fā)讀、并發(fā)寫還是并發(fā)讀寫的場(chǎng)景下,Caffeine 的性能都大幅領(lǐng)先于其他本地開源緩存組件。
本文先介紹 Caffeine 實(shí)現(xiàn)原理,再講解如何在項(xiàng)目中使用 Caffeine 。
?二、Caffeine 原理
2.1 淘汰算法
2.1.1 常見算法
對(duì)于 Java 進(jìn)程內(nèi)緩存我們可以通過 HashMap 來實(shí)現(xiàn)。不過,Java 進(jìn)程內(nèi)存是有限的,不可能無限地往里面放緩存對(duì)象。這就需要有合適的算法輔助我們淘汰掉使用價(jià)值相對(duì)不高的對(duì)象,為新進(jìn)的對(duì)象留有空間。常見的緩存淘汰算法有 FIFO、LRU、LFU。
FIFO(First In First Out):先進(jìn)先出。它是優(yōu)先淘汰掉最先緩存的數(shù)據(jù)、是最簡(jiǎn)單的淘汰算法。缺點(diǎn)是如果先緩存的數(shù)據(jù)使用頻率比較高的話,那么該數(shù)據(jù)就不停地進(jìn)進(jìn)出出,因此它的緩存命中率比較低。
LRU(Least Recently Used):最近最久未使用。它是優(yōu)先淘汰掉最久未訪問到的數(shù)據(jù)。缺點(diǎn)是不能很好地應(yīng)對(duì)偶然的突發(fā)流量。比如一個(gè)數(shù)據(jù)在一分鐘內(nèi)的前59秒訪問很多次,而在最后1秒沒有訪問,但是有一批冷門數(shù)據(jù)在最后一秒進(jìn)入緩存,那么熱點(diǎn)數(shù)據(jù)就會(huì)被沖刷掉。
LFU(Least Frequently Used):最近最少頻率使用。它是優(yōu)先淘汰掉最不經(jīng)常使用的數(shù)據(jù),需要維護(hù)一個(gè)表示使用頻率的字段。主要有兩個(gè)缺點(diǎn):一、如果訪問頻率比較高的話,頻率字段會(huì)占據(jù)一定的空間;二、無法合理更新新上的熱點(diǎn)數(shù)據(jù),比如某個(gè)歌手的老歌播放歷史較多,新出的歌如果和老歌一起排序的話,就永無出頭之日。
2.1.2 W-TinyLFU 算法
Caffeine 使用了 W-TinyLFU 算法,解決了 LRU 和LFU上述的缺點(diǎn)。W-TinyLFU 算法由論文《TinyLFU: A Highly Efficient Cache Admission Policy》提出。
它主要干了兩件事:一、采用 Count–Min Sketch 算法降低頻率信息帶來的內(nèi)存消耗;二、維護(hù)一個(gè)PK機(jī)制保障新上的熱點(diǎn)數(shù)據(jù)能夠緩存。
如下圖所示,Count–Min Sketch 算法類似布隆過濾器 (Bloom filter)思想,對(duì)于頻率統(tǒng)計(jì)我們其實(shí)不需要一個(gè)精確值。存儲(chǔ)數(shù)據(jù)時(shí),對(duì)key進(jìn)行多次 hash 函數(shù)運(yùn)算后,二維數(shù)組不同位置存儲(chǔ)頻率(Caffeine 實(shí)際實(shí)現(xiàn)的時(shí)候是用一維 long 型數(shù)組,每個(gè) long 型數(shù)字切分成16份,每份4bit,默認(rèn)15次為最高訪問頻率,每個(gè)key實(shí)際 hash 了四次,落在不同 long 型數(shù)字的16份中某個(gè)位置)。讀取某個(gè)key的訪問次數(shù)時(shí),會(huì)比較所有位置上的頻率值,取最小值返回。對(duì)于所有key的訪問頻率之和有個(gè)最大值,當(dāng)達(dá)到最大值時(shí),會(huì)進(jìn)行reset即對(duì)各個(gè)緩存key的頻率除以2。

如下圖緩存訪問頻率存儲(chǔ)主要分為兩大部分,即 LRU 和 Segmented LRU 。新訪問的數(shù)據(jù)會(huì)進(jìn)入第一個(gè) LRU,在 Caffeine 里叫 WindowDeque。當(dāng) WindowDeque 滿時(shí),會(huì)進(jìn)入 Segmented LRU 中的 ProbationDeque,在后續(xù)被訪問到時(shí),它會(huì)被提升到 ProtectedDeque。當(dāng) ProtectedDeque 滿時(shí),會(huì)有數(shù)據(jù)降級(jí)到 ProbationDeque 。數(shù)據(jù)需要淘汰的時(shí)候,對(duì) ProbationDeque 中的數(shù)據(jù)進(jìn)行淘汰。具體淘汰機(jī)制:取ProbationDeque 中的隊(duì)首和隊(duì)尾進(jìn)行 PK,隊(duì)首數(shù)據(jù)是最先進(jìn)入隊(duì)列的,稱為受害者,隊(duì)尾的數(shù)據(jù)稱為攻擊者,比較兩者 頻率大小,大勝小汰。

總的來說,通過 reset 衰減,避免歷史熱點(diǎn)數(shù)據(jù)由于頻率值比較高一直淘汰不掉,并且通過對(duì)訪問隊(duì)列分成三段,這樣避免了新加入的熱點(diǎn)數(shù)據(jù)早早地被淘汰掉。
2.2 高性能讀寫
Caffeine 認(rèn)為讀操作是頻繁的,寫操作是偶爾的,讀寫都是異步線程更新頻率信息。
2.2.1 讀緩沖
傳統(tǒng)的緩存實(shí)現(xiàn)將會(huì)為每個(gè)操作加鎖,以便能夠安全的對(duì)每個(gè)訪問隊(duì)列的元素進(jìn)行排序。一種優(yōu)化方案是將每個(gè)操作按序加入到緩沖區(qū)中進(jìn)行批處理操作。讀完把數(shù)據(jù)放到環(huán)形隊(duì)列 RingBuffer 中,為了減少讀并發(fā),采用多個(gè) RingBuffer,每個(gè)線程都有對(duì)應(yīng)的 RingBuffer。環(huán)形隊(duì)列是一個(gè)定長(zhǎng)數(shù)組,提供高性能的能力并最大程度上減少了 GC所帶來的性能開銷。數(shù)據(jù)丟到隊(duì)列之后就返回讀取結(jié)果,類似于數(shù)據(jù)庫的WAL機(jī)制,和ConcurrentHashMap 讀取數(shù)據(jù)相比,僅僅多了把數(shù)據(jù)放到隊(duì)列這一步。異步線程并發(fā)讀取 RingBuffer 數(shù)組,更新訪問信息,這邊的線程池使用的是下文實(shí)戰(zhàn)小節(jié)講的 Caffeine 配置參數(shù)中的 executor。

與讀緩沖類似,寫緩沖是為了儲(chǔ)存寫事件。讀緩沖中的事件主要是為了優(yōu)化驅(qū)逐策略的命中率,因此讀緩沖中的事件完整程度允許一定程度的有損。但是寫緩沖并不允許數(shù)據(jù)的丟失,因此其必須實(shí)現(xiàn)為一個(gè)安全的隊(duì)列。Caffeine 寫是把數(shù)據(jù)放入MpscGrowableArrayQueue 阻塞隊(duì)列中,它參考了JCTools里的MpscGrowableArrayQueue ,是針對(duì) MPSC- 多生產(chǎn)者單消費(fèi)者(Multi-Producer