Guava學習:Cache緩存

摘要: 學習Google內部使用的工具包Guava,在Java項目中輕鬆地增長緩存,提升程序獲取數據的效率。
1、什麼是緩存?
根據科普中國的定義,緩存就是數據交換的緩衝區(稱做Cache),當某一硬件要讀取數據時,會首先從緩存中查找須要的數據,若是找到了則直接執行,找不到的話則從內存中找。因爲緩存的運行速度比內存快得多,故緩存的做用就是幫助硬件更快地運行。java

在這裏,咱們借用了硬件緩存的概念,當在Java程序中計算或查詢數據的代價很高,而且對一樣的計算或查詢條件須要不止一次獲取數據的時候,就應當考慮使用緩存。換句話說,緩存就是以空間換時間,大部分應用在各類IO,數據庫查詢等耗時較長的應用當中。mysql

2、緩存原理
當獲取數據時,程序將先從一個存儲在內存中的數據結構中獲取數據。若是數據不存在,則在磁盤或者數據庫中獲取數據並存入到數據結構當中。以後程序須要再次獲取數據時,則會先查詢這個數據結構。從內存中獲取數據時間明顯小於經過IO獲取數據,這個數據結構就是緩存的實現。git

這裏引入一個概念,緩存命中率:從緩存中獲取到數據的次數/所有查詢次數,命中率越高說明這個緩存的效率好。因爲機器內存的限制,緩存通常只能佔據有限的內存大小,緩存須要不按期的刪除一部分數據,從而保證不會佔據大量內存致使機器崩潰。github

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

3、GuavaCache工做方式
GuavaCache的工做流程:獲取數據->若是存在,返回數據->計算獲取數據->存儲返回。因爲特定的工做流程,使用者必須在建立Cache或者獲取數據時指定不存在數據時應當怎麼獲取數據。GuavaCache採用LRU的工做原理,使用者必須指定緩存數據的大小,當超過緩存大小時,一定引起數據刪除。GuavaCache還可讓用戶指定緩存數據的過時時間,刷新時間等等不少有用的功能。數據庫

4、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 {async

//身份證號
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("\n使用loadingCache getIfPresent方法  第一次加載");
    man = cachDemo.getIfPresentloadingCache("002");
    System.out.println(man);

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

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

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

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

    System.out.println("\n使用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("\n使用loadingCache getIfPresent方法  第一次加載");
man = cachDemo.getIfPresentloadingCache("002");
System.out.println(man);

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

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

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

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

System.out.println("\n使用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("\n\n使用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("\n使用Cache getIfPresent方法  第一次加載");
man = cachDemo.getIfPresentCache("002");
System.out.println(man);

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

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

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

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

System.out.println("\n使用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)方法將監聽器設爲異步,筆者經過實驗發現,異步監聽不會在刪除數據時馬上調用監聽器方法。

5、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,大部分實現都是父類作的。

6、總結回顧
緩存加載: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

相關文章
相關標籤/搜索