日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當(dāng)前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

高并发之--Guava Cache

發(fā)布時(shí)間:2024/1/23 编程问答 43 豆豆
生活随笔 收集整理的這篇文章主要介紹了 高并发之--Guava Cache 小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,幫大家做個(gè)參考.

最近需要用到緩存來存放臨時(shí)數(shù)據(jù),又不想采用Redis,Java自帶的Map功能太少,發(fā)現(xiàn)google的Guava提供的Cache模塊功能很強(qiáng)大,于是選擇使用它。

本地緩存

本地緩存作用就是提高系統(tǒng)的運(yùn)行速度,是一種空間換時(shí)間的取舍。它實(shí)質(zhì)上是一個(gè)做key-value查詢的字典,但是相對于我們常用的HashMap它又有以下特點(diǎn):

1.并發(fā)性: 由于目前的應(yīng)用大都是多線程的,所以緩存需要支持并發(fā)的寫入。

2.過期策略:在某些場景中,我們可能會(huì)希望緩存的數(shù)據(jù)有一定"保質(zhì)期",過期策略可以固定時(shí)間,例如緩存寫入10分鐘后過期。也可以是相對時(shí)間,例如10分鐘內(nèi)未訪問則緩存過期(類似于servlet中的session)。在java中甚至可以使用軟引用,弱引用的過期策略。

3.淘汰策略:由于本地緩存是存放在內(nèi)存中,我們往往需要設(shè)置一個(gè)容量上限和淘汰策略來防止出現(xiàn)內(nèi)存溢出的情況。

緩存應(yīng)當(dāng)具備的屬性為:

1.能夠配置緩存的大小,保持可控的Memory。

2.適應(yīng)多種場景的數(shù)據(jù)expire策略。

3.在高并發(fā)的情況下、能夠正常緩存的更新以及返回。

Guava Cache適用于:

你愿意消耗一些內(nèi)存空間來提升速度。

你預(yù)料到某些鍵會(huì)被查詢一次以上。

緩存中存放的數(shù)據(jù)總量不會(huì)超出內(nèi)存容量

緩存的最大容量與淘汰策略

由于本地緩存是將計(jì)算結(jié)果緩存到內(nèi)存中,所以我們往往需要設(shè)置一個(gè)最大容量來防止出現(xiàn)內(nèi)存溢出的情況。這個(gè)容量可以是緩存對象的數(shù)量,也可以是一個(gè)具體的內(nèi)存大小。在Guva中僅支持設(shè)置緩存對象的數(shù)量。

當(dāng)緩存數(shù)量逼近或大于我們所設(shè)置的最大容量時(shí),為了將緩存數(shù)量控制在我們所設(shè)定的閾值內(nèi),就需要丟棄掉一些數(shù)據(jù)。由于緩存的最大容量恒定,為了提高緩存的命中率,我們需要盡量丟棄那些我們之后不再經(jīng)常訪問的數(shù)據(jù),保留那些即將被訪問的數(shù)據(jù)。為了達(dá)到以上目的,我們往往會(huì)制定一些緩存淘汰策略,常用的緩存淘汰策略有以下幾種:

1.FIFO:First In First Out,先進(jìn)先出。

一般采用隊(duì)列的方式實(shí)現(xiàn)。這種淘汰策略僅僅是保證了緩存數(shù)量不超過我們所設(shè)置的閾值,而完全沒有考慮緩存的命中率。所以在這種策略極少被使用。

2.LRU:Least Recently Used,最近最少使用;

該算法其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高”。

所以該算法是淘汰最后一次使用時(shí)間離當(dāng)前最久的緩存數(shù)據(jù),保留最近訪問的數(shù)據(jù)。所以該種算法非常適合緩存“熱點(diǎn)數(shù)據(jù)”。

但是該算法在緩存周期性數(shù)據(jù)時(shí),就會(huì)出現(xiàn)緩存污染,也就是淘汰了即將訪問的數(shù)據(jù),反而把不常用的數(shù)據(jù)讀取到緩存中。

為了解決這個(gè)問題,后續(xù)也出現(xiàn)了如LRU-K,Two queues,Multi Queue等進(jìn)階算法。

3.LFU:Least Frequently Used,最不經(jīng)常使用。

該算法的核心思想是“如果數(shù)據(jù)在以前被訪問的次數(shù)最多,那么將來被訪問的幾率就會(huì)更高”。所以該算法淘汰的是歷史訪問次數(shù)最少的數(shù)據(jù)。

一般情況下,LFU效率要優(yōu)于LRU,且能夠避免周期性或者偶發(fā)性的操作導(dǎo)致緩存命中率下降的問題。但LFU需要記錄數(shù)據(jù)的歷史訪問記錄,一旦數(shù)據(jù)訪問模式改變,LFU需要更長時(shí)間來適用新的訪問模式,即:LFU存在歷史數(shù)據(jù)影響將來數(shù)據(jù)的“緩存污染”效用。

后續(xù)出現(xiàn)LFU*,LFU-Aging,Window-LFU等改進(jìn)算法。

合理的使用淘汰算法能夠很明顯的提升緩存命中率,但是也不應(yīng)該一味的追求命中率,而是應(yīng)在命中率和資源消耗中找到一個(gè)平衡。

在guava中默認(rèn)使用LRU淘汰算法,而且在不修改源碼的情況下也不支持自定義淘汰算法。

使用Guava構(gòu)建緩存

// 通過CacheBuilder構(gòu)建一個(gè)緩存實(shí)例 Cache<String, String> cache = CacheBuilder.newBuilder().maximumSize(100) // 設(shè)置緩存的最大容量.expireAfterWrite(1, TimeUnit.MINUTES) // 設(shè)置緩存在寫入一分鐘后失效.concurrencyLevel(10) // 設(shè)置并發(fā)級別為10.recordStats() // 開啟緩存統(tǒng)計(jì).build();// 放入緩存 cache.put("key", "value"); // 獲取緩存 String value = cache.getIfPresent("key");

Cache和LoadingCache

使用CacheBuilder我們能構(gòu)建出兩種類型的cache,他們分別是Cache與LoadingCache。

Cache

Cache是通過CacheBuilder的build()方法構(gòu)建,它是Guava提供的最基本的緩存接口,并且它提供了一些常用的緩存api:

//放入/覆蓋一個(gè)緩存 cache.put("k1", "v1"); // 獲取一個(gè)緩存,如果該緩存不存在則返回一個(gè)null值 Object value = cache.getIfPresent("k1"); // 獲取緩存,當(dāng)緩存不存在時(shí),則通過Callable進(jìn)行加載并返回,該操作是原子 Object getValue = cache.get("k1", new Callable<Object>() {@Overridepublic Object call() throws Exception {return null;} });

java8也可以采用lambda表達(dá)式來代替匿名內(nèi)部類

Object getValue = cache.get("k1", () -> {return null; });

LoadingCache

LoadingCache繼承自Cache,在構(gòu)建LoadingCache時(shí),需要通過CacheBuilder的build(CacheLoader<? super K1, V1> loader)方法構(gòu)建:

  • CacheBuilder.newBuilder()

  • .build(new CacheLoader<String, String>() {

  • @Override

  • public String load(String key) throws Exception {

  • // 緩存加載邏輯

  • ...

  • }

  • });

  • LoadingCache,顧名思義,它能夠通過CacheLoader自發(fā)的加載緩存:

  • LoadingCache<Object, Object> loadingCache = CacheBuilder.newBuilder().build(new CacheLoader<Object, Object>() {

  • @Override

  • public Object load(Object key) throws Exception {

  • return null;

  • }

  • });

  • // 獲取緩存,當(dāng)緩存不存在時(shí),會(huì)通過CacheLoader自動(dòng)加載,該方法會(huì)拋出ExecutionException異常

  • loadingCache.get("k1");

  • // 以不安全的方式獲取緩存,當(dāng)緩存不存在時(shí),會(huì)通過CacheLoader自動(dòng)加載,該方法不會(huì)拋出異常

  • loadingCache.getUnchecked("k1");

  • 緩存的并發(fā)級別

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

  • CacheBuilder.newBuilder()

  • // 設(shè)置并發(fā)級別為cpu核心數(shù)

  • .concurrencyLevel(Runtime.getRuntime().availableProcessors())

  • .build();

  • 緩存的初始容量

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

  • CacheBuilder.newBuilder()

  • // 設(shè)置初始容量為100

  • .initialCapacity(100)

  • .build();

  • 緩存的回收

    在前文提到過,在構(gòu)建本地緩存時(shí),我們應(yīng)該指定一個(gè)最大容量來防止出現(xiàn)內(nèi)存溢出的情況。在guava中除了提供基于數(shù)量,和基于內(nèi)存容量兩種回收策略外,還提供了基于引用的回收。

    基于數(shù)量/容量的回收

    基于最大數(shù)量的回收策略非常簡單,我們只需指定緩存的最大數(shù)量maximumSize即可,maximumSize 設(shè)定了該緩存的最大存儲單位(key)個(gè)數(shù):

  • CacheBuilder.newBuilder()

  • .maximumSize(100) // 緩存數(shù)量上限為100

  • .build();

  • 使用基于最大容量的的回收策略時(shí),我們需要設(shè)置2個(gè)必要參數(shù):

    maximumWeigh;用于指定最大容量,maximumWeight 是根據(jù)設(shè)定緩存數(shù)據(jù)的最大值。

    Weigher;在加載緩存時(shí)用于計(jì)算緩存容量大小。

    這里我們例舉一個(gè)key和value都是String類型緩存:

  • CacheBuilder.newBuilder()

  • .maximumWeight(1024 * 1024 * 1024) // 設(shè)置最大容量為 1M

  • // 設(shè)置用來計(jì)算緩存容量的Weigher

  • .weigher(new Weigher<String, String>() {

  • @Override

  • public int weigh(String key, String value) {

  • return key.getBytes().length + value.getBytes().length;

  • }

  • }).build();

  • 當(dāng)緩存的最大數(shù)量/容量逼近或超過我們所設(shè)置的最大值時(shí),Guava就會(huì)使用LRU算法對之前的緩存進(jìn)行回收。

    基于軟/弱引用的回收

    基于引用的回收策略,是java中獨(dú)有的。在java中有對象自動(dòng)回收機(jī)制,依據(jù)程序員創(chuàng)建對象的方式不同,將對象由強(qiáng)到弱分為強(qiáng)引用、軟引用、弱引用、虛引用。對于這幾種引用他們有以下區(qū)別:

    強(qiáng)引用

    強(qiáng)引用是使用最普遍的引用。如果一個(gè)對象具有強(qiáng)引用,那垃圾回收器絕不會(huì)回收它。

    Object o=new Object();

    當(dāng)內(nèi)存空間不足,垃圾回收器不會(huì)自動(dòng)回收一個(gè)被引用的強(qiáng)引用對象,而是會(huì)直接拋出OutOfMemoryError錯(cuò)誤,使程序異常終止。

    軟引用

    相對于強(qiáng)引用,軟引用是一種不穩(wěn)定的引用方式,如果一個(gè)對象具有軟引用,當(dāng)內(nèi)存充足時(shí),GC不會(huì)主動(dòng)回收軟引用對象,而當(dāng)內(nèi)存不足時(shí)軟引用對象就會(huì)被回收。

  • SoftReference<Object> softRef=new SoftReference<Object>(new Object()); // 軟引用

  • Object object = softRef.get(); // 獲取軟引用

  • 使用軟引用能防止內(nèi)存泄露,增強(qiáng)程序的健壯性。但是一定要做好null檢測。

    弱引用

    弱引用是一種比軟引用更不穩(wěn)定的引用方式,因?yàn)闊o論內(nèi)存是否充足,弱引用對象都有可能被回收。

  • WeakReference<Object> weakRef = new WeakReference<Object>(new Object()); // 弱引用

  • Object obj = weakRef.get(); // 獲取弱引用

  • 虛引用

    而虛引用這種引用方式就是形同虛設(shè),因?yàn)槿绻粋€(gè)對象僅持有虛引用,那么它就和沒有任何引用一樣。在實(shí)踐中也幾乎沒有使用。

    在Guava cache中支持,軟/弱引用的緩存回收方式。使用這種方式能夠極大的提高內(nèi)存的利用率,并且不會(huì)出現(xiàn)內(nèi)存溢出的異常。

  • CacheBuilder.newBuilder()

  • .weakKeys() // 使用弱引用存儲鍵。當(dāng)鍵沒有其它(強(qiáng)或軟)引用時(shí),該緩存可能會(huì)被回收。

  • .weakValues() // 使用弱引用存儲值。當(dāng)值沒有其它(強(qiáng)或軟)引用時(shí),該緩存可能會(huì)被回收。

  • .softValues() // 使用軟引用存儲值。當(dāng)內(nèi)存不足并且該值其它強(qiáng)引用引用時(shí),該緩存就會(huì)被回收

  • .build();

  • 通過軟/弱引用的回收方式,相當(dāng)于將緩存回收任務(wù)交給了GC,使得緩存的命中率變得十分的不穩(wěn)定,在非必要的情況下,還是推薦基于數(shù)量和容量的回收。

    顯式回收

    在緩存構(gòu)建完畢后,我們可以通過Cache提供的接口,顯式的對緩存進(jìn)行回收,例如:

    • 個(gè)別清除:Cache.invalidate(key)
    • 批量清除:Cache.invalidateAll(keys)
    • 清除所有緩存項(xiàng):Cache.invalidateAll()
  • // 構(gòu)建一個(gè)緩存

  • Cache<String, String> cache = CacheBuilder.newBuilder().build();

  • // 回收key為k1的緩存

  • cache.invalidate("k1");

  • // 批量回收key為k1、k2的緩存

  • List<String> needInvalidateKeys = new ArrayList<>();

  • needInvalidateKeys.add("k1");

  • needInvalidateKeys.add("k2");

  • cache.invalidateAll(needInvalidateKeys);

  • // 回收所有緩存

  • cache.invalidateAll();

  • 移除監(jiān)聽器

    通過CacheBuilder.removalListener(RemovalListener),你可以聲明一個(gè)監(jiān)聽器,以便緩存項(xiàng)被移除時(shí)做一些額外操作。緩存項(xiàng)被移除時(shí),RemovalListener<會(huì)獲取移除通知[RemovalNotification],其中包含移除原因[RemovalCause]、鍵和值。

    請注意,RemovalListener拋出的任何異常都會(huì)在記錄到日志后被丟棄[swallowed]。

  • CacheLoader<Key, DatabaseConnection> loader = new CacheLoader<Key, DatabaseConnection> () {

  • public DatabaseConnection load(Key key) throws Exception {

  • return openConnection(key);

  • }

  • };

  • ?
  • RemovalListener<Key, DatabaseConnection> removalListener = new RemovalListener<Key, DatabaseConnection>() {

  • public void onRemoval(RemovalNotification<Key, DatabaseConnection> removal) {

  • DatabaseConnection conn = removal.getValue();

  • conn.close(); // tear down properly

  • }

  • };

  • ?
  • return CacheBuilder.newBuilder()

  • .expireAfterWrite(2, TimeUnit.MINUTES)

  • .removalListener(removalListener)

  • .build(loader);

  • 警告:默認(rèn)情況下,監(jiān)聽器方法是在移除緩存時(shí)同步調(diào)用的。因?yàn)榫彺娴木S護(hù)和請求響應(yīng)通常是同時(shí)進(jìn)行的,代價(jià)高昂的監(jiān)聽器方法在同步模式下會(huì)拖慢正常的緩存請求。在這種情況下,你可以使用RemovalListeners.asynchronous(RemovalListener, Executor)把監(jiān)聽器裝飾為異步操作。

    緩存的過期策略與刷新

    Guava也提供了緩存的過期策略和刷新策略。

    緩存過期策略

    緩存的過期策略分為固定時(shí)間和相對時(shí)間。

    固定時(shí)間一般是指寫入后多長時(shí)間過期,例如我們構(gòu)建一個(gè)寫入10分鐘后過期的緩存:

  • CacheBuilder.newBuilder()

  • .expireAfterWrite(10, TimeUnit.MINUTES) // 寫入10分鐘后過期

  • .build();

  • ?
  • // java8后可以使用Duration設(shè)置

  • CacheBuilder.newBuilder()

  • .expireAfterWrite(Duration.ofMinutes(10))

  • .build();

  • 相對時(shí)間一般是相對于訪問時(shí)間,也就是每次訪問后,會(huì)重新刷新該緩存的過期時(shí)間,這有點(diǎn)類似于servlet中的session過期時(shí)間,例如構(gòu)建一個(gè)在10分鐘內(nèi)未訪問則過期的緩存:

  • CacheBuilder.newBuilder()

  • .expireAfterAccess(10, TimeUnit.MINUTES) //在10分鐘內(nèi)未訪問則過期

  • .build();

  • ?
  • // java8后可以使用Duration設(shè)置

  • CacheBuilder.newBuilder()

  • .expireAfterAccess(Duration.ofMinutes(10))

  • .build();

  • 緩存刷新

    在Guava cache中支持定時(shí)刷新和顯式刷新兩種方式,其中只有LoadingCache能夠進(jìn)行定時(shí)刷新。

    定時(shí)刷新

    在進(jìn)行緩存定時(shí)刷新時(shí),我們需要指定緩存的刷新間隔,和一個(gè)用來加載緩存的CacheLoader,當(dāng)達(dá)到刷新時(shí)間間隔后,下一次獲取緩存時(shí),會(huì)調(diào)用CacheLoader的load方法刷新緩存。例如構(gòu)建個(gè)刷新頻率為10分鐘的緩存:

  • CacheBuilder.newBuilder()

  • // 設(shè)置緩存在寫入10分鐘后,通過CacheLoader的load方法進(jìn)行刷新

  • .refreshAfterWrite(10, TimeUnit.SECONDS)

  • // jdk8以后可以使用 Duration

  • // .refreshAfterWrite(Duration.ofMinutes(10))

  • .build(new CacheLoader<String, String>() {

  • @Override

  • public String load(String key) throws Exception {

  • // 緩存加載邏輯

  • ...

  • }

  • });

  • 顯式刷新

    在緩存構(gòu)建完畢后,我們可以通過Cache提供的一些借口方法,顯式的對緩存進(jìn)行刷新覆蓋,例如:

  • // 構(gòu)建一個(gè)緩存

  • Cache<String, String> cache = CacheBuilder.newBuilder().build();

  • // 使用put進(jìn)行覆蓋刷新

  • cache.put("k1", "v1");

  • // 使用Map的put方法進(jìn)行覆蓋刷新

  • cache.asMap().put("k1", "v1");

  • // 使用Map的putAll方法進(jìn)行批量覆蓋刷新

  • Map<String,String> needRefreshs = new HashMap<>();

  • needRefreshs.put("k1", "v1");

  • cache.asMap().putAll(needRefreshs);

  • // 使用ConcurrentMap的replace方法進(jìn)行覆蓋刷新

  • cache.asMap().replace("k1", "v1");

  • 對于LoadingCache,由于它能夠自動(dòng)的加載緩存,所以在進(jìn)行刷新時(shí),不需要顯式的傳入緩存的值

  • LoadingCache<String, String> loadingCache = CacheBuilder

  • .newBuilder()

  • .build(new CacheLoader<String, String>() {

  • @Override

  • public String load(String key) throws Exception {

  • // 緩存加載邏輯

  • return null;

  • }

  • });

  • // loadingCache 在進(jìn)行刷新時(shí)無需顯式的傳入 value

  • loadingCache.refresh("k1");

  • 統(tǒng)計(jì)

    CacheBuilder.recordStats()用來開啟Guava Cache的統(tǒng)計(jì)功能。統(tǒng)計(jì)打開后,Cache.stats()方法會(huì)返回CacheStats對象以提供如下統(tǒng)計(jì)信息:

    • hitRate():緩存命中率;

    • averageLoadPenalty():加載新值的平均時(shí)間,單位為納秒;

    • evictionCount():緩存項(xiàng)被回收的總數(shù),不包括顯式清除。

    此外,還有其他很多統(tǒng)計(jì)信息。這些統(tǒng)計(jì)信息對于調(diào)整緩存設(shè)置是至關(guān)重要的,在性能要求高的應(yīng)用中我們建議密切關(guān)注這些數(shù)據(jù)。
    Guava 提供了recordStats()方法,相當(dāng)于啟動(dòng)了記錄模式,通過Cache.stats()方法可以獲取CacheStats對象,里面存儲著緩存的使用情況,通過觀察它就可以知道緩存的命中率,加載耗時(shí)等信息,有了這些數(shù)據(jù)的反饋就可以調(diào)整的緩存的大小以及其他的優(yōu)化工作了。

    asMap視圖

    asMap視圖提供了緩存的ConcurrentMap形式,但asMap視圖與緩存的交互需要注意:

    • cache.asMap()包含當(dāng)前所有加載到緩存的項(xiàng)。因此相應(yīng)地,cache.asMap().keySet()包含當(dāng)前所有已加載鍵;
    • asMap().get(key)實(shí)質(zhì)上等同于cache.getIfPresent(key),而且不會(huì)引起緩存項(xiàng)的加載。這和Map的語義約定一致。
    • 所有讀寫操作都會(huì)重置相關(guān)緩存項(xiàng)的訪問時(shí)間,包括Cache.asMap().get(Object)方法和Cache.asMap().put(K, V)方法,但不包括Cache.asMap().containsKey(Object)方法,也不包括在Cache.asMap()的集合視圖上的操作。比如,遍歷Cache.asMap().entrySet()不會(huì)重置緩存項(xiàng)的讀取時(shí)間。

    常見問題

    緩存使用的最常見的問題,上文中,提到緩存數(shù)據(jù)拉取出來后,需要添加一些關(guān)于每一個(gè)訪問用戶的額外信息,例如拉取出上課列表后,每一個(gè)用戶針對課程的狀態(tài)是不一樣的(報(bào)名狀態(tài)),通常會(huì)犯的一個(gè)錯(cuò)誤就是直接在緩存數(shù)據(jù)基礎(chǔ)上進(jìn)行修改,通常我們緩存的對象會(huì)是一個(gè)Map,或者List,對其引用的修改其實(shí)已經(jīng)修改了對應(yīng)值本身,這樣會(huì)造成數(shù)據(jù)的混亂。因此記得在修改之前將緩存數(shù)據(jù)先深拷貝。

    總結(jié)

    以上是生活随笔為你收集整理的高并发之--Guava Cache的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網(wǎng)站內(nèi)容還不錯(cuò),歡迎將生活随笔推薦給好友。