前記:最近公司在做的項目完全基于Cache(Gemfire)構(gòu)建了一個類數(shù)據(jù)庫的系統(tǒng),自己做的一個小項目里用過Guava的Cache,以前做過的項目中使用過EHCache,既然和Cache那么有緣,那就趁這個機會好好研究一下Java中的Cache庫。在Java社區(qū)中已經(jīng)提供了很多Cache庫實現(xiàn),具體可以參考http://www.open-open.com/13.htm,這里只關(guān)注自己用到的幾個Cache庫而且這幾個庫都比較具有代表性:Guava中提供的Cache是基于單JVM的簡單實現(xiàn);EHCache出自Hibernate,也是基于單JVM的實現(xiàn),是對單JVM Cache比較完善的實現(xiàn);而Gemfire則提供了對分布式Cache的完善實現(xiàn)。這一系列的文章主要關(guān)注在這幾個Cache系統(tǒng)的實現(xiàn)上,因而步探討關(guān)于Cache的好處、何時用Cache等問題,由于他們都是基于內(nèi)存的Cache,因而也僅局限于這種類型的Cache(說實話,我不知道有沒有其他的Cache系統(tǒng),比如基于文件?囧)。
記得我最早接觸Cache是在大學(xué)學(xué)計算機組成原理的時候,由于CPU的速度要遠大于內(nèi)存的讀取速度,為了提高CPU的效率,CPU會在內(nèi)部提供緩存區(qū),該緩存區(qū)的讀取速度和CPU的處理速度類似,CPU可以直接從緩存區(qū)中讀取數(shù)據(jù),從而解決CPU的處理速度和內(nèi)存讀取速度不匹配的問題。緩存之所以能解決這個問題是基于程序的局部性原理,即”程序在執(zhí)行時呈現(xiàn)出局部性規(guī)律,即在一段時間內(nèi),整個程序的執(zhí)行僅限于程序中的某一部分。相應(yīng)地,執(zhí)行所訪問的存儲空間也局限于某個內(nèi)存區(qū)域。局部性原理又表現(xiàn)為:時間局部性和空間局部性。時間局部性是指如果程序中的某條指令一旦執(zhí)行,則不久之后該指令可能再次被執(zhí)行;如果某數(shù)據(jù)被訪問,則不久之后該數(shù)據(jù)可能再次被訪問。空間局部性是指一旦程序訪問了某個存儲單元,則不久之后。其附近的存儲單元也將被訪問。”在實際工作中,CPU先向緩存區(qū)讀取數(shù)據(jù),如果緩存區(qū)已存在,則讀取緩存中的數(shù)據(jù)(命中),否則(失效),將內(nèi)存中相應(yīng)數(shù)據(jù)塊載入緩存中,以提高接下來的訪問速度。由于成本和CPU大小的限制,CPU只能提供有限的緩存區(qū),因而緩存區(qū)的大小是衡量CPU性能的重要指標之一。
使用緩存,在CPU向內(nèi)存更新數(shù)據(jù)時需要處理一個問題(寫回策略問題),即CPU在更新數(shù)據(jù)時只更新緩存的數(shù)據(jù)(write back,寫回,當緩存需要被替換時才將緩存中更新的值寫回內(nèi)存),還是CPU在更新數(shù)據(jù)時同時更新緩存中和內(nèi)存中的數(shù)據(jù)(write through,寫通)。在寫回策略中,為了減少內(nèi)存寫操作,緩存塊通常還設(shè)有一個臟位(dirty bit),用以標識該塊在被載入之后是否發(fā)生過更新。如果一個緩存塊在被置換回內(nèi)存之前從未被寫入過,則可以免去回寫操作;寫回的優(yōu)點是節(jié)省了大量的寫操作。這主要是因為,對一個數(shù)據(jù)塊內(nèi)不同單元的更新僅需一次寫操作即可完成。這種內(nèi)存帶寬上的節(jié)省進一步降低了能耗,因此頗適用于嵌入式系統(tǒng)。寫通策略由于要經(jīng)常和內(nèi)存交互(有些CPU設(shè)計會在中間提供寫緩沖器以緩解性能),因而性能較差,但是它實現(xiàn)簡單,而且能簡單的維持數(shù)據(jù)一致性。
在軟件的緩存系統(tǒng)中,一般是為了解決內(nèi)存的訪問速率和磁盤、網(wǎng)絡(luò)、數(shù)據(jù)庫(屬于磁盤或網(wǎng)絡(luò)訪問,單獨列出來因為它的應(yīng)用比較廣泛)等訪問速率不匹配的問題(對于內(nèi)存緩存系統(tǒng)來說)。但是由于內(nèi)存大小和成本的限制,我們又不能把所有的數(shù)據(jù)先加載進內(nèi)存來。因而如CPU中的緩存,我們只能先將一部分數(shù)據(jù)保存在緩存中。此時,對于緩存,我們一般要解決如下需求:
- 使用給定Key從Cache中讀取Value值。CPU是通過內(nèi)存地址來定位內(nèi)存已獲取相應(yīng)內(nèi)存中的值,類似的在軟件Cache中,需要通過某個Key值來標識相關(guān)的值。因而可以簡單的認為軟件中的Cache是一個存儲鍵值對的Map,比如Gemfire中的Region就繼承自Map,只是Cache的實現(xiàn)更加復(fù)雜。
- 當給定的Key在當前Cache不存在時,程序員可以通過指定相應(yīng)的邏輯從其他源(如數(shù)據(jù)庫、網(wǎng)絡(luò)等源)中加載該Key對應(yīng)的Value值,同時將該值返回。在CPU中,基于程序局部性原理,一般是默認的加載接下來的一段內(nèi)存塊,然而在軟件中,不同的需求有不同的加載邏輯,因而需要用戶自己指定對應(yīng)的加載邏輯,而且一般來說也很難預(yù)知接下來要讀取的數(shù)據(jù),所以只能一次只加載一條紀錄(對可預(yù)知的場景下當然可以批量加載數(shù)據(jù),只是此時需要權(quán)衡當前操作的響應(yīng)時間問題)。
- 可以向Cache中寫入Key-Value鍵值對(新增的紀錄或?qū)υ械逆I值對的更新)。就像CPU的寫回策略中有寫回和寫通策略,有些Cache系統(tǒng)提供了寫通接口。如果沒有提供寫通接口,程序員需要額外的邏輯處理寫通策略。也可以如CPU中的Cache一樣,只當相應(yīng)的鍵值對移出Cache以后,再將值寫回到數(shù)據(jù)源,可以提供一個標記位以決定要不要寫回(不過感覺這種實現(xiàn)比較復(fù)雜,代碼的的耦合度也比較高,如果為提升寫的速度,采用異步寫回即可,為防止數(shù)據(jù)丟失,可以使用Queue來存儲)。
- 將給定Key的鍵值對移出Cache(或給定多個Key以批量移除,甚至清除整個Cache)。
- 配置Cache的最大使用率,當Cache超過該使用率時,可配置溢出策略
- 直接移除溢出的鍵值對。在移除時決定是否要寫回已更新的數(shù)據(jù)到數(shù)據(jù)源。
- 將溢出的溢出的鍵值對寫到磁盤中。在寫磁盤時需要解決如何序列化鍵值對,如何存儲序列化后的數(shù)據(jù)到磁盤中,如何布局磁盤存儲,如何解決磁盤碎片問題,如何從磁盤中找回相應(yīng)的鍵值對,如何讀取磁盤中的數(shù)據(jù)并方序列化,如何處理磁盤溢出等問題。
- 在溢出策略中,除了如何處理溢出的鍵值對問題,還需要處理如何選擇溢出的鍵值對問題。這有點類似內(nèi)存的頁面置換算法(其實內(nèi)存也可以看作是對磁盤的Cache),一般使用的算法有:先進先出(FIFO)、最近最少使用(LRU)、最少使用(LFU)、Clock置換(類LRU)、工作集等算法。
- 對Cache中的鍵值對,可以配置其生存時間,以處理某些鍵值對在長時間不被使用,但是又沒能溢出的問題(因為溢出策略的選擇或者Cache沒有到溢出階段),以提前釋放內(nèi)存。
- 對某些特定的鍵值對,我們希望它能一直留在內(nèi)存中不被溢出,有些Cache系統(tǒng)提供PIN配置(動態(tài)或靜態(tài)),以確保該鍵值對不會被溢出。
- 提供Cache狀態(tài)、命中率等統(tǒng)計信息,如磁盤大小、Cache大小、平均查詢時間、每秒查詢次數(shù)、內(nèi)存命中次數(shù)、磁盤命中次數(shù)等信息。
- 提供注冊Cache相關(guān)的事件處理器,如Cache的創(chuàng)建、Cache的銷毀、一條鍵值對的添加、一條鍵值對的更新、鍵值對溢出等事件。
- 由于引入Cache的目的就是為了提升程序的讀寫性能,而且一般Cache都需要在多線程環(huán)境下工作,因而在實現(xiàn)時一般需要保證線程安全,以及提供高效的讀寫性能。
在Java中,Map是最簡單的Cache,為了高效的在多線程環(huán)境中使用,可以使用ConcurrentHashMap,這也正是我之前參與的一個項目中最開始的實現(xiàn)(后來引入EHCache)。為了語意更加清晰、保持接口的簡單,下面我實現(xiàn)了一個基于Map的最簡單的Cache系統(tǒng),用以演示Cache的基本使用方式。用戶可以向它提供數(shù)據(jù)、查詢數(shù)據(jù)、判斷給定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實現(xiàn)只是對HashMap的封裝,之所以選擇HashMap而不是ConcurrentHashMap是因為在ConcurrentHashMap無法實現(xiàn)getAll()方法;并且這里所有的操作我都加鎖了,因而也不需要ConcurrentHashMap來保證線程安全問題;為了提升性能,我使用了讀寫鎖,以提升并發(fā)查詢性能。因為代碼比較簡單,所以把所有代碼都貼上了(懶得整理了。。。。)。
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);
}
posted on 2013-10-15 23:46
DLevin 閱讀(15755)
評論(1) 編輯 收藏 所屬分類:
CodeTools 、
EHCache