1. 程式人生 > 其它 >Java基於LoadingCache實現本地快取

Java基於LoadingCache實現本地快取

技術標籤:javajava快取

Java基於LoadingCache實現本地快取

一、 新增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)

配置快取數量上限,快達到上限或達到上限,處理了時間最長沒被訪問過的物件或者根據配置的被釋放的物件
3️⃣expireAfterAccess(long, TimeUnit)快取項在給定時間內沒有被讀/寫訪問,則回收。請注意這種快取的回收順序和基於大小回收一樣
4️⃣expireAfterWrite(long, TimeUnit)快取項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。如果認為快取資料總是在固定時候後變得陳舊不可用,這種回收方式是可取的。
5️⃣refreshAfterWrite(long duration, TimeUnit unit)定時重新整理,可以為快取增加自動定時重新整理功能。和expireAfterWrite相反,refreshAfterWrite通過定時重新整理可以讓快取項保持可用,但請注意:快取項只有在被檢索時才會真正重新整理,即只有重新整理間隔時間到了再去get(key)才會重新去執行Loading,否則就算重新整理間隔時間到了也不會執行loading操作。因此,如果在快取上同時宣告expireAfterWrite和refreshAfterWrite,快取並不會因為重新整理盲目地定時重置,如果快取項沒有被檢索,那重新整理就不會真的發生,快取項在過期時間後也變得可以回收。還有一點比較重要的是refreshAfterWrite和expireAfterWrite兩個方法設定以後,重新get會引起loading操作都是同步序列的。這其實可能會有一個隱患,當某一個時間點剛好有大量檢索過來而且都有重新整理或者回收的話,是會產生大量的請求同步呼叫loading方法,這些請求佔用執行緒資源的時間明顯變長。如正常請求也就20ms,當重新整理以後加上同步請求loading這個功能介面可能響應時間遠遠大於20ms。為了預防這種井噴現象,可以不設refreshAfterWrite方法,改用LoadingCache.refresh(K)因為它是非同步執行的,不會影響正在讀的請求,同時使用ScheduledExecutorService可以很好地實現這樣的定時排程,配上cache.asMap().keySet()返回當前所有已載入鍵,這樣所有的key定時重新整理就有了。如果訪問量沒有這麼大則直接用CacheBuilder.refreshAfterWrite(long, TimeUnit)也可以。

三、建立 CacheLoader

LoadingCache<Long, String> cache = CacheBuilder.newBuilder()
        //快取池大小,在快取項接近該大小時, Guava開始回收舊的快取項
        .maximumSize(10000)
        //設定時間物件沒有被讀/寫訪問則物件從記憶體中刪除(在另外的執行緒裡面不定期維護)
        .expireAfterAccess(10, TimeUnit.MINUTES)
        //移除監聽器,快取項被移除時會觸發
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //執行邏輯操作
          }
        })
        .recordStats()//開啟Guava Cache的統計功能
        .build(new CacheLoader<String, Object>() {
                    @Override
                    public Object load(String key) {
                       //從 SQL或者NoSql 獲取物件
                    }
                });//CacheLoader類 實現自動載入

四、工具類

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();
  /** 快取項最大數量 */
  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);
    }
  }

  /**
   * 全域性快取設定
   * 快取項最大數量: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)
        //設定時間物件沒有被讀/寫訪問則物件從記憶體中刪除(在另外的執行緒裡面不定期維護)
        .expireAfterAccess(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        // 設定快取在寫入之後 設定時間 後失效
        .expireAfterWrite(GUAVA_CACHE_DAY, TimeUnit.DAYS)
        //移除監聽器,快取項被移除時會觸發
        .removalListener(new RemovalListener <Long, String>() {
          @Override
          public void onRemoval(RemovalNotification<Long, String> rn) {
            //邏輯操作
          }
        })
        //開啟Guava Cache的統計功能
        .recordStats()
        .build(cacheLoader);
    return cache;
  }

  /**
   * 設定快取值
   * 注: 若已有該key值,則會先移除(會觸發removalListener移除監聽器),再新增
   *
   * @param key
   * @param value
   */
  public static void put(Long key, String value) {
    try {
      GLOBAL_CACHE.put(key, value);
    } catch (Exception e) {
      log.error("設定快取值出錯", e);
    }
  }

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

  /**
   * 獲取快取值
   * 注:如果鍵不存在值,將呼叫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);
    }
  }

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

五、guava Cache資料移除

1️⃣移除機制

guava做cache的時候,資料的移除分為被動移除和主動移除兩種。

【被動移除分為三種】
1)基於大小的移除:
按照快取的大小來移除,如果即將到達指定的大小,那就會把不常用的鍵值對從cache中移除。定義的方式一般為 CacheBuilder.maximumSize(long),還有一種可以算權重的方法,個人認為實際使用中不太用到。就這個常用有一下注意點:

a. 這個size指的是cache中的條目數,不是記憶體大小或是其他;
b. 並不是完全到了指定的size系統才開始移除不常用的資料的,而是接近這個size的時候系統就會開始做移除的動作;
c. 如果一個鍵值對已經從快取中被移除了,再次請求訪問的時候,如果cachebuild是使用cacheloader方式的,那依然還是會從cacheloader中再取一次值,如果這樣還沒有,就會丟擲異常。

2)基於時間的移除:
expireAfterAccess(long, TimeUnit) 根據某個鍵值對最後一次訪問之後多少時間後移除;
expireAfterWrite(long, TimeUnit) 根據某個鍵值對被建立或值被替換後多少時間移除
3)基於引用的移除:主要是基於Java的垃圾回收機制,根據鍵或者值的引用關係決定移除

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

如果需要在移除資料的時候有所動作還可以定義Removal Listener,但是有點需要注意的是預設Removal Listener中的行為是和移除動作同步執行的,如果需要改成非同步形式,可以考慮使用RemovalListeners.asynchronous(RemovalListener, Executor)

2️⃣遇到的問題

1)在put操作之前,如果已經有該鍵值,會先觸發removalListener移除監聽器,再新增
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構建的快取不會在特定時間自動執行清理和回收工作,也不會在某個快取項過期後馬上清理,它不會啟動一個執行緒來進行快取維護,因為:

a)執行緒相對較重
b)某些環境限制執行緒的建立。它會在寫操作時順帶做少量的維護工作,或者偶爾在讀操作時做

當然,也可以建立自己的維護執行緒,以固定的時間間隔呼叫Cache.cleanUp()。