007-guava 緩存

1、概述

  Guava Cache與ConcurrentMap很類似,但也不徹底同樣。最基本的區別是ConcurrentMap會一直保存全部添加的元素,直到顯式地移除。相對地,Guava Cache爲了限制內存佔用,一般都設定爲自動回收元素。在某些場景下,儘管LoadingCache 不回收元素,它也是頗有用的,由於它會自動加載緩存。html

1.一、使用場景

一般來講,Guava Cache適用於:java

  • 你願意消耗一些內存空間來提高速度。
  • 你預料到某些鍵會被查詢一次以上。
  • 緩存中存放的數據總量不會超出內存容量。(Guava Cache是單個應用運行時的本地緩存。它不把數據存放到文件或外部服務器。若是這不符合你的需求,請嘗試redis這類工具)

若是你的場景符合上述的每一條,Guava Cache就適合你。git

:若是你不須要Cache中的特性,使用ConcurrentHashMap有更好的內存效率——但Cache的大多數特性都很難基於舊有的ConcurrentMap複製,甚至根本不可能作到。redis

2、使用

2.一、Guava Cache有如下兩種建立方式:

  經過這兩種方法建立的cache,和一般用map來緩存的作法比,不一樣在於,這兩種方法都實現了一種邏輯——從緩存中取key X的值,若是該值已經緩存過了,則返回緩存中的值,若是沒有緩存過,能夠經過某個方法來獲取這個值。但不一樣的在於cacheloader的定義比較寬泛,是針對整個cache定義的,能夠認爲是統一的根據key值load value的方法。而callable的方式較爲靈活,容許你在get的時候指定。緩存

方式1、建立 CacheLoader服務器

  LoadingCache是附帶CacheLoader構建而成的緩存實現。建立本身的CacheLoader一般只須要簡單地實現V load(K key) throws Exception方法。例如,你能夠用下面的代碼構建LoadingCache:
  CacheLoader: 當檢索不存在的時候,會自動的加載信息的異步

    class Person{
        private String name;

        public Person(String name) {
            this.name = name;
        }

        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    '}';
        }
    }
    public com.google.common.cache.CacheLoader<String, Person> createCacheLoader() {
        return new com.google.common.cache.CacheLoader<String, Person>() {
            @Override
            public Person load(String key) throws Exception {
                System.out.println("加載建立key:" + key);
                return new Person(key+":ddd");
            }
        };
    }

    @Test
    public void testCreateCacheLoader() throws ExecutionException {
        LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .expireAfterWrite(10, TimeUnit.MINUTES)
                .build(createCacheLoader());
        cache.put("aa",new Person("aaa"));
        Person aa = cache.get("aa");
        System.out.println(aa);//Person{name='aaa'}


        Person bb = cache.get("bb");
        System.out.println(bb); //加載建立key:bb   Person{name='bb:ddd'}
    }

方式2、建立 Callableasync

    @Test
    public void testCreateCallable() throws Exception {
        Cache<String, Person> cache = CacheBuilder.newBuilder()
                .maximumSize(1000)
                .build(); // look Ma, no CacheLoader

        try {
            cache.put("aa", new Person("aaaa"));
            // If the key wasn't in the "easy to compute" group, we need to
            // do things the hard way.
            Person aa = cache.get("aa", new Callable<Person>() {
                @Override
                public Person call() throws Exception {
                    return new Person("defalut");
//                    return doThingsTheHardWay(key);
                }
            });
            System.out.println(aa);//Person{name='aaaa'}
        } catch (Exception e) {
            throw new Exception(e.getCause());
        }

        Person bb = cache.get("bb", () -> new Person("defalut"));
        System.out.println(bb); //Person{name='defalut'}
    }

cache的參數ide

基本方法介紹函數

一、getIfPresent(Object key);  該方法從本地緩存中找值,若是找不到返回null,找到就返回相應的值。

二、get:首先會在緩存中找,緩存中找不到再經過load加載。

三、remove(@Nullable Object key);調用LocalManualCache的invalidate(Object key)方法便可調用remove.

四、evictEntries(ReferenceEntry<K, V> newest);傳入的參數爲最新的Entry,多是剛插入的,也多是剛更新過的。

該方法只有在設置了在構建緩存的時候指定了maximumSize纔會往下執行。首先清除recencyQueue,判斷該元素自身的權重是否超過上限,若是超過則移除當前元素。而後判斷總的權重是否大於上限,若是超過則去accessQueue裏找到隊首(即最不常訪問的元素)進行

五、preWriteCleanup(long now);傳人蔘數只有當前時間。鍵值引用隊列中都是存儲已經被GC,等待清除的entry信息,因此首先去處理這個裏面的entry.

讀寫隊列裏面是按照讀寫時間排序的,取出隊列中的首元素,若是當前時間與該元素的時間相差值大於設定值,則進行回收。

六、put

public V put(K key, V value); //onlyIfAbsent爲false

public V putIfAbsent(K key, V value); //onlyIfAbsent爲true

該方法顯式往本地緩存裏面插入值。從下面的流程圖中能夠看出,在執行每次put前都會進行preWriteCleanUP,在put返回前若是更新了entry則要進行evictEntries操做。

七、getUnchecked

  若是你的CacheLoader沒有定義任何checked Exception,那你可使用getUnchecked。

2.二、顯示插入數據

  使用cache.put(key, value)方法能夠直接向緩存中插入值,這會直接覆蓋掉給定鍵以前映射的值。使用Cache.asMap()視圖提供的任何方法也能修改緩存。但請注意,asMap視圖的任何方法都不能保證緩存項被原子地加載到緩存中

  進一步說,asMap視圖的原子運算在Guava Cache的原子加載範疇以外,因此相比於Cache.asMap().putIfAbsent(K,V),Cache.get(K, Callable<V>) 應該老是優先使用。

2.三、緩存回收

  Guava Cache提供了三種基本的緩存回收方式:基於容量回收、定時回收和基於引用回收。

一、基於容量的回收(size-based eviction)

  大小

  若是要規定緩存項的數目不超過固定值,只需使用CacheBuilder.maximumSize(long)。緩存將嘗試回收最近沒有使用或整體上不多使用的緩存項。  

  警告:在緩存項的數目達到限定值以前,緩存就可能進行回收操做——一般來講,這種狀況發生在緩存項的數目逼近限定值時。

  權重

  另外,不一樣的緩存項有不一樣的「權重」(weights)——例如,若是你的緩存值,佔據徹底不一樣的內存空間,你可使用CacheBuilder.weigher(Weigher)指定一個權重函數,而且用CacheBuilder.maximumWeight(long)指定最大總重。在權重限定場景中,除了要注意回收也是在重量逼近限定值時就進行了,還要知道重量是在緩存建立時計算的,所以要考慮重量計算的複雜度。 

    @Test
    public void testWeight() throws Exception {
        LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
                .maximumWeight(5)
                .weigher((Weigher<String, Person>) (s, person) -> {
                    //權重計算器
                    int weight = person.name.length();
                    System.out.println("key:"+s);
                    return weight;
                })
                .build(new CacheLoader<String, Person>() {
                    @Override
                    public Person load(String key) {
                        System.out.println("加載建立key:" + key);
                        return new Person(key + ":default");
                    }
                });

        cache.put("a",new Person("aaaaaaa1"));
        cache.put("b",new Person("bbbbbb1"));
        cache.put("c",new Person("cc1"));

        Person a = cache.get("a");
        System.out.println(a);
        Person b = cache.get("b");
        System.out.println(b);
        Person c = cache.get("c");
        System.out.println(c);

        //緩存只有 一個 c
        System.out.println(cache.asMap());
    }

輸出:

key:a
key:b
key:c
加載建立key:a
key:a
Person{name='a:default'}
加載建立key:b
key:b
Person{name='b:default'}
Person{name='cc1'}
{c=Person{name='cc1'}}

二、定時回收(Timed Eviction)

CacheBuilder提供兩種定時回收的方法:

  • expireAfterAccess(long, TimeUnit):緩存項在給定時間內沒有被讀/寫訪問,則回收。請注意這種緩存的回收順序和基於大小回收同樣。【讀一次多久後沒有被訪問過時】
  • expireAfterWrite(long, TimeUnit):緩存項在給定時間內沒有被寫訪問(建立或覆蓋),則回收。若是認爲緩存數據老是在固定時候後變得陳舊不可用,這種回收方式是可取的。【寫完多久後過時】

以下文所討論,定時回收週期性地在寫操做中執行,偶爾在讀操做中執行。

    @Test
    public void testEvictionByAccessTime() throws ExecutionException, InterruptedException {
        LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
                .expireAfterAccess(2, TimeUnit.SECONDS)
                .build(createCacheLoader());
        cache.getUnchecked("wangji");
        TimeUnit.SECONDS.sleep(3);
        Person employee = cache.getIfPresent("wangji"); //不會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));
        cache.getUnchecked("guava");

        TimeUnit.SECONDS.sleep(1);
        employee = cache.getIfPresent("guava"); //會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));

        TimeUnit.SECONDS.sleep(2);
        employee = cache.getIfPresent("guava"); //不會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));

        TimeUnit.SECONDS.sleep(2);
        employee = cache.getIfPresent("guava"); //不會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));

        TimeUnit.SECONDS.sleep(2);
        employee = cache.getIfPresent("guava"); //不會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));
    }

 

輸出

加載建立key:wangji
被銷燬:是的
加載建立key:guava
被銷燬:否
被銷燬:是的
被銷燬:是的
被銷燬:是的

 

三、基於引用的回收(Reference-based Eviction)【強(strong)、軟(soft)、弱(weak)、虛(phantom】

經過使用弱引用的鍵、或弱引用的值、或軟引用的值,Guava Cache能夠把緩存設置爲容許垃圾回收:

  • CacheBuilder.weakKeys():使用弱引用存儲鍵。當鍵沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用鍵的緩存用==而不是equals比較鍵。
  • CacheBuilder.weakValues():使用弱引用存儲值。當值沒有其它(強或軟)引用時,緩存項能夠被垃圾回收。由於垃圾回收僅依賴恆等式(==),使用弱引用值的緩存用==而不是equals比較值。
  • CacheBuilder.softValues():使用軟引用存儲值。軟引用只有在響應內存須要時,才按照全局最近最少使用的順序回收。考慮到使用軟引用的性能影響,咱們一般建議使用更有性能預測性的緩存大小限定(見上文,基於容量回收)。使用軟引用值的緩存一樣用==而不是equals比較值。
    @Test
    public void testWeakKey() throws ExecutionException, InterruptedException {
        LoadingCache<String, Person> cache = CacheBuilder.newBuilder()
//                .weakValues()
                .weakKeys()
                .softValues()
                .build(createCacheLoader());
        cache.getUnchecked("guava");
        cache.getUnchecked("wangji");

        System.gc();
        TimeUnit.MILLISECONDS.sleep(100);
        Person employee = cache.getIfPresent("guava"); //不會從新加載建立cache
        System.out.println("被銷燬:" + (employee == null ? "是的" : "否"));
    }

 

輸出:

加載建立key:guava
加載建立key:wangji
被銷燬:否

 

2.四、顯式清除

  任什麼時候候,你均可以顯式地清除緩存項,而不是等到它被回收:
  個別清除:Cache.invalidate(key)
  批量清除:Cache.invalidateAll(keys)
  清除全部緩存項:Cache.invalidateAll()

清理何時發生

  使用CacheBuilder構建的緩存不會」自動」執行清理和回收工做,也不會在某個緩存項過時後立刻清理,也沒有諸如此類的清理機制。相反,它會在寫操做時順帶作少許的維護工做,或者偶爾在讀操做時作——若是寫操做實在太少的話。

  這樣作的緣由在於:若是要自動地持續清理緩存,就必須有一個線程,這個線程會和用戶操做競爭共享鎖。此外,某些環境下線程建立可能受限制,這樣CacheBuilder就不可用了。

  相反,咱們把選擇權交到你手裏。若是你的緩存是高吞吐的,那就無需擔憂緩存的維護和清理等工做。若是你的 緩存只會偶爾有寫操做,而你又不想清理工做阻礙了讀操做,那麼能夠建立本身的維護線程,以固定的時間間隔調用Cache.cleanUp()。

  ScheduledExecutorService能夠幫助你很好地實現這樣的定時調度。

刷新

  刷新和回收不太同樣。正如LoadingCache.refresh(K)所聲明,刷新表示爲鍵加載新值,這個過程能夠是異步的。在刷新操做進行時,緩存仍然能夠向其餘線程返回舊值,而不像回收操做,讀緩存的線程必須等待新值加載完成。

    @Test
    public void testCacheRefresh() throws InterruptedException {
        AtomicInteger counter = new AtomicInteger(0);
        CacheLoader<String, Long> cacheLoader = CacheLoader.from(k -> {
            counter.incrementAndGet();
            System.out.println("建立 key :" + k);
            return System.currentTimeMillis();
        });
        LoadingCache<String, Long> cache = CacheBuilder.newBuilder()
                .refreshAfterWrite(2, TimeUnit.SECONDS) // 2s後從新刷新
                .build(cacheLoader);


        Long result1 = cache.getUnchecked("guava");
        TimeUnit.SECONDS.sleep(3);
        Long result2 = cache.getUnchecked("guava");
        System.out.println(result1.longValue() != result2.longValue() ? "是的" : "否");
    }

  CacheBuilder.refreshAfterWrite(long, TimeUnit)能夠爲緩存增長自動定時刷新功能。和expireAfterWrite相反,refreshAfterWrite經過定時刷新可讓緩存項保持可用,但請注意:緩存項只有在被檢索時纔會真正刷新(若是CacheLoader.refresh實現爲異步,那麼檢索不會被刷新拖慢)。所以,若是你在緩存上同時聲明expireAfterWrite和refreshAfterWrite,緩存並不會由於刷新盲目地定時重置,若是緩存項沒有被檢索,那刷新就不會真的發生,緩存項在過時時間後也變得能夠回收。

2.五、移除監聽器

  經過CacheBuilder.removalListener(RemovalListener),你能夠聲明一個監聽器,以便緩存項被移除時作一些額外操做。緩存項被移除時,RemovalListener會獲取移除通知[RemovalNotification],其中包含移除緣由[RemovalCause]、鍵和值。

    @Test
    public void testCacheRemovedNotification() {
        CacheLoader<String, String> loader = CacheLoader.from(String::toUpperCase);
        RemovalListener<String, String> listener = notification -> {
            if (notification.wasEvicted()) {
                RemovalCause cause = notification.getCause();
                System.out.println("remove cause is :" + cause.toString());
                System.out.println("key:" + notification.getKey() + "value:" + notification.getValue());
            }
        };
        LoadingCache<String, String> cache = CacheBuilder.newBuilder()
                .maximumSize(3)
                .removalListener(listener)// 添加刪除監聽
                .build(loader);
        cache.getUnchecked("lhx");
        cache.getUnchecked("wangwang");
        cache.getUnchecked("guava");
        cache.getUnchecked("test");
        cache.getUnchecked("test1");
    }

 

輸出

remove cause is :SIZE
key:lhxvalue:LHX
remove cause is :SIZE
key:wangwangvalue:WANGWANG

  警告:默認狀況下,監聽器方法是在移除緩存時同步調用的。由於緩存的維護和請求響應一般是同時進行的,代價高昂的監聽器方法在同步模式下會拖慢正常的緩存請求。在這種狀況下,你可使用RemovalListeners.asynchronous(RemovalListener, Executor)把監聽器裝飾爲異步操做。

  請注意,RemovalListener拋出的任何異常都會在記錄到日誌後被丟棄[swallowed]。

相關文章
相關標籤/搜索