Guava學習:Cache快取

Huangy遠發表於2019-01-19

摘要: 學習Google內部使用的工具包Guava,在Java專案中輕鬆地增加快取,提高程式獲取資料的效率。
一、什麼是快取?
根據科普中國的定義,快取就是資料交換的緩衝區(稱作Cache),當某一硬體要讀取資料時,會首先從快取中查詢需要的資料,如果找到了則直接執行,找不到的話則從記憶體中找。由於快取的執行速度比記憶體快得多,故快取的作用就是幫助硬體更快地執行。

在這裡,我們借用了硬體快取的概念,當在Java程式中計算或查詢資料的代價很高,並且對同樣的計算或查詢條件需要不止一次獲取資料的時候,就應當考慮使用快取。換句話說,快取就是以空間換時間,大部分應用在各種IO,資料庫查詢等耗時較長的應用當中。

二、快取原理
當獲取資料時,程式將先從一個儲存在記憶體中的資料結構中獲取資料。如果資料不存在,則在磁碟或者資料庫中獲取資料並存入到資料結構當中。之後程式需要再次獲取資料時,則會先查詢這個資料結構。從記憶體中獲取資料時間明顯小於通過IO獲取資料,這個資料結構就是快取的實現。

這裡引入一個概念,快取命中率:從快取中獲取到資料的次數/全部查詢次數,命中率越高說明這個快取的效率好。由於機器記憶體的限制,快取一般只能佔據有限的記憶體大小,快取需要不定期的刪除一部分資料,從而保證不會佔據大量記憶體導致機器崩潰。

如何提高命中率呢?那就得從刪除一部分資料著手了。目前有三種刪除資料的方式,分別是:FIFO(先進先出)、LFU(定期淘汰最少使用次數)、LRU(淘汰最長時間未被使用)。

三、GuavaCache工作方式
GuavaCache的工作流程:獲取資料->如果存在,返回資料->計算獲取資料->儲存返回。由於特定的工作流程,使用者必須在建立Cache或者獲取資料時指定不存在資料時應當怎麼獲取資料。GuavaCache採用LRU的工作原理,使用者必須指定快取資料的大小,當超過快取大小時,必定引發資料刪除。GuavaCache還可以讓使用者指定快取資料的過期時間,重新整理時間等等很多有用的功能。

四、GuavaCache使用Demo
4.1 簡單使用
有人說我就想簡簡單單的使用cache,就像Map那樣方便就行。接下來展示一段簡單的使用方式。

首先定義一個需要儲存的Bean,物件Man:

/**

  • @author jiangmitiao
  • @version V1.0
  • @Title: 標題
  • @Description: Bean
  • @date 2016/10/27 10:01

*/
public class Man {

//身份證號
private String id;
//姓名
private String name;

public String getId() {
    return id;
}

public void setId(String id) {
    this.id = id;
}

public String getName() {
    return name;
}

public void setName(String name) {
    this.name = name;
}

@Override
public String toString() {
    return "Man{" +
            "id=`" + id + ``` +
            ", name=`" + name + ``` +
            `}`;
}

}
接下來我們寫一個Demo:

import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;

/**

  • @author jiangmitiao
  • @version V1.0
  • @Description: Demo
  • @date 2016/10/27 10:00

*/
public class GuavaCachDemo {

private LoadingCache<String,Man> loadingCache;

//loadingCache
public void InitLoadingCache() {
    //指定一個如果資料不存在獲取資料的方法
    CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() {
        @Override
        public Man load(String key) throws Exception {
            //模擬mysql操作
            Logger logger = LoggerFactory.getLogger("LoadingCache");
            logger.info("LoadingCache測試 從mysql載入快取ing...(2s)");
            Thread.sleep(2000);
            logger.info("LoadingCache測試 從mysql載入快取成功");
            Man tmpman = new Man();
            tmpman.setId(key);
            tmpman.setName("其他人");
            if (key.equals("001")) {
                tmpman.setName("張三");
                return tmpman;
            }
            if (key.equals("002")) {
                tmpman.setName("李四");
                return tmpman;
            }
            return tmpman;
        }
    };
    //快取數量為1,為了展示快取刪除效果
    loadingCache = CacheBuilder.newBuilder().maximumSize(1).build(cacheLoader);
}
//獲取資料,如果不存在返回null
public Man getIfPresentloadingCache(String key){
    return loadingCache.getIfPresent(key);
}
//獲取資料,如果資料不存在則通過cacheLoader獲取資料,快取並返回
public Man getCacheKeyloadingCache(String key){
    try {
        return loadingCache.get(key);
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    return null;
}
//直接向快取put資料
public void putloadingCache(String key,Man value){
    Logger logger = LoggerFactory.getLogger("LoadingCache");
    logger.info("put key :{} value : {}",key,value.getName());
    loadingCache.put(key,value);
}

}
接下來,我們寫一些測試方法,檢測一下

public class Test {

public static void main(String[] args){
    GuavaCachDemo cachDemo = new GuavaCachDemo()
    System.out.println("使用loadingCache");
    cachDemo.InitLoadingCache();

    System.out.println("使用loadingCache get方法  第一次載入");
    Man man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);

    System.out.println("
使用loadingCache getIfPresent方法  第一次載入");
    man = cachDemo.getIfPresentloadingCache("002");
    System.out.println(man);

    System.out.println("
使用loadingCache get方法  第一次載入");
    man = cachDemo.getCacheKeyloadingCache("002");
    System.out.println(man);

    System.out.println("
使用loadingCache get方法  已載入過");
    man = cachDemo.getCacheKeyloadingCache("002");
    System.out.println(man);

    System.out.println("
使用loadingCache get方法  已載入過,但是已經被剔除掉,驗證重新載入");
    man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);

    System.out.println("
使用loadingCache getIfPresent方法  已載入過");
    man = cachDemo.getIfPresentloadingCache("001");
    System.out.println(man);

    System.out.println("
使用loadingCache put方法  再次get");
    Man newMan = new Man();
    newMan.setId("001");
    newMan.setName("額外新增");
    cachDemo.putloadingCache("001",newMan);
    man = cachDemo.getCacheKeyloadingCache("001");
    System.out.println(man);
}

}
測試結果如下:

150850_81Jv_1983603.png

4.2 高階特性
由於目前使用有侷限性,接下來只講我用到的一些方法。
我來演示一下GuavaCache自帶的兩個Cache

GuavaCacheDemo.java

import com.google.common.cache.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.concurrent.*;

/**

  • @author jiangmitiao
  • @version V1.0
  • @Description: Demo
  • @date 2016/10/27 10:00

*/
public class GuavaCachDemo {

private Cache<String, Man> cache;
private LoadingCache<String,Man> loadingCache;
private RemovalListener<String, Man> removalListener;

public void Init(){
    //移除key-value監聽器
    removalListener = new RemovalListener<String, Man>(){
        public void onRemoval(RemovalNotification<String, Man> notification) {
            Logger logger = LoggerFactory.getLogger("RemovalListener");
            logger.info(notification.getKey()+"被移除");
            //可以在監聽器中獲取key,value,和刪除原因
            notification.getValue();
            notification.getCause();//EXPLICIT、REPLACED、COLLECTED、EXPIRED、SIZE

        }};
    //可以使用RemovalListeners.asynchronous方法將移除監聽器設為非同步方法
    //removalListener = RemovalListeners.asynchronous(removalListener, new ThreadPoolExecutor(1,1,1000, TimeUnit.MINUTES,new ArrayBlockingQueue<Runnable>(1)));
}

//loadingCache
public void InitLoadingCache() {
    //指定一個如果資料不存在獲取資料的方法
    CacheLoader<String, Man> cacheLoader = new CacheLoader<String, Man>() {
        @Override
        public Man load(String key) throws Exception {
            //模擬mysql操作
            Logger logger = LoggerFactory.getLogger("LoadingCache");
            logger.info("LoadingCache測試 從mysql載入快取ing...(2s)");
            Thread.sleep(2000);
            logger.info("LoadingCache測試 從mysql載入快取成功");
            Man tmpman = new Man();
            tmpman.setId(key);
            tmpman.setName("其他人");
            if (key.equals("001")) {
                tmpman.setName("張三");
                return tmpman;
            }
            if (key.equals("002")) {
                tmpman.setName("李四");
                return tmpman;
            }
            return tmpman;
        }
    };
    //快取數量為1,為了展示快取刪除效果
    loadingCache = CacheBuilder.newBuilder().
            //設定2分鐘沒有獲取將會移除資料
            expireAfterAccess(2, TimeUnit.MINUTES).
            //設定2分鐘沒有更新資料則會移除資料
            expireAfterWrite(2, TimeUnit.MINUTES).
            //每1分鐘重新整理資料
            refreshAfterWrite(1,TimeUnit.MINUTES).
            //設定key為弱引用
            weakKeys().

// weakValues().//設定存在時間和重新整理時間後不能再次設定
// softValues().//設定存在時間和重新整理時間後不能再次設定

            maximumSize(1).
            removalListener(removalListener).
            build(cacheLoader);
}

//獲取資料,如果不存在返回null
public Man getIfPresentloadingCache(String key){
    return loadingCache.getIfPresent(key);
}

//獲取資料,如果資料不存在則通過cacheLoader獲取資料,快取並返回
public Man getCacheKeyloadingCache(String key){
    try {
        return loadingCache.get(key);
    } catch (ExecutionException e) {
        e.printStackTrace();
    }
    return null;
}

//直接向快取put資料
public void putloadingCache(String key,Man value){
    Logger logger = LoggerFactory.getLogger("LoadingCache");
    logger.info("put key :{} value : {}",key,value.getName());
    loadingCache.put(key,value);
}

public void InitDefault() {
    cache = CacheBuilder.newBuilder().
            expireAfterAccess(2, TimeUnit.MINUTES).
            expireAfterWrite(2, TimeUnit.MINUTES).

// refreshAfterWrite(1,TimeUnit.MINUTES).//沒有cacheLoader的cache不能設定重新整理,因為沒有指定獲取資料的方式

            weakKeys().

// weakValues().//設定存在時間和重新整理時間後不能再次設定
// softValues().//設定存在時間和重新整理時間後不能再次設定

            maximumSize(1).
            removalListener(removalListener).
            build();
}

public Man getIfPresentCache(String key){
    return cache.getIfPresent(key);
}
public Man getCacheKeyCache(final String key) throws ExecutionException {
    return cache.get(key, new Callable<Man>() {
        public Man call() throws Exception {
            //模擬mysql操作
            Logger logger = LoggerFactory.getLogger("Cache");
            logger.info("Cache測試 從mysql載入快取ing...(2s)");
            Thread.sleep(2000);
            logger.info("Cache測試 從mysql載入快取成功");
            Man tmpman = new Man();
            tmpman.setId(key);
            tmpman.setName("其他人");
            if (key.equals("001")) {
                tmpman.setName("張三");
                return tmpman;
            }
            if (key.equals("002")) {
                tmpman.setName("李四");
                return tmpman;
            }
            return tmpman;
        }
    });
}

public void putCache(String key,Man value){
    Logger logger = LoggerFactory.getLogger("Cache");
    logger.info("put key :{} value : {}",key,value.getName());
    cache.put(key,value);
}

}
在這個demo中,分別採用了Guava自帶的兩個Cache:LocalLoadingCache和LocalManualCache。並且新增了監聽器,當資料被刪除後會列印日誌。

Main:

public static void main(String[] args){

GuavaCachDemo cachDemo = new GuavaCachDemo();
cachDemo.Init();

System.out.println("使用loadingCache");
cachDemo.InitLoadingCache();

System.out.println("使用loadingCache get方法  第一次載入");
Man man = cachDemo.getCacheKeyloadingCache("001");
System.out.println(man);

System.out.println("
使用loadingCache getIfPresent方法  第一次載入");
man = cachDemo.getIfPresentloadingCache("002");
System.out.println(man);

System.out.println("
使用loadingCache get方法  第一次載入");
man = cachDemo.getCacheKeyloadingCache("002");
System.out.println(man);

System.out.println("
使用loadingCache get方法  已載入過");
man = cachDemo.getCacheKeyloadingCache("002");
System.out.println(man);

System.out.println("
使用loadingCache get方法  已載入過,但是已經被剔除掉,驗證重新載入");
man = cachDemo.getCacheKeyloadingCache("001");
System.out.println(man);

System.out.println("
使用loadingCache getIfPresent方法  已載入過");
man = cachDemo.getIfPresentloadingCache("001");
System.out.println(man);

System.out.println("
使用loadingCache put方法  再次get");
Man newMan = new Man();
newMan.setId("001");
newMan.setName("額外新增");
cachDemo.putloadingCache("001",newMan);
man = cachDemo.getCacheKeyloadingCache("001");
System.out.println(man);

///////////////////////////////////
System.out.println("

使用Cache");
cachDemo.InitDefault();

System.out.println("使用Cache get方法  第一次載入");
try {
    man = cachDemo.getCacheKeyCache("001");
} catch (ExecutionException e) {
    e.printStackTrace();
}
System.out.println(man);

System.out.println("
使用Cache getIfPresent方法  第一次載入");
man = cachDemo.getIfPresentCache("002");
System.out.println(man);

System.out.println("
使用Cache get方法  第一次載入");
try {
    man = cachDemo.getCacheKeyCache("002");
} catch (ExecutionException e) {
    e.printStackTrace();
}
System.out.println(man);

System.out.println("
使用Cache get方法  已載入過");
try {
    man = cachDemo.getCacheKeyCache("002");
} catch (ExecutionException e) {
    e.printStackTrace();
}
System.out.println(man);

System.out.println("
使用Cache get方法  已載入過,但是已經被剔除掉,驗證重新載入");
try {
    man = cachDemo.getCacheKeyCache("001");
} catch (ExecutionException e) {
    e.printStackTrace();
}
System.out.println(man);

System.out.println("
使用Cache getIfPresent方法  已載入過");
man = cachDemo.getIfPresentCache("001");
System.out.println(man);

System.out.println("
使用Cache put方法  再次get");
Man newMan1 = new Man();
newMan1.setId("001");
newMan1.setName("額外新增");
cachDemo.putloadingCache("001",newMan1);
man = cachDemo.getCacheKeyloadingCache("001");
System.out.println(man);

}
測試結果如下:
152412_Afd2_1983603.png

152425_uKCJ_1983603.png

由上述結果可以表明,GuavaCache可以在資料儲存到達指定大小後刪除資料結構中的資料。我們可以設定定期刪除而達到定期從資料庫、磁碟等其他地方更新資料等(再次訪問時資料不存在重新獲取)。也可以採用定時重新整理的方式更新資料。

還可以設定移除監聽器對被刪除的資料進行一些操作。通過RemovalListeners.asynchronous(RemovalListener,Executor)方法將監聽器設為非同步,筆者通過實驗發現,非同步監聽不會在刪除資料時立刻呼叫監聽器方法。

五、GuavaCache結構初探
153356_Z1zV_1983603.png

類結構圖

GuavaCache並不希望我們設定複雜的引數,而讓我們採用建造者模式建立Cache。GuavaCache分為兩種Cache:Cache,LoadingCache。LoadingCache繼承了Cache,他比Cache主要多了get和refresh方法。多這兩個方法能幹什麼呢?

在第四節高階特性demo中,我們看到builder生成不帶CacheLoader的Cache例項。在類結構圖中其實是生成了LocalManualCache類例項。而帶CacheLoader的Cache例項生成的是LocalLoadingCache。他可以定時重新整理資料,因為獲取資料的方法已經作為構造引數方法存入了Cache例項中。同樣,在get時,不需要像LocalManualCache還需要傳入一個Callable例項。

實際上,這兩個Cache實現類都繼承自LocalCache,大部分實現都是父類做的。

六、總結回顧
快取載入:CacheLoader、Callable、顯示插入(put)

快取回收:LRU,定時(expireAfterAccess,expireAfterWrite),軟弱引用,顯示刪除(Cache介面方法invalidate,invalidateAll)

監聽器:CacheBuilder.removalListener(RemovalListener)

清理快取時間:只有在獲取資料時才或清理快取LRU,使用者可以單起執行緒採用Cache.cleanUp()方法主動清理。

重新整理:主動重新整理方法LoadingCache.referesh(K)

資訊統計:CacheBuilder.recordStats() 開啟Guava Cache的統計功能。Cache.stats() 返回CacheStats物件。(其中包括命中率等相關資訊)

獲取當前快取所有資料:cache.asMap(),cache.asMap().get(Object)會重新整理資料的訪問時間(影響的是:建立時設定的在多久沒訪問後刪除資料)

LocalManualCache和LocalLoadingCache的選擇
ManualCache可以在get時動態設定獲取資料的方法,而LoadingCache可以定時重新整理資料。如何取捨?我認為在快取資料有很多種類的時候採用第一種cache。而資料單一,資料庫資料會定時重新整理時採用第二種cache。

參考資料:
http://www.cnblogs.com/peida/…

https://github.com/tiantianga…

http://www.blogjava.net/DLevi…

http://ifeve.com/google-guava…

更多文章:http://blog.gavinzh.com

相關文章