Guava學習:Cache緩存入門

摘要: 學習Google內部使用的工具包Guava,在Java項目中輕鬆地增長緩存,提升程序獲取數據的效率。

1、什麼是緩存?

根據科普中國的定義,緩存就是數據交換的緩衝區(稱做Cache),當某一硬件要讀取數據時,會首先從緩存中查找須要的數據,若是找到了則直接執行,找不到的話則從內存中找。因爲緩存的運行速度比內存快得多,故緩存的做用就是幫助硬件更快地運行。html

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

2、緩存原理

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

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

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

3、GuavaCache工做方式

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

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 {
    //身份證號
    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:CacheLoadingCache。LoadingCache繼承了Cache,他比Cache主要多了get和refresh方法。多這兩個方法能幹什麼呢?

在第四節高級特性demo中,咱們看到builder生成不帶CacheLoader的Cache實例。在類結構圖中實際上是生成了LocalManualCache類實例。而帶CacheLoader的Cache實例生成的是LocalLoadingCache。他能夠定時刷新數據,由於獲取數據的方法已經做爲構造參數方法存入了Cache實例中。一樣,在get時,不須要像LocalManualCache還須要傳入一個Callable實例。

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

6、總結回顧

緩存加載:CacheLoader、Callable、顯示插入(put)

緩存回收:LRU,定時(expireAfterAccessexpireAfterWrite),軟弱引用,顯示刪除(Cache接口方法invalidateinvalidateAll)

監聽器: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

相關文章
相關標籤/搜索