Guava 源碼分析(Cache 原理)

1.jpeg

前言

Google 出的 Guava 是 Java 核心加強的庫,應用很是普遍。java

我平時用的也挺頻繁,此次就藉助平常使用的 Cache 組件來看看 Google 大牛們是如何設計的。git

緩存

本次主要討論緩存。

緩存在平常開發中舉足輕重,若是你的應用對某類數據有着較高的讀取頻次,而且改動較小時那就很是適合利用緩存來提升性能。github

緩存之因此能夠提升性能是由於它的讀取效率很高,就像是 CPU 的 L一、L二、L3 緩存同樣,級別越高相應的讀取速度也會越快。算法

但也不是什麼好處都佔,讀取速度快了可是它的內存更小資源更寶貴,因此咱們應當緩存真正須要的數據。緩存

其實也就是典型的空間換時間。

下面談談 Java 中所用到的緩存。安全

JVM 緩存

首先是 JVM 緩存,也能夠認爲是堆緩存。微信

其實就是建立一些全局變量,如 Map、List 之類的容器用於存放數據。數據結構

這樣的優點是使用簡單可是也有如下問題:併發

  • 只能顯式的寫入,清除數據。
  • 不能按照必定的規則淘汰數據,如 LRU,LFU,FIFO 等。
  • 清除數據時的回調通知。
  • 其餘一些定製功能等。

Ehcache、Guava Cache

因此出現了一些專門用做 JVM 緩存的開源工具出現了,如本文提到的 Guava Cache。分佈式

它具備上文 JVM 緩存不具備的功能,如自動清除數據、多種清除算法、清除回調等。

但也正由於有了這些功能,這樣的緩存必然會多出許多東西須要額外維護,天然也就增長了系統的消耗。

分佈式緩存

剛纔提到的兩種緩存其實都是堆內緩存,只能在單個節點中使用,這樣在分佈式場景下就招架不住了。

因而也有了一些緩存中間件,如 Redis、Memcached,在分佈式環境下能夠共享內存。

具體不在本次的討論範圍。

Guava Cache 示例

之因此想到 Guava 的 Cache,也是最近在作一個需求,大致以下:

從 Kafka 實時讀取出應用系統的日誌信息,該日誌信息包含了應用的健康情況。
若是在時間窗口 N 內發生了 X 次異常信息,相應的我就須要做出反饋(報警、記錄日誌等)。

對此 Guava 的 Cache 就很是適合,我利用了它的 N 個時間內不寫入數據時緩存就清空的特色,在每次讀取數據時判斷異常信息是否大於 X 便可。

僞代碼以下:

@Value("${alert.in.time:2}")
    private int time ;

    @Bean
    public LoadingCache buildCache(){
        return CacheBuilder.newBuilder()
                .expireAfterWrite(time, TimeUnit.MINUTES)
                .build(new CacheLoader<Long, AtomicLong>() {
                    @Override
                    public AtomicLong load(Long key) throws Exception {
                        return new AtomicLong(0);
                    }
                });
    }
    
    
    /**
     * 判斷是否須要報警
     */
    public void checkAlert() {
        try {
            if (counter.get(KEY).incrementAndGet() >= limit) {
                LOGGER.info("***********報警***********");

                //將緩存清空
                counter.get(KEY).getAndSet(0L);
            }
        } catch (ExecutionException e) {
            LOGGER.error("Exception", e);
        }
    }

首先是構建了 LoadingCache 對象,在 N 分鐘內不寫入數據時就回收緩存(當經過 Key 獲取不到緩存時,默認返回 0)。

而後在每次消費時候調用 checkAlert() 方法進行校驗,這樣就能夠達到上文的需求。

咱們來設想下 Guava 它是如何實現過時自動清除數據,而且是能夠按照 LRU 這樣的方式清除的。

大膽假設下:

內部經過一個隊列來維護緩存的順序,每次訪問過的數據移動到隊列頭部,而且額外開啓一個線程來判斷數據是否過時,過時就刪掉。有點相似於我以前寫過的 動手實現一個 LRU cache

胡適說過:大膽假設當心論證

下面來看看 Guava 究竟是怎麼實現。

原理分析

看原理最好不過是跟代碼一步步走了:

示例代碼在這裏:

https://github.com/crossoverJie/Java-Interview/blob/master/src/main/java/com/crossoverjie/guava/CacheLoaderTest.java

8.png

爲了能看出 Guava 是怎麼刪除過時數據的在獲取緩存以前休眠了 5 秒鐘,達到了超時條件。

2.png

最終會發如今 com.google.common.cache.LocalCache 類的 2187 行比較關鍵。

再跟進去以前第 2182 行會發現先要判斷 count 是否大於 0,這個 count 保存的是當前緩存的數量,並用 volatile 修飾保證了可見性。

更多關於 volatile 的相關信息能夠查看 你應該知道的 volatile 關鍵字

接着往下跟到:

3.png

2761 行,根據方法名稱能夠看出是判斷當前的 Entry 是否過時,該 entry 就是經過 key 查詢到的。

4.png

這裏就很明顯的看出是根據根據構建時指定的過時方式來判斷當前 key 是否過時了。

5.png

若是過時就往下走,嘗試進行過時刪除(須要加鎖,後面會具體討論)。

6.png

到了這裏也很清晰了:

  • 獲取當前緩存的總數量
  • 自減一(前面獲取了鎖,因此線程安全)
  • 刪除並將更新的總數賦值到 count。

其實大致上就是這個流程,Guava 並無按照以前猜測的另起一個線程來維護過時數據。

應該是如下緣由:

  • 新起線程須要資源消耗。
  • 維護過時數據還要獲取額外的鎖,增長了消耗。

而在查詢時候順帶作了這些事情,可是若是該緩存遲遲沒有訪問也會存在數據不能被回收的狀況,不過這對於一個高吞吐的應用來講也不是問題。

總結

最後再來總結下 Guava 的 Cache。

其實在上文跟代碼時會發現經過一個 key 定位數據時有如下代碼:

7.png

若是有看過 ConcurrentHashMap 的原理 應該會想到這其實很是相似。

其實 Guava Cache 爲了知足併發場景的使用,核心的數據結構就是按照 ConcurrentHashMap 來的,這裏也是一個 key 定位到一個具體位置的過程。

先找到 Segment,再找具體的位置,等因而作了兩次 Hash 定位。

上文有一個假設是對的,它內部會維護兩個隊列 accessQueue,writeQueue 用於記錄緩存順序,這樣才能夠按照順序淘汰數據(相似於利用 LinkedHashMap 來作 LRU 緩存)。

同時從上文的構建方式來看,它也是構建者模式來建立對象的。

由於做爲一個給開發者使用的工具,須要有不少的自定義屬性,利用構建則模式再合適不過了。

Guava 其實還有不少東西沒談到,好比它利用 GC 來回收內存,移除數據時的回調通知等。以後再接着討論。

掃碼關注微信公衆號,第一時間獲取消息。

weixin.png

相關文章
相關標籤/搜索