Java基于LoadingCache實現(xiàn)本地緩存

一责鳍、 添加maven依賴

<dependency>
  <groupId>com.google.guava</groupId>
  <artifactId>guava</artifactId>
  <version>27.1-jre</version>
</dependency>

二理盆、CacheBuilder方法說明

1??LoadingCache build(CacheLoader loader)
2??CacheBuilder.maximumSize(long size)配置緩存數(shù)量上限啄育,快達(dá)到上限或達(dá)到上限坏瞄,處理了時間最長沒被訪問過的對象或者根據(jù)配置的被釋放的對象
3??expireAfterAccess(long, TimeUnit)緩存項在給定時間內(nèi)沒有被讀/寫訪問,則回收哮洽。請注意這種緩存的回收順序和基于大小回收一樣
4??expireAfterWrite(long, TimeUnit)緩存項在給定時間內(nèi)沒有被寫訪問(創(chuàng)建或覆蓋)打却,則回收杉适。如果認(rèn)為緩存數(shù)據(jù)總是在固定時候后變得陳舊不可用,這種回收方式是可取的柳击。
5??refreshAfterWrite(long duration, TimeUnit unit)定時刷新猿推,可以為緩存增加自動定時刷新功能。和expireAfterWrite相反捌肴,refreshAfterWrite通過定時刷新可以讓緩存項保持可用蹬叭,但請注意:緩存項只有在被檢索時才會真正刷新毯侦,即只有刷新間隔時間到了再去get(key)才會重新去執(zhí)行Loading,否則就算刷新間隔時間到了也不會執(zhí)行l(wèi)oading操作具垫。因此侈离,如果在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存并不會因為刷新盲目地定時重置筝蚕,如果緩存項沒有被檢索卦碾,那刷新就不會真的發(fā)生,緩存項在過期時間后也變得可以回收起宽。還有一點比較重要的是refreshAfterWrite和expireAfterWrite兩個方法設(shè)置以后洲胖,重新get會引起loading操作都是同步串行的。這其實可能會有一個隱患坯沪,當(dāng)某一個時間點剛好有大量檢索過來而且都有刷新或者回收的話绿映,是會產(chǎn)生大量的請求同步調(diào)用loading方法,這些請求占用線程資源的時間明顯變長腐晾。如正常請求也就20ms叉弦,當(dāng)刷新以后加上同步請求loading這個功能接口可能響應(yīng)時間遠(yuǎn)遠(yuǎn)大于20ms。為了預(yù)防這種井噴現(xiàn)象藻糖,可以不設(shè)refreshAfterWrite方法淹冰,改用LoadingCache.refresh(K)因為它是異步執(zhí)行的,不會影響正在讀的請求巨柒,同時使用ScheduledExecutorService可以很好地實現(xiàn)這樣的定時調(diào)度樱拴,配上cache.asMap().keySet()返回當(dāng)前所有已加載鍵,這樣所有的key定時刷新就有了洋满。如果訪問量沒有這么大則直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以晶乔。

三、創(chuàng)建 CacheLoader

LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //緩存池大小牺勾,在緩存項接近該大小時正罢, Guava開始回收舊的緩存項
        .maximumSize(10000)
        //設(shè)置時間對象沒有被讀/寫訪問則對象從內(nèi)存中刪除(在另外的線程里面不定期維護)
        .expireAfterAccess(10, TimeUnit.MINUTES)
        //移除監(jiān)聽器,緩存項被移除時會觸發(fā)
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //執(zhí)行邏輯操作
          }
        })
        .recordStats()//開啟Guava Cache的統(tǒng)計功能
        .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) {
                       //從 SQL或者NoSql 獲取對象
                    }
                });//CacheLoader類 實現(xiàn)自動加載

四、工具類

import com.google.common.cache.*;
import org.slf4j.Logger;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.TimeUnit;

public class CacheManager {

  private static Logger log = Log.get();
  /** 緩存項最大數(shù)量 */
  private static final long GUAVA_CACHE_SIZE = 100000;
  /** 緩存時間:天 */
  private static final long GUAVA_CACHE_DAY = 10;
  /** 緩存操作對象 */
  private static LoadingCache<Long, String> GLOBAL_CACHE = null;

  static {
    try {
      GLOBAL_CACHE = loadCache(new CacheLoader <Long, String>() {
        @Override
        public String load(Long key) throws Exception {
          // 處理緩存鍵不存在緩存值時的處理邏輯
          return "";
        }
      });
    } catch (Exception e) {
      log.error("初始化Guava Cache出錯", e);
    }
  }

  /**
   * 全局緩存設(shè)置
   * 緩存項最大數(shù)量:100000
   * 緩存有效時間(天):10
   * @param cacheLoader
   * @return
   * @throws Exception
   */
  private static LoadingCache<Long, String> loadCache(CacheLoader<Long, String> cacheLoader) 
throws Exception {
    LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //緩存池大小禽最,在緩存項接近該大小時腺怯, Guava開始回收舊的緩存項
        .maximumSize(GUAVA_CACHE_SIZE)
        //設(shè)置時間對象沒有被讀/寫訪問則對象從內(nèi)存中刪除(在另外的線程里面不定期維護)
        .expireAfterAccess(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        // 設(shè)置緩存在寫入之后 設(shè)定時間 后失效
        .expireAfterWrite(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        //移除監(jiān)聽器,緩存項被移除時會觸發(fā)
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //邏輯操作
          }
        })
        //開啟Guava Cache的統(tǒng)計功能
        .recordStats()
        .build(cacheLoader);
    return cache;
  }

  /**
   * 設(shè)置緩存值
   * 注: 若已有該key值逃魄,則會先移除(會觸發(fā)removalListener移除監(jiān)聽器)溉箕,再添加
   *
   * @param key
   * @param value
   */
  public static void put(Long key, String value) {
    try {
      GLOBAL_CACHE.put(key, value);
    } catch (Exception e) {
      log.error("設(shè)置緩存值出錯", e);
    }
  }

  /**
   * 批量設(shè)置緩存值
   *
   * @param map
   */
  public static void putAll(Map<? extends Long, ? extends String> map) {
    try {
      GLOBAL_CACHE.putAll(map);
    } catch (Exception e) {
      log.error("批量設(shè)置緩存值出錯", e);
    }
  }

  /**
   * 獲取緩存值
   * 注:如果鍵不存在值务荆,將調(diào)用CacheLoader的load方法加載新值到該鍵中
   *
   * @param key
   * @return
   */
  public static String get(Long key) {
    String token = "";
    try {
      token = GLOBAL_CACHE.get(key);
    } catch (Exception e) {
      log.error("獲取緩存值出錯", e);
    }
    return token;
  }

 /**
   * 移除緩存
   * @param key
   */
  public static void remove(Long key) {
    try {
      GLOBAL_CACHE.invalidate(key);
    } catch (Exception e) {
      log.error("移除緩存出錯", e);
    }
  }

  /**
   * 批量移除緩存
   * @param keys
   */
  public static void removeAll(Iterable<Long> keys) {
    try {
      GLOBAL_CACHE.invalidateAll(keys);
    } catch (Exception e) {
      log.error("批量移除緩存出錯", e);
    }
  }

  /**
   * 清空所有緩存
   */
  public static void removeAll() {
    try {
      GLOBAL_CACHE.invalidateAll();
    } catch (Exception e) {
      log.error("清空所有緩存出錯", e);
    }
  }

  /**
   * 獲取緩存項數(shù)量
   * @return
   */
  public static long size() {
    long size = 0;
    try {
      size = GLOBAL_CACHE.size();
    } catch (Exception e) {
      log.error("獲取緩存項數(shù)量出錯", e);
    }
    return size;
  }
}

五、guava Cache數(shù)據(jù)移除

1??移除機制

guava做cache的時候懦趋,數(shù)據(jù)的移除分為被動移除和主動移除兩種。

【被動移除分為三種】
1)基于大小的移除:
按照緩存的大小來移除疹味,如果即將到達(dá)指定的大小仅叫,那就會把不常用的鍵值對從cache中移除帜篇。定義的方式一般為 CacheBuilder.maximumSize(long),還有一種可以算權(quán)重的方法诫咱,個人認(rèn)為實際使用中不太用到笙隙。就這個常用有一下注意點:

a. 這個size指的是cache中的條目數(shù),不是內(nèi)存大小或是其他坎缭;
b. 并不是完全到了指定的size系統(tǒng)才開始移除不常用的數(shù)據(jù)的竟痰,而是接近這個size的時候系統(tǒng)就會開始做移除的動作;
c. 如果一個鍵值對已經(jīng)從緩存中被移除了掏呼,再次請求訪問的時候坏快,如果cachebuild是使用cacheloader方式的,那依然還是會從cacheloader中再取一次值憎夷,如果這樣還沒有莽鸿,就會拋出異常。

2)基于時間的移除:
expireAfterAccess(long, TimeUnit) 根據(jù)某個鍵值對最后一次訪問之后多少時間后移除拾给;
expireAfterWrite(long, TimeUnit) 根據(jù)某個鍵值對被創(chuàng)建或值被替換后多少時間移除
3)基于引用的移除:主要是基于Java的垃圾回收機制祥得,根據(jù)鍵或者值的引用關(guān)系決定移除

【主動移除分為三種】
1)單獨移除:Cache.invalidate(key)
2)批量移除:Cache.invalidateAll(keys)
3)移除所有:Cache.invalidateAll()

如果需要在移除數(shù)據(jù)的時候有所動作還可以定義Removal Listener,但是有點需要注意的是默認(rèn)Removal Listener中的行為是和移除動作同步執(zhí)行的蒋得,如果需要改成異步形式啃沪,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)

2??遇到的問題

1)在put操作之前,如果已經(jīng)有該鍵值窄锅,會先觸發(fā)removalListener移除監(jiān)聽器创千,再添加
2)配置了expireAfterAccess和expireAfterWrite,但在指定時間后沒有被移除入偷。

解決方案:CacheBuilder在文檔上有說明:

If expireAfterWrite or expireAfterAccess is requested entries may be evicted on each cache modification, on occasional cache accesses, or on calls to Cache.cleanUp(). Expired entries may be counted in Cache.size(), but will never be visible to read or write operations.

翻譯過來大概的意思是:CacheBuilder構(gòu)建的緩存不會在特定時間自動執(zhí)行清理和回收工作追驴,也不會在某個緩存項過期后馬上清理,它不會啟動一個線程來進行緩存維護疏之,因為:

a)線程相對較重
b)某些環(huán)境限制線程的創(chuàng)建殿雪。它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做

當(dāng)然锋爪,也可以創(chuàng)建自己的維護線程丙曙,以固定的時間間隔調(diào)用Cache.cleanUp()。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末其骄,一起剝皮案震驚了整個濱河市亏镰,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌拯爽,老刑警劉巖索抓,帶你破解...
    沈念sama閱讀 217,907評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡逼肯,警方通過查閱死者的電腦和手機耸黑,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,987評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來篮幢,“玉大人大刊,你說我怎么就攤上這事∪唬” “怎么了缺菌?”我有些...
    開封第一講書人閱讀 164,298評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長赋续。 經(jīng)常有香客問我男翰,道長,這世上最難降的妖魔是什么纽乱? 我笑而不...
    開封第一講書人閱讀 58,586評論 1 293
  • 正文 為了忘掉前任蛾绎,我火速辦了婚禮,結(jié)果婚禮上鸦列,老公的妹妹穿的比我還像新娘租冠。我一直安慰自己,他們只是感情好薯嗤,可當(dāng)我...
    茶點故事閱讀 67,633評論 6 392
  • 文/花漫 我一把揭開白布顽爹。 她就那樣靜靜地躺著,像睡著了一般骆姐。 火紅的嫁衣襯著肌膚如雪镜粤。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,488評論 1 302
  • 那天玻褪,我揣著相機與錄音肉渴,去河邊找鬼。 笑死带射,一個胖子當(dāng)著我的面吹牛同规,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播窟社,決...
    沈念sama閱讀 40,275評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼券勺,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了灿里?” 一聲冷哼從身側(cè)響起关炼,我...
    開封第一講書人閱讀 39,176評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎钠四,沒想到半個月后盗扒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體跪楞,經(jīng)...
    沈念sama閱讀 45,619評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡缀去,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,819評論 3 336
  • 正文 我和宋清朗相戀三年侣灶,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片缕碎。...
    茶點故事閱讀 39,932評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡褥影,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出咏雌,到底是詐尸還是另有隱情凡怎,我是刑警寧澤,帶...
    沈念sama閱讀 35,655評論 5 346
  • 正文 年R本政府宣布赊抖,位于F島的核電站统倒,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏氛雪。R本人自食惡果不足惜房匆,卻給世界環(huán)境...
    茶點故事閱讀 41,265評論 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望报亩。 院中可真熱鬧浴鸿,春花似錦、人聲如沸弦追。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,871評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽劲件。三九已至掸哑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間零远,已是汗流浹背苗分。 一陣腳步聲響...
    開封第一講書人閱讀 32,994評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留遍烦,地道東北人俭嘁。 一個月前我還...
    沈念sama閱讀 48,095評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像服猪,于是被迫代替她去往敵國和親供填。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,884評論 2 354