Guava cache使用總結(jié)

緩存分為本地緩存和遠端緩存。常見的遠端緩存有Redis阎肝,MongoDB甩骏;本地緩存一般使用map的方式保存在本地內(nèi)存中窗市。一般我們在業(yè)務(wù)中操作緩存,都會操作緩存和數(shù)據(jù)源兩部分饮笛。如:put數(shù)據(jù)時咨察,先插入DB,再刪除原來的緩存福青;ge數(shù)據(jù)時摄狱,先查緩存,命中則返回无午,沒有命中時媒役,需要查詢DB,再把查詢結(jié)果放入緩存中 宪迟。如果訪問量大酣衷,我們還得兼顧本地緩存的線程安全問題。必要的時候也要考慮緩存的回收策略次泽。

  • 很好的封裝了get穿仪、put操作,能夠集成數(shù)據(jù)源 箕憾;
  • 線程安全的緩存牡借,與ConcurrentMap相似拳昌,但前者增加了更多的元素失效策略袭异,后者只能顯示的移除元素;
  • Guava Cache提供了三種基本的緩存回收方式:基于容量回收炬藤、定時回收和基于引用回收御铃。定時回收有兩種:按照寫入時間,最早寫入的最先回收沈矿;按照訪問時間上真,最早訪問的最早回收;
  • 監(jiān)控緩存加載/命中情況
    主要實現(xiàn)的緩存功能有:自動將節(jié)點加載至緩存結(jié)構(gòu)中羹膳,當(dāng)緩存的數(shù)據(jù)超過最大值時睡互,使用LRU算法替換;它具備根據(jù)節(jié)點上一次被訪問或?qū)懭霑r間計算緩存過期機制,緩存的key被封裝在WeakReference引用中就珠,緩存的value被封裝在WeakReference或SoftReference引用中寇壳;還可以統(tǒng)計緩存使用過程中的命中率、異常率和命中率等統(tǒng)計數(shù)據(jù)妻怎。
package com.rickiyang.learn.cache;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;


public class GuavaCacheService {

    public void setCache() {
        LoadingCache<Integer, String> cache = CacheBuilder.newBuilder()
                //設(shè)置并發(fā)級別為8壳炎,并發(fā)級別是指可以同時寫緩存的線程數(shù)
                .concurrencyLevel(8)
                //設(shè)置緩存容器的初始容量為10
                .initialCapacity(10)
                //設(shè)置緩存最大容量為100,超過100之后就會按照LRU最近雖少使用算法來移除緩存項
                .maximumSize(100)
                //是否需要統(tǒng)計緩存情況,該操作消耗一定的性能,生產(chǎn)環(huán)境應(yīng)該去除
                .recordStats()
                //設(shè)置寫緩存后n秒鐘過期
                .expireAfterWrite(60, TimeUnit.SECONDS)
                //設(shè)置讀寫緩存后n秒鐘過期,實際很少用到,類似于expireAfterWrite
                //.expireAfterAccess(17, TimeUnit.SECONDS)
                //只阻塞當(dāng)前數(shù)據(jù)加載線程逼侦,其他線程返回舊值
                //.refreshAfterWrite(13, TimeUnit.SECONDS)
                //設(shè)置緩存的移除通知
                .removalListener(notification -> {
                    System.out.println(notification.getKey() + " " + notification.getValue() + " 被移除,原因:" + notification.getCause());
                })
                //build方法中可以指定CacheLoader匿辩,在緩存不存在時通過CacheLoader的實現(xiàn)自動加載緩存
                .build(new DemoCacheLoader());

        //模擬線程并發(fā)
        new Thread(() -> {
            //非線程安全的時間格式化工具
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
            try {
                for (int i = 0; i < 10; i++) {
                    String value = cache.get(1);
                    System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
                    TimeUnit.SECONDS.sleep(3);
                }
            } catch (Exception ignored) {
            }
        }).start();

        new Thread(() -> {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("HH:mm:ss");
            try {
                for (int i = 0; i < 10; i++) {
                    String value = cache.get(1);
                    System.out.println(Thread.currentThread().getName() + " " + simpleDateFormat.format(new Date()) + " " + value);
                    TimeUnit.SECONDS.sleep(5);
                }
            } catch (Exception ignored) {
            }
        }).start();
        //緩存狀態(tài)查看
        System.out.println(cache.stats().toString());

    }

    /**
     * 隨機緩存加載,實際使用時應(yīng)實現(xiàn)業(yè)務(wù)的緩存加載邏輯,例如從數(shù)據(jù)庫獲取數(shù)據(jù)
     */
    public static class DemoCacheLoader extends CacheLoader<Integer, String> {
        @Override
        public String load(Integer key) throws Exception {
            System.out.println(Thread.currentThread().getName() + " 加載數(shù)據(jù)開始");
            TimeUnit.SECONDS.sleep(8);
            Random random = new Random();
            System.out.println(Thread.currentThread().getName() + " 加載數(shù)據(jù)結(jié)束");
            return "value:" + random.nextInt(10000);
        }
    }
}

LoadingCache是Cache的子接口,相比較于Cache榛丢,當(dāng)從LoadingCache中讀取一個指定key的記錄時铲球,如果該記錄不存在,則LoadingCache可以自動執(zhí)行加載數(shù)據(jù)到緩存的操作晰赞。
在調(diào)用CacheBuilder的build方法時睬辐,必須傳遞一個CacheLoader類型的參數(shù),CacheLoader的load方法需要我們提供實現(xiàn)宾肺。當(dāng)調(diào)用LoadingCache的get方法時溯饵,如果緩存不存在對應(yīng)key的記錄,則CacheLoader中的load方法會被自動調(diào)用從外存加載數(shù)據(jù)锨用,load方法的返回值會作為key對應(yīng)的value存儲到LoadingCache中丰刊,并從get方法返回。
當(dāng)然如果你不想指定重建策略增拥,那么你可以使用無參的build()方法啄巧,它將返回Cache類型的構(gòu)建對象。
CacheBuilder 是Guava 提供的一個快速構(gòu)建緩存對象的工具類掌栅。CacheBuilder類采用builder設(shè)計模式秩仆,它的每個方法都返回CacheBuilder本身,直到build方法被調(diào)用猾封。 該類中提供了很多的參數(shù)設(shè)置選項澄耍,你可以設(shè)置cache的默認大小,并發(fā)數(shù)晌缘,存活時間齐莲,過期策略等等。

緩存的并發(fā)級別

Guava提供了設(shè)置并發(fā)級別的api磷箕,使得緩存支持并發(fā)的寫入和讀取选酗。同 ConcurrentHashMap 類似Guava cache的并發(fā)也是通過分離鎖實現(xiàn)。在一般情況下岳枷,將并發(fā)級別設(shè)置為服務(wù)器cpu核心數(shù)是一個比較不錯的選擇芒填。

CacheBuilder.newBuilder()
        // 設(shè)置并發(fā)級別為cpu核心數(shù)
        .concurrencyLevel(Runtime.getRuntime().availableProcessors()) 
        .build();

緩存的初始容量設(shè)置

我們在構(gòu)建緩存時可以為緩存設(shè)置一個合理大小初始容量呜叫,由于Guava的緩存使用了分離鎖的機制,擴容的代價非常昂貴殿衰。所以合理的初始容量能夠減少緩存容器的擴容次數(shù)怀偷。

CacheBuilder.newBuilder()
        // 設(shè)置初始容量為100
        .initialCapacity(100)
        .build();

設(shè)置最大存儲

Guava Cache可以在構(gòu)建緩存對象時指定緩存所能夠存儲的最大記錄數(shù)量。當(dāng)Cache中的記錄數(shù)量達到最大值后再調(diào)用put方法向其中添加對象播玖,Guava會先從當(dāng)前緩存的對象記錄中選擇一條刪除掉椎工,騰出空間后再將新的對象存儲到Cache中。

  1. 基于容量的清除(size-based eviction): 通過CacheBuilder.maximumSize(long)方法可以設(shè)置Cache的最大容量數(shù)蜀踏,當(dāng)緩存數(shù)量達到或接近該最大值時维蒙,Cache將清除掉那些最近最少使用的緩存;
  2. **基于權(quán)重的清除: ** 使用CacheBuilder.weigher(Weigher)指定一個權(quán)重函數(shù),并且用CacheBuilder.maximumWeight(long)指定最大總重果覆。比如每一項緩存所占據(jù)的內(nèi)存空間大小都不一樣颅痊,可以看作它們有不同的“權(quán)重”(weights)。
緩存清除策略
1. 基于存活時間的清除
  • expireAfterWrite 寫緩存后多久過期
  • expireAfterAccess 讀寫緩存后多久過期
  • refreshAfterWrite 寫入數(shù)據(jù)后多久過期,只阻塞當(dāng)前數(shù)據(jù)加載線程,其他線程返回舊值

這幾個策略時間可以單獨設(shè)置,也可以組合配置局待。

2. 上面提到的基于容量的清除
3. 顯式清除

任何時候斑响,你都可以顯式地清除緩存項,而不是等到它被回收钳榨,Cache接口提供了如下API:

  1. 個別清除:Cache.invalidate(key)

  2. 批量清除:Cache.invalidateAll(keys)

  3. 清除所有緩存項:Cache.invalidateAll()

4. 基于引用的清除(Reference-based Eviction)

在構(gòu)建Cache實例過程中舰罚,通過設(shè)置使用弱引用的鍵、或弱引用的值薛耻、或軟引用的值营罢,從而使JVM在GC時順帶實現(xiàn)緩存的清除,不過一般不輕易使用這個特性饼齿。

  • CacheBuilder.weakKeys():使用弱引用存儲鍵饲漾。當(dāng)鍵沒有其它(強或軟)引用時,緩存項可以被垃圾回收缕溉。因為垃圾回收僅依賴恒等式考传,使用弱引用鍵的緩存用而不是equals比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲值证鸥。當(dāng)值沒有其它(強或軟)引用時僚楞,緩存項可以被垃圾回收。因為垃圾回收僅依賴恒等式敌土,使用弱引用值的緩存用而不是equals比較值镜硕。
  • CacheBuilder.softValues():使用軟引用存儲值运翼。軟引用只有在響應(yīng)內(nèi)存需要時返干,才按照全局最近最少使用的順序回收⊙剩考慮到使用軟引用的性能影響矩欠,我們通常建議使用更有性能預(yù)測性的緩存大小限定(見上文财剖,基于容量回收)。使用軟引用值的緩存同樣用==而不是equals比較值癌淮。
清理什么時候發(fā)生

也許這個問題有點奇怪躺坟,如果設(shè)置的存活時間為一分鐘,難道不是一分鐘后這個key就會立即清除掉嗎乳蓄?我們來分析一下如果要實現(xiàn)這個功能咪橙,那Cache中就必須存在線程來進行周期性地檢查、清除等工作虚倒,很多cache如redis美侦、ehcache都是這樣實現(xiàn)的。

使用CacheBuilder構(gòu)建的緩存不會”自動”執(zhí)行清理和回收工作魂奥,也不會在某個緩存項過期后馬上清理菠剩,也沒有諸如此類的清理機制。相反耻煤,它會在寫操作時順帶做少量的維護工作具壮,或者偶爾在讀操作時做——如果寫操作實在太少的話。

這樣做的原因在于:如果要自動地持續(xù)清理緩存哈蝇,就必須有一個線程棺妓,這個線程會和用戶操作競爭共享鎖。此外炮赦,某些環(huán)境下線程創(chuàng)建可能受限制涧郊,這樣CacheBuilder就不可用了。

給移除操作添加一個監(jiān)聽器:

可以為Cache對象添加一個移除監(jiān)聽器眼五,這樣當(dāng)有記錄被刪除時可以感知到這個事件妆艘。

RemovalListener<String, String> listener = notification -> System.out.println("[" + notification.getKey() + ":" + notification.getValue() + "] is removed!");
        Cache<String,String> cache = CacheBuilder.newBuilder()
                .maximumSize(5)
                .removalListener(listener)
                .build();

但是要注意的是:

默認情況下,監(jiān)聽器方法是在移除緩存時同步調(diào)用的看幼。因為緩存的維護和請求響應(yīng)通常是同時進行的批旺,代價高昂的監(jiān)聽器方法在同步模式下會拖慢正常的緩存請求。在這種情況下诵姜,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽器裝飾為異步操作汽煮。

自動加載

面我們說過使用get方法的時候如果key不存在你可以使用指定方法去加載這個key。在Cache構(gòu)建的時候通過指定CacheLoder的方式棚唆。如果你沒有指定暇赤,你也可以在get的時候顯式的調(diào)用call方法來設(shè)置key不存在的補救策略。

Cache的get方法有兩個參數(shù)宵凌,第一個參數(shù)是要從Cache中獲取記錄的key鞋囊,第二個記錄是一個Callable對象。

當(dāng)緩存中已經(jīng)存在key對應(yīng)的記錄時瞎惫,get方法直接返回key對應(yīng)的記錄溜腐。如果緩存中不包含key對應(yīng)的記錄译株,Guava會啟動一個線程執(zhí)行Callable對象中的call方法,call方法的返回值會作為key對應(yīng)的值被存儲到緩存中挺益,并且被get方法返回歉糜。

package com.rickiyang.learn.cache;

import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;

/**
 * @author: rickiyang
 * @date: 2019/6/12
 * @description:
 */
public class GuavaCacheService {


    private static Cache<String, String> cache = CacheBuilder.newBuilder()
            .maximumSize(3)
            .build();

    public static void main(String[] args) {

        new Thread(() -> {
            System.out.println("thread1");
            try {
                String value = cache.get("key", new Callable<String>() {
                    public String call() throws Exception {
                        System.out.println("thread1"); //加載數(shù)據(jù)線程執(zhí)行標志
                        Thread.sleep(1000); //模擬加載時間
                        return "thread1";
                    }
                });
                System.out.println("thread1 " + value);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(() -> {
            System.out.println("thread2");
            try {
                String value = cache.get("key", new Callable<String>() {
                    public String call() throws Exception {
                        System.out.println("thread2"); //加載數(shù)據(jù)線程執(zhí)行標志
                        Thread.sleep(1000); //模擬加載時間
                        return "thread2";
                    }
                });
                System.out.println("thread2 " + value);
            } catch (ExecutionException e) {
                e.printStackTrace();
            }
        }).start();
    }

}

輸出結(jié)果為:
thread1
thread2
thread2
thread1 thread2
thread2 thread2

可以看到輸出結(jié)果:兩個線程都啟動,輸出thread1望众,thread2匪补,接著又輸出了thread2,說明進入了thread2的call方法了烂翰,此時thread1正在阻塞叉袍,等待key被設(shè)置。然后thread1 得到了value是thread2刽酱,thread2的結(jié)果自然也是thread2喳逛。

這段代碼中有兩個線程共享同一個Cache對象,兩個線程同時調(diào)用get方法獲取同一個key對應(yīng)的記錄棵里。由于key對應(yīng)的記錄不存在润文,所以兩個線程都在get方法處阻塞。此處在call方法中調(diào)用Thread.sleep(1000)模擬程序從外存加載數(shù)據(jù)的時間消耗殿怜。

從結(jié)果中可以看出典蝌,雖然是兩個線程同時調(diào)用get方法,但只有一個get方法中的Callable會被執(zhí)行(沒有打印出load2)头谜。Guava可以保證當(dāng)有多個線程同時訪問Cache中的一個key時骏掀,如果key對應(yīng)的記錄不存在,Guava只會啟動一個線程執(zhí)行g(shù)et方法中Callable參數(shù)對應(yīng)的任務(wù)加載數(shù)據(jù)存到緩存柱告。當(dāng)加載完數(shù)據(jù)后截驮,任何線程中的get方法都會獲取到key對應(yīng)的值。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末际度,一起剝皮案震驚了整個濱河市葵袭,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌乖菱,老刑警劉巖坡锡,帶你破解...
    沈念sama閱讀 222,807評論 6 518
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異窒所,居然都是意外死亡鹉勒,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,284評論 3 399
  • 文/潘曉璐 我一進店門吵取,熙熙樓的掌柜王于貴愁眉苦臉地迎上來禽额,“玉大人,你說我怎么就攤上這事海渊∶嗥#” “怎么了哲鸳?”我有些...
    開封第一講書人閱讀 169,589評論 0 363
  • 文/不壞的土叔 我叫張陵臣疑,是天一觀的道長盔憨。 經(jīng)常有香客問我,道長讯沈,這世上最難降的妖魔是什么郁岩? 我笑而不...
    開封第一講書人閱讀 60,188評論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮缺狠,結(jié)果婚禮上问慎,老公的妹妹穿的比我還像新娘。我一直安慰自己挤茄,他們只是感情好如叼,可當(dāng)我...
    茶點故事閱讀 69,185評論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著穷劈,像睡著了一般笼恰。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上歇终,一...
    開封第一講書人閱讀 52,785評論 1 314
  • 那天社证,我揣著相機與錄音,去河邊找鬼评凝。 笑死追葡,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的奕短。 我是一名探鬼主播宜肉,決...
    沈念sama閱讀 41,220評論 3 423
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼翎碑!你這毒婦竟也來了崖飘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,167評論 0 277
  • 序言:老撾萬榮一對情侶失蹤杈女,失蹤者是張志新(化名)和其女友劉穎朱浴,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體达椰,經(jīng)...
    沈念sama閱讀 46,698評論 1 320
  • 正文 獨居荒郊野嶺守林人離奇死亡翰蠢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,767評論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了啰劲。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片梁沧。...
    茶點故事閱讀 40,912評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖蝇裤,靈堂內(nèi)的尸體忽然破棺而出廷支,到底是詐尸還是另有隱情频鉴,我是刑警寧澤,帶...
    沈念sama閱讀 36,572評論 5 351
  • 正文 年R本政府宣布恋拍,位于F島的核電站垛孔,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏施敢。R本人自食惡果不足惜周荐,卻給世界環(huán)境...
    茶點故事閱讀 42,254評論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望僵娃。 院中可真熱鬧概作,春花似錦、人聲如沸默怨。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,746評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽匙睹。三九已至愚屁,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間垃僚,已是汗流浹背集绰。 一陣腳步聲響...
    開封第一講書人閱讀 33,859評論 1 274
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留谆棺,地道東北人栽燕。 一個月前我還...
    沈念sama閱讀 49,359評論 3 379
  • 正文 我出身青樓,卻偏偏與公主長得像改淑,于是被迫代替她去往敵國和親碍岔。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,922評論 2 361

推薦閱讀更多精彩內(nèi)容

  • 概述 緩存是日常開發(fā)中經(jīng)常應(yīng)用到的一種技術(shù)手段朵夏,合理的利用緩存可以極大的改善應(yīng)用程序的性能蔼啦。Guava官方對Cac...
    小陳阿飛閱讀 1,790評論 0 0
  • 使用場景 緩存在很多場景下都是相當(dāng)有用的。例如仰猖,計算或檢索一個值的代價很高捏肢,并且對同樣的輸入需要不止一次獲取值的時...
    jiangmo閱讀 801評論 0 3
  • 緩存在日常開發(fā)中舉足輕重,如果你的應(yīng)用對某類數(shù)據(jù)有著較高的讀取頻次饥侵,并且改動較小時那就非常適合利用緩存來提高性能鸵赫。...
    tracy_668閱讀 4,317評論 0 9
  • 原文鏈接:原文鏈接 注:這篇文章是我自己根據(jù)官方文檔的原文翻譯的,因為能力有限躏升,有些地方翻譯的不好辩棒,歡迎批評指正,...
    大風(fēng)過崗閱讀 27,093評論 0 16
  • 久違的晴天,家長會一睁。 家長大會開好到教室時钻弄,離放學(xué)已經(jīng)沒多少時間了。班主任說已經(jīng)安排了三個家長分享經(jīng)驗者吁。 放學(xué)鈴聲...
    飄雪兒5閱讀 7,528評論 16 22