1. 程式人生 > >Java Cache系列之Cache概述和Simple Cache

Java Cache系列之Cache概述和Simple Cache

前記:最近公司在做的專案完全基於Cache(Gemfire)構建了一個類資料庫的系統,自己做的一個小專案裡用過Guava的Cache,以前做過的專案中使用過EHCache,既然和Cache那麼有緣,那就趁這個機會好好研究一下Java中的Cache庫。在Java社群中已經提供了很多Cache庫實現,具體可以參考http://www.open-open.com/13.htm,這裡只關注自己用到的幾個Cache庫而且這幾個庫都比較具有代表性:Guava中提供的Cache是基於單JVM的簡單實現;EHCache出自Hibernate,也是基於單JVM的實現,是對單JVM Cache比較完善的實現;而Gemfire則提供了對分散式Cache的完善實現。這一系列的文章主要關注在這幾個Cache系統的實現上,因而步探討關於Cache的好處、何時用Cache等問題,由於他們都是基於記憶體的Cache,因而也僅侷限於這種型別的Cache(說實話,我不知道有沒有其他的Cache系統,比如基於檔案?囧)。


記得我最早接觸Cache是在大學學計算機組成原理的時候,由於CPU的速度要遠大於記憶體的讀取速度,為了提高CPU的效率,CPU會在內部提供快取區,該快取區的讀取速度和CPU的處理速度類似,CPU可以直接從快取區中讀取資料,從而解決CPU的處理速度和記憶體讀取速度不匹配的問題。快取之所以能解決這個問題是基於程式的區域性性原理,即”程式在執行時呈現出區域性性規律,即在一段時間內,整個程式的執行僅限於程式中的某一部分。相應地,執行所訪問的儲存空間也侷限於某個記憶體區域。區域性性原理又表現為:時間區域性性和空間區域性性。時間區域性性是指如果程式中的某條指令一旦執行,則不久之後該指令可能再次被執行;如果某資料被訪問,則不久之後該資料可能再次被訪問。空間區域性性是指一旦程式訪問了某個儲存單元,則不久之後。其附近的儲存單元也將被訪問。”在實際工作中,CPU先向快取區讀取資料,如果快取區已存在,則讀取快取中的資料(命中),否則(失效),將記憶體中相應資料塊載入快取中,以提高接下來的訪問速度。由於成本和CPU大小的限制,CPU只能提供有限的快取區,因而快取區的大小是衡量CPU效能的重要指標之一。


使用快取,在CPU向記憶體更新資料時需要處理一個問題(寫回策略問題),即CPU在更新資料時只更新快取的資料(write back,寫回,當快取需要被替換時才將快取中更新的值寫回記憶體),還是CPU在更新資料時同時更新快取中和記憶體中的資料(write through,寫通)。在寫回策略中,為了減少記憶體寫操作,快取塊通常還設有一個髒位(dirty bit),用以標識該塊在被載入之後是否發生過更新。如果一個快取塊在被置換回記憶體之前從未被寫入過,則可以免去回寫操作;寫回的優點是節省了大量的寫操作。這主要是因為,對一個數據塊內不同單元的更新僅需一次寫操作即可完成。這種記憶體頻寬上的節省進一步降低了能耗,因此頗適用於嵌入式系統。寫通策略由於要經常和記憶體互動(有些CPU設計會在中間提供寫緩衝器以緩解效能),因而效能較差,但是它實現簡單,而且能簡單的維持資料一致性。


在軟體的快取系統中,一般是為了解決記憶體的訪問速率和磁碟、網路、資料庫(屬於磁碟或網路訪問,單獨列出來因為它的應用比較廣泛)等訪問速率不匹配的問題(對於記憶體快取系統來說)。但是由於記憶體大小和成本的限制,我們又不能把所有的資料先載入進記憶體來。因而如CPU中的快取,我們只能先將一部分資料儲存在快取中。此時,對於快取,我們一般要解決如下需求:
  1. 使用給定Key從Cache中讀取Value值。CPU是通過記憶體地址來定位記憶體已獲取相應記憶體中的值,類似的在軟體Cache中,需要通過某個Key值來標識相關的值。因而可以簡單的認為軟體中的Cache是一個儲存鍵值對的Map,比如Gemfire中的Region就繼承自Map,只是Cache的實現更加複雜。
  2. 當給定的Key在當前Cache不存在時,程式設計師可以通過指定相應的邏輯從其他源(如資料庫、網路等源)中載入該Key對應的Value值,同時將該值返回。在CPU中,基於程式區域性性原理,一般是預設的載入接下來的一段記憶體塊,然而在軟體中,不同的需求有不同的載入邏輯,因而需要使用者自己指定對應的載入邏輯,而且一般來說也很難預知接下來要讀取的資料,所以只能一次只加載一條紀錄(對可預知的場景下當然可以批量載入資料,只是此時需要權衡當前操作的響應時間問題)。
  3. 可以向Cache中寫入Key-Value鍵值對(新增的紀錄或對原有的鍵值對的更新)。就像CPU的寫回策略中有寫回和寫通策略,有些Cache系統提供了寫通介面。如果沒有提供寫通介面,程式設計師需要額外的邏輯處理寫通策略。也可以如CPU中的Cache一樣,只當相應的鍵值對移出Cache以後,再將值寫回到資料來源,可以提供一個標記位以決定要不要寫回(不過感覺這種實現比較複雜,程式碼的的耦合度也比較高,如果為提升寫的速度,採用非同步寫回即可,為防止資料丟失,可以使用Queue來儲存)。
  4. 將給定Key的鍵值對移出Cache(或給定多個Key以批量移除,甚至清除整個Cache)。
  5. 配置Cache的最大使用率,當Cache超過該使用率時,可配置溢位策略
    1. 直接移除溢位的鍵值對。在移除時決定是否要寫回已更新的資料到資料來源。
    2. 將溢位的溢位的鍵值對寫到磁碟中。在寫磁碟時需要解決如何序列化鍵值對,如何儲存序列化後的資料到磁碟中,如何佈局磁碟儲存,如何解決磁碟碎片問題,如何從磁碟中找回相應的鍵值對,如何讀取磁碟中的資料並方序列化,如何處理磁碟溢位等問題。
    3. 在溢位策略中,除了如何處理溢位的鍵值對問題,還需要處理如何選擇溢位的鍵值對問題。這有點類似記憶體的頁面置換演算法(其實記憶體也可以看作是對磁碟的Cache),一般使用的演算法有:先進先出(FIFO)、最近最少使用(LRU)、最少使用(LFU)、Clock置換(類LRU)、工作集等演算法。
  6. 對Cache中的鍵值對,可以配置其生存時間,以處理某些鍵值對在長時間不被使用,但是又沒能溢位的問題(因為溢位策略的選擇或者Cache沒有到溢位階段),以提前釋放記憶體。
  7. 對某些特定的鍵值對,我們希望它能一直留在記憶體中不被溢位,有些Cache系統提供PIN配置(動態或靜態),以確保該鍵值對不會被溢位。
  8. 提供Cache狀態、命中率等統計資訊,如磁碟大小、Cache大小、平均查詢時間、每秒查詢次數、記憶體命中次數、磁碟命中次數等資訊。
  9. 提供註冊Cache相關的事件處理器,如Cache的建立、Cache的銷燬、一條鍵值對的新增、一條鍵值對的更新、鍵值對溢位等事件。
  10. 由於引入Cache的目的就是為了提升程式的讀寫效能,而且一般Cache都需要在多執行緒環境下工作,因而在實現時一般需要保證執行緒安全,以及提供高效的讀寫效能。
在Java中,Map是最簡單的Cache,為了高效的在多執行緒環境中使用,可以使用ConcurrentHashMap,這也正是我之前參與的一個專案中最開始的實現(後來引入EHCache)。為了語意更加清晰、保持介面的簡單,下面我實現了一個基於Map的最簡單的Cache系統,用以演示Cache的基本使用方式。使用者可以向它提供資料、查詢資料、判斷給定Key的存在性、移除給定的Key(s)、清除整個Cache等操作。以下是Cache的介面定義。
public interface Cache<K, V> {
    public String getName();
    public V get(K key);
    public Map<? extends K, ? extends V> getAll(Iterator<? extends K> keys);
    public boolean isPresent(K key);
    public void put(K key, V value);
    public void putAll(Map<? extends K, ? extends V> entries);
    public void invalidate(K key);
    public void invalidateAll(Iterator<? extends K> keys);
    public void invalidateAll();
    public boolean isEmpty();
    public int size();
    public void clear();
    public Map<? extends K, ? extends V> asMap();
} 這個簡單的Cache實現只是對HashMap的封裝,之所以選擇HashMap而不是ConcurrentHashMap是因為在ConcurrentHashMap無法實現getAll()方法;並且這裡所有的操作我都加鎖了,因而也不需要ConcurrentHashMap來保證執行緒安全問題;為了提升效能,我使用了讀寫鎖,以提升併發查詢效能。因為程式碼比較簡單,所以把所有程式碼都貼上了(懶得整理了。。。。)。
public class CacheImpl<K, V> implements Cache<K, V> {
    private final String name;
    private final HashMap<K, V> cache;
    private final ReadWriteLock lock = new ReentrantReadWriteLock();
    private final Lock readLock = lock.readLock();
    private final Lock writeLock = lock.writeLock();
    
    public CacheImpl(String name) {
        this.name = name;
        cache = new HashMap<K, V>();
    }
    
    public CacheImpl(String name, int initialCapacity) {
        this.name = name;
        cache = new HashMap<K, V>(initialCapacity);
    }
    
    public String getName() {
        return name;
    }

    public V get(K key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    public Map<? extends K, ? extends V> getAll(Iterator<? extends K> keys) {
        readLock.lock();
        try {
            Map<K, V> map = new HashMap<K, V>();
            List<K> noEntryKeys = new ArrayList<K>();
            while(keys.hasNext()) {
                K key = keys.next();
                if(isPresent(key)) {
                    map.put(key, cache.get(key));
                } else {
                    noEntryKeys.add(key);
                }
            }
            
            if(!noEntryKeys.isEmpty()) {
                throw new CacheEntriesNotExistException(this, noEntryKeys);
            }
            
            return map;
        } finally {
            readLock.unlock();
        }
    }

    public boolean isPresent(K key) {
        readLock.lock();
        try {
            return cache.containsKey(key);
        } finally {
            readLock.unlock();
        }
    }

    public void put(K key, V value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    public void putAll(Map<? extends K, ? extends V> entries) {
        writeLock.lock();
        try {
            cache.putAll(entries);
        } finally {
            writeLock.unlock();
        }
    }

    public void invalidate(K key) {
        writeLock.lock();
        try {
            if(!isPresent(key)) {
                throw new CacheEntryNotExistsException(this, key);
            }
            cache.remove(key);
        } finally {
            writeLock.unlock();
        }
    }

    public void invalidateAll(Iterator<? extends K> keys) {
        writeLock.lock();
        try {
            List<K> noEntryKeys = new ArrayList<K>();
            while(keys.hasNext()) {
                K key = keys.next();
                if(!isPresent(key)) {
                    noEntryKeys.add(key);
                }
            }
            if(!noEntryKeys.isEmpty()) {
                throw new CacheEntriesNotExistException(this, noEntryKeys);
            }
            
            while(keys.hasNext()) {
                K key = keys.next();
                invalidate(key);
            }
        } finally {
            writeLock.unlock();
        }
    }

    public void invalidateAll() {
        writeLock.lock();
        try {
            cache.clear();
        } finally {
            writeLock.unlock();
        }
    }

    public int size() {
        readLock.lock();
        try {
            return cache.size();
        } finally {
            readLock.unlock();
        }
    }

    public void clear() {
        writeLock.lock();
        try {
            cache.clear();
        } finally {
            writeLock.unlock();
        }
    }

    public Map<? extends K, ? extends V> asMap() {
        readLock.lock();
        try {
            return new ConcurrentHashMap<K, V>(cache);
        } finally {
            readLock.unlock();
        }
    }

    public boolean isEmpty() {
        readLock.lock();
        try {
            return cache.isEmpty();
        } finally {
            readLock.unlock();
        }
    }

} 其簡單的使用用例如下: 
    @Test
    public void testCacheSimpleUsage() {
        Book uml = bookFactory.createUMLDistilled();
        Book derivatives = bookFactory.createDerivatives();
        
        String umlBookISBN = uml.getIsbn();
        String derivativesBookISBN = derivatives.getIsbn();
        
        Cache<String, Book> cache = cacheFactory.create("book-cache");
        cache.put(umlBookISBN, uml);
        cache.put(derivativesBookISBN, derivatives);
        
        Book fetchedBackUml = cache.get(umlBookISBN);
        System.out.println(fetchedBackUml);
        
        Book fetchedBackDerivatives = cache.get(derivativesBookISBN);
        System.out.println(fetchedBackDerivatives);
    }