天天看點

Guava學習:Cache緩存入門

一、什麼是緩存?

        根據科普中國的定義,緩存就是資料交換的緩沖區(稱作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;
                }
                if (key.equals("003")) {
                    tmpman.setName("王麻子");
                    return tmpman;
                }
                if (key.equals("004")) {
                    tmpman.setName("小明");
                    return tmpman;
                }
                if (key.equals("005")) {
                    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);
    }
}
           

測試結果如下:

Guava學習:Cache緩存入門

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);
    }
           

測試結果如下:

Guava學習:Cache緩存入門
Guava學習:Cache緩存入門

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

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

五、GuavaCache結構初探

Guava學習:Cache緩存入門

類結構圖

        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/p/Guava_Cache.html

https://github.com/tiantiangao/guava-study/blob/master/doc/caches.md

http://www.blogjava.net/DLevin/archive/2013/10/20/404847.html

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

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

版權聲明:本文為CSDN部落客「weixin_34195142」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。

原文連結:https://blog.csdn.net/weixin_34195142/article/details/92570909