最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

詳解高性能緩存Caffeine原理及實(shí)戰(zhàn)

 更新時(shí)間:2021年06月17日 14:16:26   作者:vivo互聯(lián)網(wǎng)技術(shù)  
Caffeine是基于Java 8開(kāi)發(fā)的,提供了近乎最佳命中率的高性能本地緩存組件,Spring5開(kāi)始不再支持Guava Cache,改為使用Caffeine。Caffeine提供的內(nèi)存緩存使用參考Google guava的API

一、簡(jiǎn)介

下面是Caffeine 官方測(cè)試報(bào)告。

由上面三幅圖可見(jiàn):不管在并發(fā)讀、并發(fā)寫還是并發(fā)讀寫的場(chǎng)景下,Caffeine 的性能都大幅領(lǐng)先于其他本地開(kāi)源緩存組件。

本文先介紹 Caffeine 實(shí)現(xiàn)原理,再講解如何在項(xiàng)目中使用 Caffeine 。

二、Caffeine 原理

2.1、淘汰算法

2.1.1、常見(jiàn)算法

對(duì)于 Java 進(jìn)程內(nèi)緩存我們可以通過(guò) HashMap 來(lái)實(shí)現(xiàn)。不過(guò),Java 進(jìn)程內(nèi)存是有限的,不可能無(wú)限地往里面放緩存對(duì)象。這就需要有合適的算法輔助我們淘汰掉使用價(jià)值相對(duì)不高的對(duì)象,為新進(jìn)的對(duì)象留有空間。常見(jiàn)的緩存淘汰算法有 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)先淘汰掉最久未訪問(wèn)到的數(shù)據(jù)。缺點(diǎn)是不能很好地應(yīng)對(duì)偶然的突發(fā)流量。比如一個(gè)數(shù)據(jù)在一分鐘內(nèi)的前59秒訪問(wèn)很多次,而在最后1秒沒(méi)有訪問(wèn),但是有一批冷門數(shù)據(jù)在最后一秒進(jìn)入緩存,那么熱點(diǎn)數(shù)據(jù)就會(huì)被沖刷掉。

LFU(Least Frequently Used):

最近最少頻率使用。它是優(yōu)先淘汰掉最不經(jīng)常使用的數(shù)據(jù),需要維護(hù)一個(gè)表示使用頻率的字段。

主要有兩個(gè)缺點(diǎn):

一、如果訪問(wèn)頻率比較高的話,頻率字段會(huì)占據(jù)一定的空間;

二、無(wú)法合理更新新上的熱點(diǎn)數(shù)據(jù),比如某個(gè)歌手的老歌播放歷史較多,新出的歌如果和老歌一起排序的話,就永無(wú)出頭之日。

2.1.2、W-TinyLFU 算法

Caffeine 使用了 W-TinyLFU 算法,解決了 LRU 和LFU上述的缺點(diǎn)。W-TinyLFU 算法由論文《TinyLFU: A Highly Efficient Cache Admission Policy》提出。

它主要干了兩件事:

一、采用 Count–Min Sketch 算法降低頻率信息帶來(lái)的內(nèi)存消耗;

二、維護(hù)一個(gè)PK機(jī)制保障新上的熱點(diǎn)數(shù)據(jù)能夠緩存。

如下圖所示,Count–Min Sketch 算法類似布隆過(guò)濾器 (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次為最高訪問(wèn)頻率,每個(gè)key實(shí)際 hash 了四次,落在不同 long 型數(shù)字的16份中某個(gè)位置)。讀取某個(gè)key的訪問(wèn)次數(shù)時(shí),會(huì)比較所有位置上的頻率值,取最小值返回。對(duì)于所有key的訪問(wèn)頻率之和有個(gè)最大值,當(dāng)達(dá)到最大值時(shí),會(huì)進(jìn)行reset即對(duì)各個(gè)緩存key的頻率除以2。

如下圖緩存訪問(wèn)頻率存儲(chǔ)主要分為兩大部分,即 LRU 和 Segmented LRU 。新訪問(wèn)的數(shù)據(jù)會(huì)進(jìn)入第一個(gè) LRU,在 Caffeine 里叫 WindowDeque。當(dāng) WindowDeque 滿時(shí),會(huì)進(jìn)入 Segmented LRU 中的 ProbationDeque,在后續(xù)被訪問(wèn)到時(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ù)稱為攻擊者,比較兩者 頻率大小,大勝小汰。

總的來(lái)說(shuō),通過(guò) reset 衰減,避免歷史熱點(diǎn)數(shù)據(jù)由于頻率值比較高一直淘汰不掉,并且通過(guò)對(duì)訪問(wèn)隊(duì)列分成三段,這樣避免了新加入的熱點(diǎn)數(shù)據(jù)早早地被淘汰掉。

2.2、高性能讀寫

Caffeine 認(rèn)為讀操作是頻繁的,寫操作是偶爾的,讀寫都是異步線程更新頻率信息。

2.2.1、讀緩沖

傳統(tǒng)的緩存實(shí)現(xiàn)將會(huì)為每個(gè)操作加鎖,以便能夠安全的對(duì)每個(gè)訪問(wèn)隊(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所帶來(lái)的性能開(kāi)銷。數(shù)據(jù)丟到隊(duì)列之后就返回讀取結(jié)果,類似于數(shù)據(jù)庫(kù)的WAL機(jī)制,和ConcurrentHashMap 讀取數(shù)據(jù)相比,僅僅多了把數(shù)據(jù)放到隊(duì)列這一步。異步線程并發(fā)讀取 RingBuffer 數(shù)組,更新訪問(wèn)信息,這邊的線程池使用的是下文實(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 & Single-Consumer)場(chǎng)景的高性能實(shí)現(xiàn)。多個(gè)生產(chǎn)者同時(shí)并發(fā)地寫入隊(duì)列是線程安全的,但是同一時(shí)刻只允許一個(gè)消費(fèi)者消費(fèi)隊(duì)列。

三、Caffeine 實(shí)戰(zhàn)

3.1、配置參數(shù)

Caffeine 借鑒了Guava Cache 的設(shè)計(jì)思想,如果之前使用過(guò) Guava Cache,那么Caffeine 很容易上手,只需要改變相應(yīng)的類名就行。構(gòu)造一個(gè)緩存 Cache 示例代碼如下:

Cache cache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(6, TimeUnit.MINUTES).softValues().build();

Caffeine 類相當(dāng)于建造者模式的 Builder 類,通過(guò) Caffeine 類配置 Cache,配置一個(gè)Cache 有如下參數(shù):

  • expireAfterWrite:寫入間隔多久淘汰;
  • expireAfterAccess:最后訪問(wèn)后間隔多久淘汰;
  • refreshAfterWrite:寫入后間隔多久刷新,該刷新是基于訪問(wèn)被動(dòng)觸發(fā)的,支持異步刷新和同步刷新,如果和 expireAfterWrite 組合使用,能夠保證即使該緩存訪問(wèn)不到、也能在固定時(shí)間間隔后被淘汰,否則如果單獨(dú)使用容易造成OOM;
  • expireAfter:自定義淘汰策略,該策略下 Caffeine 通過(guò)時(shí)間輪算法來(lái)實(shí)現(xiàn)不同key 的不同過(guò)期時(shí)間;
  • maximumSize:緩存 key 的最大個(gè)數(shù);weakKeys:key設(shè)置為弱引用,在 GC 時(shí)可以直接淘汰;
  • weakValues:value設(shè)置為弱引用,在 GC 時(shí)可以直接淘汰;
  • softValues:value設(shè)置為軟引用,在內(nèi)存溢出前可以直接淘汰;
  • executor:選擇自定義的線程池,默認(rèn)的線程池實(shí)現(xiàn)是 ForkJoinPool.commonPool();
  • maximumWeight:設(shè)置緩存最大權(quán)重;weigher:設(shè)置具體key權(quán)重;
  • recordStats:緩存的統(tǒng)計(jì)數(shù)據(jù),比如命中率等;
  • removalListener:緩存淘汰監(jiān)聽(tīng)器;writer:緩存寫入、更新、淘汰的監(jiān)聽(tīng)器。

3.2、項(xiàng)目實(shí)戰(zhàn)

Caffeine 支持解析字符串參數(shù),參照 Ehcache 的思想,可以把所有緩存項(xiàng)參數(shù)信息放入配置文件里面,比如有一個(gè) caffeine.properties 配置文件,里面配置參數(shù)如下:

users=maximumSize=10000,expireAfterWrite=180s,softValues
goods=maximumSize=10000,expireAfterWrite=180s,softValues

針對(duì)不同的緩存,解析配置文件,并加入 Cache 容器里面,代碼如下:

@Component
@Slf4j
public class CaffeineManager {
    private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);
  
    @PostConstruct
    public void afterPropertiesSet() {
        String filePath = CaffeineManager.class.getClassLoader().getResource("").getPath() + File.separator + "config"
            + File.separator + "caffeine.properties";
        Resource resource = new FileSystemResource(filePath);
        if (!resource.exists()) {
            return;
        }
        Properties props = new Properties();
        try (InputStream in = resource.getInputStream()) {
            props.load(in);
            Enumeration propNames = props.propertyNames();
            while (propNames.hasMoreElements()) {
                String caffeineKey = (String) propNames.nextElement();
                String caffeineSpec = props.getProperty(caffeineKey);
                CaffeineSpec spec = CaffeineSpec.parse(caffeineSpec);
                Caffeine caffeine = Caffeine.from(spec);
                Cache manualCache = caffeine.build();
                cacheMap.put(caffeineKey, manualCache);
            }
        }
        catch (IOException e) {
            log.error("Initialize Caffeine failed.", e);
        }
    }
}

當(dāng)然也可以把 caffeine.properties 里面的配置項(xiàng)放入配置中心,如果需要?jiǎng)討B(tài)生效,可以通過(guò)如下方式:

至于是否利用 Spring 的 EL 表達(dá)式通過(guò)注解的方式使用,仁者見(jiàn)仁智者見(jiàn)智,筆者主要考慮幾點(diǎn):

一、EL 表達(dá)式上手需要學(xué)習(xí)成本;

二、引入注解需要注意動(dòng)態(tài)代理失效場(chǎng)景;

獲取緩存時(shí)通過(guò)如下方式:

caffeineManager.getCache(cacheName).get(redisKey, value -> getTFromRedis(redisKey, targetClass, supplier));

Caffeine 這種帶有回源函數(shù)的 get 方法最終都是調(diào)用 ConcurrentHashMap 的 compute 方法,它能確保高并發(fā)場(chǎng)景下,如果對(duì)一個(gè)熱點(diǎn) key 進(jìn)行回源時(shí),單個(gè)進(jìn)程內(nèi)只有一個(gè)線程回源,其他都在阻塞。業(yè)務(wù)需要確保回源的方法耗時(shí)比較短,防止線程阻塞時(shí)間比較久,系統(tǒng)可用性下降。

筆者實(shí)際開(kāi)發(fā)中用了 Caffeine 和 Redis 兩級(jí)緩存。Caffeine 的 cache 緩存 key 和 Redis 里面一致,都是命名為 redisKey。targetClass 是返回對(duì)象類型,從 Redis 中獲取字符串反序列化成實(shí)際對(duì)象時(shí)使用。supplier 是函數(shù)式接口,是緩存回源到數(shù)據(jù)庫(kù)的業(yè)務(wù)邏輯。

getTFromRedis 方法實(shí)現(xiàn)如下:

private <T> T getTFromRedis(String redisKey, Class targetClass, Supplier supplier) {
    String data;
    T value;
    String redisValue = UUID.randomUUID().toString();
    if (tryGetDistributedLockWithRetry(redisKey + RedisKey.DISTRIBUTED_SUFFIX, redisValue, 30)) {
        try {
            data = getFromRedis(redisKey);
            if (StringUtils.isEmpty(data)) {
                value = (T) supplier.get();
                setToRedis(redisKey, JackSonParser.bean2Json(value));
            }
            else {
                value = json2Bean(targetClass, data);
            }
        }
        finally {
            releaseDistributedLock(redisKey + RedisKey.DISTRIBUTED_SUFFIX, redisValue);
        }
    }
    else {
        value = json2Bean(targetClass, getFromRedis(redisKey));
    }
    return value;
}

由于回源都是從 MySQL 查詢,雖然 Caffeine 本身解決了進(jìn)程內(nèi)同一個(gè) key 只有一個(gè)線程回源,需要注意多個(gè)業(yè)務(wù)節(jié)點(diǎn)的分布式情況下,如果 Redis 沒(méi)有緩存值,并發(fā)回源時(shí)會(huì)穿透到 MySQL ,所以回源時(shí)加了分布式鎖,保證只有一個(gè)節(jié)點(diǎn)回源。

注意一點(diǎn):從本地緩存獲取對(duì)象時(shí),如果業(yè)務(wù)要對(duì)緩存對(duì)象做更新,需要深拷貝一份對(duì)象,不然并發(fā)場(chǎng)景下多個(gè)線程取值會(huì)相互影響。

筆者項(xiàng)目之前都是使用 Ehcache 作為本地緩存,切換成 Caffeine 后,涉及本地緩存的接口,同樣 TPS 值時(shí),CPU 使用率能降低 10% 左右,接口性能都有一定程度提升,最多的提升了 25%。上線后觀察調(diào)用鏈,平均響應(yīng)時(shí)間降低24%左右。

四、總結(jié)

Caffeine 是目前比較優(yōu)秀的本地緩存解決方案,通過(guò)使用 W-TinyLFU 算法,實(shí)現(xiàn)了緩存高命中率、內(nèi)存低消耗。如果之前使用過(guò) Guava Cache,看下接口名基本就能上手。如果之前使用的是 Ehcache,筆者分享的使用方式可以作為參考。

以上就是詳解高性能緩存Caffeine原理及實(shí)戰(zhàn)的詳細(xì)內(nèi)容,更多關(guān)于Caffeine 原理的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • Java使用Socket通信傳輸文件的方法示例

    Java使用Socket通信傳輸文件的方法示例

    這篇文章主要介紹了Java使用Socket通信傳輸文件的方法,結(jié)合實(shí)例形式分析了java socket編程實(shí)現(xiàn)文件傳輸操作的相關(guān)技巧,需要的朋友可以參考下
    2017-06-06
  • Java行為型模式中命令模式分析

    Java行為型模式中命令模式分析

    在軟件設(shè)計(jì)中,我們經(jīng)常需要向某些對(duì)象發(fā)送請(qǐng)求,但是并不知道請(qǐng)求的接收者是誰(shuí),也不知道被請(qǐng)求的操作是哪個(gè),我們只需在程序運(yùn)行時(shí)指定具體的請(qǐng)求接收者即可,此時(shí)可以使用命令模式來(lái)進(jìn)行設(shè)計(jì)
    2023-02-02
  • Java RandomAccessFile的用法詳解

    Java RandomAccessFile的用法詳解

    下面小編就為大家?guī)?lái)一篇Java RandomAccessFile的用法詳解。小編覺(jué)得挺不錯(cuò)的,現(xiàn)在就分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧
    2016-06-06
  • 快速上手Mybatis-plus結(jié)構(gòu)構(gòu)建過(guò)程

    快速上手Mybatis-plus結(jié)構(gòu)構(gòu)建過(guò)程

    這篇文章主要介紹了快速上手Mybatis-plus結(jié)構(gòu)構(gòu)建過(guò)程,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-07-07
  • SpringBoot上傳下載文件+oss實(shí)例

    SpringBoot上傳下載文件+oss實(shí)例

    這篇文章主要介紹了SpringBoot上傳下載文件+oss實(shí)例,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-04-04
  • 基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例

    基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例

    這篇文章主要為大家詳細(xì)介紹了基于JAVA的短信驗(yàn)證碼api調(diào)用代碼實(shí)例,感興趣的小伙伴們可以參考一下
    2016-05-05
  • Springboot2整合knife4j過(guò)程解析

    Springboot2整合knife4j過(guò)程解析

    這篇文章主要介紹了Springboot2整合knife4j過(guò)程解析,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-03-03
  • Java利用隨機(jī)分錢模擬財(cái)富變化

    Java利用隨機(jī)分錢模擬財(cái)富變化

    這篇文章主要為大家詳細(xì)介紹了Java如何利用隨機(jī)分錢思想模擬財(cái)富的變化,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下
    2022-12-12
  • java截取圖片示例

    java截取圖片示例

    這篇文章主要介紹了java截取圖片示例,把代碼中的圖片路徑改成自己的圖片,運(yùn)行就可以看到效果了,需要的朋友可以參考下
    2014-03-03
  • SpringBoot實(shí)現(xiàn)單點(diǎn)登錄(SSO)的四種方案

    SpringBoot實(shí)現(xiàn)單點(diǎn)登錄(SSO)的四種方案

    單點(diǎn)登錄(Single?Sign-On,SSO)是企業(yè)應(yīng)用系統(tǒng)中常見(jiàn)的用戶認(rèn)證方案,它允許用戶使用一組憑證訪問(wèn)多個(gè)相關(guān)但獨(dú)立的系統(tǒng),無(wú)需重復(fù)登錄,本文給大家介紹了SpringBoot實(shí)現(xiàn)單點(diǎn)登錄(SSO)的四種方案,需要的朋友可以參考下
    2025-04-04

最新評(píng)論

阿拉尔市| 福州市| 阳朔县| 瑞安市| 富源县| 喀喇沁旗| 镇原县| 天全县| 新乐市| 乾安县| 忻州市| 扶余县| 盖州市| 义马市| 从化市| 揭阳市| 谷城县| 湘阴县| 修水县| 象山县| 赣榆县| 邮箱| 崇文区| 怀柔区| 织金县| 华容县| 北宁市| 宜兰市| 太白县| 稻城县| 达州市| 乳源| 吉安市| 固阳县| 宁阳县| 永德县| 兴安县| 吉林省| 彭泽县| 长沙市| 麻江县|