Guava Cache探索及spring項目整合GuavaCache實例

背景

  對於高頻訪問可是低頻更新的數據咱們通常會作緩存,尤爲是在併發量比較高的業務裏,原始的手段咱們可使用HashMap或者ConcurrentHashMap來存儲.html

  這樣沒什麼毛病,可是會面臨一個問題,對於緩存中的數據只有當咱們顯示的調用remove方法,纔會移除某個元素,即使是高頻的數據,也會有訪問命中率的高低之分,內存老是有限的,咱們不可能無限地去增長Map中的數據.java

  我但願的比較完美的場景時.對於一個業務,我只想分配給你2k的內存,咱們假設map中一條數據(鍵值對)是1B,那麼最多它能存2048條數據,當數據達到這個量級的時候,須要淘汰一些訪問率比較低的數據來給新的數據騰地方,使用傳統的HashMap比較難實現,由於咱們不知道哪些數據訪問率低(除非專門去記錄),那麼Guava針對內存緩存優化的一個組件就閃亮登場了.redis

準備

  上面說到咱們須要一種淘汰策略來自動篩選緩存數據,下面簡單瞭解下,幾種淘汰算法算法

  先進先出算法(FIFO):這種淘汰策略顧名思義,先存的先淘汰.這樣簡單粗暴,可是會錯殺一些高頻訪問的數據spring

  最近最少使用算法(LRU):這個算法能有效優化FIFO的問題,高頻訪問的數據不太容易被淘汰掉,但也不能徹底避免.GuavaCache一些特性符合這種算法緩存

      最近最少頻率算法(LFU): 這個算法又對LRU作了優化,會記錄每一個數據的訪問次數,綜合訪問時間和訪問次數來淘汰數據.安全

Guava Cache基礎

  GuavaCache提供了線程安全的實現機制,簡單易用,上手成本很低,在須要使用內存作緩存的業務場景時能夠考慮使用.併發

  GuavaCache緩存機制有兩個接口,Cache和LoadingCache,後者也是一個接口,繼承自Cache,並額外多了幾個接口,若是咱們想實例化一個Cache對象,還須要瞭解一個CacheBuilder類,這個類就是雨歷來構建Cache對象的,咱們先來用CacheBuilder實例化一個Cache對象再學習它的一些字段含義.app

public static void main(String[] args) {
        Cache<String,String> myMap = CacheBuilder.newBuilder()
                .expireAfterAccess(30L, TimeUnit.SECONDS)
                .expireAfterWrite(3L,TimeUnit.MINUTES)
                .concurrencyLevel(6)
                .initialCapacity(100)
                .maximumSize(1000)
                .softValues()
                .build();

        myMap.put("name", "張三");

        System.out.println(myMap.getIfPresent("name"));

}

     這樣咱們就建立一個相似map接口的Cache對象,描述一下上面建立的這個對象:less

     建立了一個Cache對象,這個對象有這樣的特性,初始大小爲100(能存100個鍵值對),最大size爲1000,在數據寫入3分鐘後會被自動移除,而且數據若是在30秒內,沒有被訪問則會被移除,另外這Map結構的對象支持最多6個調用方同時更新這個緩存結構的數據,即併發更新操做最大數量爲6.

  咱們看到還有一個softValues()屬性沒有講,會放在下面說明,其實CacheBuilder並不僅有這麼幾個屬性可設置,下面咱們具體講一下.

CacheBuilder中一些經常使用的屬性字段:

  concurrencyLevel(int):指定容許同時更新的操做數,若不設置CacheBuilder默認爲4,這個參數會影響緩存存儲空間的分塊,能夠簡單理解爲,默認會建立指定size個map,每一個map稱爲一個區塊,數據會分別存到每一個map裏,咱們根據實際須要設置這個值的大小.

  initialCapacity(int):指定緩存初始化的空間大小,若是設置了40,而且concurrencyLevel取默認,會分紅4個區塊,每一個區塊最大的size爲10,當更新數據時,會對這個區塊進行加鎖,這就是爲何說,容許同時更新的操做數爲4,延伸一點,在淘汰數據時,也是每一個區塊單獨維護本身的淘汰策略.也就是說,若是每一個區塊size太大,競爭就會很激烈.

  maximumSize(long):指定最大緩存大小.當緩存數據達到最大值時,會按照策略淘汰掉一些不經常使用的數據,須要注意的是,在緩存數據量快要到達最大值的時候,就會開始數據的回收,簡單理解爲"防患於未然"吧

下邊三個參數分別是,SoftValues(),weakKeys(),weakValues(),在解釋這三個參數前,須要咱們先了解一下java中的軟引用,和弱引用.

  和弱引用對應的是強引用,也是咱們在編碼過程當中最常使用的,咱們聲明的變量,對象,基本都是強引用,這樣的對象,jvm在GC時不會回收,哪怕是拋出OOM.

  而弱引用就不同了,在java中,用java.lang.ref.WeakReference標示聲明的值,jvm在垃圾回收的時候會將它回收掉,那麼軟引用呢?就是用SoftReference標示的,聲明爲弱引用的對象,會在jvm的內存不足時回收掉.

  看出區別了嗎,簡單總結下就是,軟引用,只有在內存不足時纔可能被回收,在正常的垃圾回收時不會被回收,弱引用,會在jvm進行垃圾回收的時候被刪除.

  softValues():將緩存中的數據設置爲softValues模式。數據使用SoftReference類聲明,就是在SoftReference實例中存儲真實的數據。設置了softValues()的數據,會被全局垃圾回收管理器託管,按照LRU的原則來按期GC數據。數據被GC後,可能仍然會被size方法計數,可是對其執行read或write方法已經無效

  weakKeys()和weakValues():當設置爲weakKey或weakValues時,會使用(==)來匹配key或者value值(默認強引用時,使用的是equals方法),這種狀況下,數據可能會被GC,數據被GC後,可能仍然會被size方法計數,可是對其執行read或write方法已經無效

Guava cache在spring項目中的使用

  下面以一個我在項目中的實際應用梳理一下在spring項目中應該若是整合guava cache

 1.引入guava的maven依賴

<dependency>
    <groupId>com.google.guava</groupId>
    <artifactId>guava</artifactId>
    <version>26.0-jre</version>
</dependency>

  上面使用的版本是我在寫這篇筆記時的最新版本.

 2.在application-context.xml加入配置

<!--開啓緩存註解-->
    <cache:annotation-driven />

    <bean id="cacheManager" class="org.springframework.cache.guava.GuavaCacheManager">
        <property name="cacheSpecification" value="initialCapacity=500,maximumSize=5000,expireAfterAccess=2m,softValues" />
        <property name="cacheNames">
            <list>
                <value>questionCreatedTrack</value>
            </list>
        </property>
    </bean>

  在上面配置中咱們實現了一個cacheManager,這是必需要配置的,默認配置的是org.springframework.cache.support.SimpleCacheManager,咱們這裏把它改爲了Guava的緩存管理器的實現.若是使用其餘的實現,好比redis,這裏只須要配置成redis的相關緩存管理器便可

  cacheManager能夠簡單理解爲保存Cache的地方,Cache裏邊有咱們具體想要緩存的數據,通常以key-value的鍵值對形式

  上述配置的bean中聲明的兩個屬性,一個是cacheSpecification,不須要多說了,參考上面的詳細參數,須要瞭解一點的是,這裏的參數使用的是CacheBuilderSpec類,以解析表明CacheBuilder配置的字符串的形式來建立CacheBuilder實例

  cacheNames能夠根據本身的實際業務命名,可聲明多個

 3.在代碼中使用spring的cache相關注解

@Cacheable(value = "questionCreatedTrack",key="#voiceId",condition = "#voiceId>0")
    public Long getQuestionIdByVoiceId(Long anchorId, Long voiceId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        String value = redisProxy.getValue(key, String.valueOf(voiceId));
        return StringUtils.isEmpty(value) ? null : Long.parseLong(value);
    }

    @CachePut(value = "questionCreatedTrack",key = "#voiceId",condition = "#voiceId>0")
    public Long statCollectionQuestionToCache(Long anchorId, Long voiceId, Long questionId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        redisProxy.setOneToHash(key, String.valueOf(voiceId), String.valueOf(questionId));
        return questionId;
    }

    @CacheEvict(value = "questionCreatedTrack",key="#voiceId")
    public void removeCollectionQuestionFromCache(Long anchorId, Long voiceId) {
        String key = String.format(HOMEWORK_QUESTION_ANCHOR_KEY, anchorId);
        redisProxy.deleteOneToHash(key, String.valueOf(voiceId));
    }

 先簡單說一下這裏的邏輯,我主要是使用內存作一個一級緩存,redis作第二級的緩存,上面三個方法的做用分別是

  getQuestionIdByVoiceId(..):經過voiceId查詢questionId,使用@Cacheable註解標記的意思是,代碼執行到這個方法時,會先去guava cache裏去找,有的話直接返回不走方法,沒有的話再去執行方法,返回後同時加入Cache,緩存結構中的value是方法的返回值,key是方法入參中的vocieId,這裏的緩存結構是,key=voiceId,value=questionId

  statCollectionQuestionToCache():方法的邏輯是將voiceId和questionId保存進redis裏,使用@CachePut註解標記的意思是,不去緩存裏找,直接執行方法,執行完方法後,將鍵值對加入Cache.

  removeCollectionQuestionFromCache():方法的邏輯是刪除redis中的key爲voiceId的數據,使用@CacheEvict註解標記的意思是,清除Cache中key爲voiceId的數據

  經過以上三個註解,能夠實現這樣的功能,當查詢voiceId=123的對應questionId時,會先去Cache裏查,Cache裏若是沒有再去redis裏查,有的話同時加入Cache,(沒有的話,也會加入Cache,這個下面會說),而後在新增數據以及移除數據的時候,redis和Cache都會同步.

  @Cacheable方法查詢結果爲null怎麼處理

  這個須要咱們根據實際須要,決定要不要緩存查詢結果爲null的數據,若是不須要,須要使用下面的註解

    @Cacheable(value = "questionCreatedTrack",key="#voiceId",condition = "#voiceId>0",unless = "#result==null")

參考資料

http://www.voidcn.com/article/p-pvvfgdga-bos.html

http://www.javashuo.com/article/p-yfruwgfl-dm.html

相關文章
相關標籤/搜索