首先,介紹一下我(作者)自己使用Cache的背景,以便讀者更清楚地瞭解我下面要講述哪些內容。
我主要是一個Cache實現者,而不是使用者。為了給一些ORM(比如JPA實現)提供Cache支援,我需要封裝其它的Open Source Cache,並考察它們的特性。
我對這些Open Source Cache的一些工作原理,瞭解得比較多。具體配置和使用細節,瞭解的比較少。
本文主要講述的也是Cache的特性和工作原理,而不是一個安裝、配置、使用的入門手冊。
本文簡述Cache的一般特性,詳述Cache的進階特性,比如,分布式Cache,關聯對象的Cache,POJO Cache等。
閱讀本文需要具備基本的Cluster知識,ORM知識,資料庫事務知識。本文不解釋這些基本概念。
-------------------------------------------------------
Cache Features
首先,我們來瀏覽一下常見的Cache。
這個連結給出了常用的Java Open Source Cache。
http://java-source.net/open-source/cache-solutions
memcached,JBoss Cache,SwarmCache,OSCache,JCS,EHCache等開源項目的出鏡率和關注率比較高。
memcached和其他幾個不同,後面會詳述。
JBoss Cache的特點是,功能大而全,可算是Cache集大成者,幾乎什麼都支援。
其餘的幾個都很輕量。SwarmCache,OSCache,JCS支援Cluster。EHCache不支援Cluster。
下面列出Cache的基本特性。
1. 時間記錄
資料進入Cache的時間。
2. timeout到期時間
Cache裡面的資料多久到期
3. Eviction Policy 清除策略
Cache滿了之後,根據什麼策略,應該清除哪些資料。
比如,最不經常被訪問的資料,最久沒有訪問到的資料。
4. 命中率
Cache的資料被選中的比率
5. 分級Cache
有些Cache有分級的概念。比如,幾乎所有的Cache都支援Region分區的概念。可以指定某一類的資料存放在特定的Region裡面。JBoss Cache可以支援更多的層級。
6. 分布式Cache
分布在不同電腦上的Cache
7. 鎖,事務,資料同步
一些Cache提供了完善的鎖,事務支援。
以上特性,大部分Cache都有相應的API支援。這些API很直觀,也很簡單,本文不打算展開講述。
本文下面主要介紹,memcached和JBoss Cache這兩個具有代表意義的Cache的進階特性,包括分布式Cache的支援。
-------------------------------------------------------
memcached
http://www.danga.com/memcached/
memcached是一個Client Server結構的遠程Cache實現。
Server是用C寫的,提供了多種語言的用戶端API,包括Java, C#, Ruby, Python, PHP, Perl, C等多種語言。
memcached主要使用在Shared Nothing Architecture中。應用程式通過用戶端API,從memcached server存取資料。
典型的應用,比如,用memcached作為資料庫緩衝。
也常有這樣的用法,用memcached存放HTTP Session的資料。具體做法是封裝Session Interface,截獲setAttribute(), getAttribute()方法。
MemcachedSessionWrapper {
Object getAttribute( key ){
return memcachedClient.get (session.getId() + key);
}
void setAttribute( key, value ){
memcachedClient.setObject(session.getId() + key, value);
}
}
不同電腦上的應用程式通過一個IP地址來訪問memcahced Server。
同一個key對應的資料,只存在於一台memcached server的一份記憶體中。
memcached server也可以部署在多台電腦上。Memcached通過key的hashcode來判斷從哪台memcached server上存取資料資料。我們可以看到,同一個key對應的資料,還是只存在於一台memcached server的一份記憶體中。
所以,memcached不存在資料同步的問題。這個特性很關鍵,我們後面講到Cluster Cache的時候,就會涉及到資料同步的問題。
memcached由於是遠程Cache,要求放到Cache的Key和Value都是Serializable。
遠程Cache,最令人擔心的網路通訊開銷。據有經驗的人說,memcached網路通訊開銷很小。
memcached的API設計也是遠程通訊友好的,提供了getMulti()等高粒度的調用方法,能夠批量擷取資料,從而減少網路通訊次數。
-------------------------------------------------------
JBoss Cache
http://www.jboss.org/products/jbosscache
有一個商業Cluster Cache,叫做tangosol。
JBoss Cache是我唯一知道的能夠和tangosol媲美的開源Cache。
Cluster Cache的資料同步,需要網路通訊,這就要求放到Cache的資料是Serializable。
JBoss Cache提出了POJO Cache的概念,意思是資料不是Serializable,一樣能夠在Cluster中同步。
JBoss POJO Cache通過AOP機制,支援對象同步,支援對象屬性的同步,支援關聯對象的Cache,支援繼承,集合,Query,並支援不同層級的事務,儼然一個小型記憶體資料庫層級的資料存放區系統。
下面進行解釋。
最令人迷惑不解的是這個POJO的Cluster同步如何?。
JBoss POJO Cache採用AOP來照管了POJO的通訊和傳播工作。天下沒有免費的午餐,POJO不支援序列化,架構本身就要做這個工作——Marshal and Unmarshal,比如通過把Java對象翻譯成XML,傳播出去,對方收到XML,再翻譯成Java對象。
上面說了,JBoss POJO Cache很像一個小型儲存容器,JBoss POJO Cache的對象管理也非常類似Hibernate,JDO,JPA等ORM工具,同樣有Detach和Attach的概念。
Attach就是put,把對象放入到Cache中。Detach就是remove,把對象從Cache中刪除。為啥要多起個名?
原因是,put的時候,放進去的是個乾乾淨淨的POJO,出來的時候,就是Enhanced Object,裡面夾雜了很多Interceptor代碼,監聽對象的方法。
你操作這個對象的時候,JBoss AOP架構就獲得了相應的通知,能夠做出相應的反應,比如資料同步等。
JBoss POJO Cache支援對集合類型的AOP。同樣需要把集合Attach(Put)進Cache,然後get出來,然後對集合進行操作,就可以被JBoss AOP截獲了。
JBoss POJO Cache的基礎是JBoss Tree Cache。這個Tree Cache類似於一個XML DOM樹形資料結構。
JBoss Cache採用Full Qualified Name作為Cache Key,類似於XPath。比如,a/b/c/d。
當你刪除a/b的時候,a/b/c,a/b/c/d等所有屬於a/b的Key和對應資料,都被刪除。
JBoss Cache的findObjects方法能夠找出一串對象。比如,findObjects根據a/b/c/d能夠找出a,b,c,d等4個對象,放在一個Map中返回。
具體用法要參見API詳細說明,因為JBoss POJO Cache提供了很多行為模式。
這 種分級的Cache功能很有用,實現起來也不難。只是,我覺得,還是不夠強大。既然支援了類似於XPath的Key,不如索性支援XPath的條件查詢。 比如,a[name=”n”]/b/c。當然,實現這種功能的代價非常大,需要遍曆整個Cache Tree,正如XPath需要遍曆整個DOM節點一樣。
最後,JBoss Cache和tangosol一樣,都支援了一個我認為如同雞肋一般的功能,鎖機制和事務支援。這個事務支援的意思是,Cache本身實現了類似於資料庫的4種交易隔離等級。
在我看來,這種支援無疑是為了賺取眼球。Cache不當做Cache來用,搞些歪門邪道,大而不當。想當作資料庫來用,那還不如把主要功夫花在上述提到的那種精確批量查詢功能上。
-------------------------------------------------------
Cluster同步
Cluster 之間的Cache同步有多種實現方法。比如,JMS,RMI,Client Server Socket等方法,用的最多的,支援最廣的方法是JGroups開源項目實現的Multicast。配置Cluster Cache,通常就相當於配置JGroups,需要閱讀JGroups配置文檔。
Cache的操作通常有4個,get,put,remove,clear。
對 於Cluster Cache來說,讀操作(get)肯定是Local方法,只需要從本台電腦記憶體中擷取資料。Remove/clear兩個寫操作,肯定是Remote方 法,需要和Cluster其他電腦進行同步。Put這個寫方法,可以是Local,也可以是Remote的。
Remote Put方法的情境是這樣,一台電腦把資料放到Cache裡面,這個資料就會被傳播到Cluster其他電腦上。這個做法的好處是Cluster各台電腦的Cache資料可以及時得到補充,壞處是傳播的資料量比較大,這個代價比較大
Local Put方法的情境是這樣,一台電腦把資料放到Cache裡面,這個資料不會被傳播到Cluster其他電腦上。這個做法的好處是不需要傳播資料,壞處 是Cluster各台電腦的Cache資料不能及時得到補充,這個不是很明顯的問題,從Cache中得不到資料,從資料庫擷取資料是很正常的現象。
Local Put比起Remote Put的優勢很明顯,所以,通常的Cluster Cache都採用Local Put的策略。各Cache一般都提供了Local Put的配置選項,如果你沒有看到這個支援,那麼請換一個Cache。
-------------------------------------------------------
Center vs Cluster
Memcached可以看作是Center Cache。
Center Cache和Cluster Cache的特性比較如下:
Center Cache沒有同步問題,所以,remove/clear的時候,比較有優勢,不需要把通知發送到好幾個電腦上。
但是,Center Cache的所有操作,get/put/remove/clear都是Remote操作。而Cluster Cache的get/put都是Local操作,所以,Cluster Cache在get/put操作上具有優勢。
Local get/put在關聯對象的組裝和分拆方面,優勢比較明顯。
關聯對象的分拆是這個意思。
比如,有一個Topic對象,下面有幾個Post對象,每個Post對象都有一個User對象。
Topic對象存放到Cache中的時候,下面的關聯對象都要拆開來,分成各自的Entity Region來存放。
Topic Region -> Topic ID -> Topic Object
Post Region -> Post ID -> Post Object
User Region -> User ID -> User Object
這個時候,put的動作可能發生多次。Remote Put的開銷就比較大。
Get的過程類似,也需要get多次,才能拼裝成一個完整的Topic對象。
-------------------------------------------------------
到期資料
Cache可以用在任何地方,比如,頁面緩衝。但Cache的最常用情境是用在ORM中,比如,Hibernate,JDO,JPA中。
ORM Cache的使用方法有個原則——不要把沒有Commit的修改資料放入到緩衝中。這是為了防止Read Dirty。
資料庫事務分為兩種,一種是讀事務,不修改資料,一種是寫事務,修改資料。
寫事務的操作流程如下:
db.commt();
cache.remove(key); // 這一步操作,清除了Cache資料,也記錄了一個時間removeTime。
讀事務的操作流程如下:
readTime = current time;
data = cache.get(key);
if(data is null){
data = db.load(key);
cache.put(key, data, readTime); // 這裡要readTime傳進去
}
這裡需要注意的是put的時候,需要readTime這個參數。
這個readTime要和上一次的removeTime進行比較。
如果readTime > removeTime,這個put才能成功,資料才能夠進入緩衝。
這是為了保證不把到期資料放入到Cache中,及時反映資料庫的變化。
另外,需要注意的是,cache.remove(key); 這個事件需要傳播到Cluster其他電腦,通知它們清理緩衝。
為什麼需要這個通知?
一定要注意,這不是為了避免並發修改衝突。並發修改衝突的避免需要引入樂觀鎖版本控制機制。
有可能存在這樣的誤解,認為有了樂觀鎖版本控制機制,就不需要Cache.remove通知了。這是不對的。
Cache.remove通知的主要目的是,保證緩衝能夠及時清理到期資料,反映資料的變化,保證大部分時間內,應用程式顯示給使用者的不是到期資料。
另外,db.commt(); cache.remove(key); 這兩步調用之間,有很小的可能發生另外的事務。這段極小的時間內,可能無法保證Read Committed,可能出現很短期的到期資料。
為什麼說很短期,因為緊接著的Cache.remove就會清理到期資料。
如果偏執到這種程度,這麼短期的幾乎不可能發生的小機率事件,都不能容忍,那麼可以,db.commt()之前,給Cache加一個悲觀鎖,不讓別的事務,把資料Put進入Cache,就可以防止這個小機率、微影響的事件。
JBoss Cache和Tangosol就提供了這類雞肋一般的悲觀鎖機制。典型的開發資源配置不當,有用的需要的不做,沒用的功能使勁做。
ORM Query Cache
ORM Cache一般分為兩種。一種是ID Cache(ORM文檔中稱為二級Cache),用來存放Entity ID對應的Entity對象;一種是Query Cache,用來存放一條查詢語句對應的查詢結果集。
ID Cache非常直觀,如同上述講述的,一般是一個Entity Class對應一個Region,Entity存放到對應的Region裡面。
Query Cache比較複雜,而且潛在作用很大,值得仔細講解。
現有的ORM對Query Cache的支援並不是很理想。
比如,Hibernate把整個結果集直接放在Query Cache中。這樣,有任何風吹草動,發生了任何資料庫的寫操作,Query Cache都需要清空。
有 一種比較好的做法,把ID List存放在Query Cache中,每次擷取的時候,先擷取ID List,然後根據ID List擷取Entity List。Query Cache根據Query涉及到的Table Name來進行清理,一旦發生對這些Table Name的修改操作,就可以根據不同情況,清理Query Cache。
比如,select t2.* from t1, t2 where t1.id = t2.foreign_id and t1.name = ‘a’
那麼insert into t1, delete from t1, insert into t2, delete from t2都會清除這條Query Cache。
同樣的 update t1 set 這樣的語句也會清除這條Query Cache。
Hibernate為什麼不這麼做,因為Query Cache的情況比較複雜。也許選擇的結果集並不是只有一個Entity類型,也許只是幾個欄位。
這個地方,如果細分,還是有很多功夫可以做的。而且也很值得花功夫做,因為Query Cache對於效能的提高,有很大作用。
-------------------------------------------------------
ORM Query Cache
Cache可以用在任何地方,比如,頁面緩衝。但Cache的最常用情境是用在ORM中,比如,Hibernate,JDO,JPA中。
ORM Cache一般分為兩種。一種是ID Cache(ORM文檔中稱為二級Cache),用來存放Entity ID對應的Entity對象;一種是Query Cache,用來存放一條查詢語句對應的查詢結果集。
ID Cache非常直觀,如同上述講述的,一般是一個Entity Class對應一個Region,Entity存放到對應的Region裡面。
Query Cache比較複雜,而且潛在作用很大,值得仔細講解。
現有的ORM對Query Cache的支援並不是很理想。
比如,Hibernate把整個結果集直接放在Query Cache中。這樣,有任何風吹草動,發生了任何資料庫的寫操作,Query Cache都需要清空。
有 一種比較好的做法,把ID List存放在Query Cache中,每次擷取的時候,先擷取ID List,然後根據ID List擷取Entity List。Query Cache根據Query涉及到的Table Name來進行清理,一旦發生對這些Table Name的修改操作,就可以根據不同情況,清理Query Cache。
比如,select t2.* from t1, t2 where t1.id = t2.foreign_id and t1.name = ‘a’
那麼insert into t1, delete from t1, insert into t2, delete from t2都會清除這條Query Cache。
同樣的 update t1 set 這樣的語句也會清除這條Query Cache。
Hibernate為什麼不這麼做,因為Query Cache的情況比較複雜。也許選擇的結果集並不是只有一個Entity類型,也許只是幾個欄位。
這個地方,如果細分,還是有很多功夫可以做的。而且也很值得花功夫做,因為Query Cache對於效能的提高,有很大作用。
-----------------------------------------------------------
Query Key
Query Cache的效能需要考慮幾個方面。比如,Query Key。Query Key一般由2個部分組成:Query String部分,SQL, HQL, EQL, or OQL;參數部分。
尋找Query Key的對應資料的時候,Query Key的比較有兩個步驟,先hash,然後equals。所以,Query Key的hashcode和equals兩個方法很重要。尤其是equals方法。
equals 方法需要比較很長的Query String。如果沒有命中,Query String不相等,那麼開銷很小,因為通常來說,不相等的String長度都不同,或者前面的字串都不相同。開銷最大的是命中的時候,Query String相等,那麼需要把String從頭比到尾。
我們可以採取一些方法來提高String的比較速度。比如,大部分的情況屬於靜態查 詢,我們可以採用Singleton String。相同reference的String之間的比較速度很快。對於ORM來說,最好直接使用最外面的HQL, EQL, OQL作為Query Key,而不是採用產生的SQL結果。因為產生的SQL結果每次都是一個新String,具有不同的reference,Cache命中的時候,需要比較 整個字串。
動態拼裝的Query String的效能提高比較難辦。因為最終的結果,都是一個新String。我採用的一種方式是,動態拼裝的結果是一個String[]。兩個 String[]如果相等,那麼裡面的元素String的reference都是相等的,這是由JVM對一個Class內部的String常量進行最佳化的 結果。
比如,
String[] a = {
“select * from t where”
“a = 1”
“and b = 2”
};
String[] b = {
“select * from t where”
“a = 1”
“and b = 2”
};
那麼a和b的比較只需要3次String reference的比較
引用EHCache不支援Cluster
這個是不正確得,ehcache1.2之後就支援cluster了,
還有一點就是關於query cache描述也不是很正確,
1 query cache其實應該是一個很大得範疇,不應該只用在orm那一層,也不局限把sql語句作為key,orm裡這樣使用定義query cache只是情況之一。
2 查詢結果集也可以被緩衝。只有當經常使用同樣的參數進行查詢時,這才會有些用處。在查詢快取中,它並不緩衝結果集中所包含的實體的確切狀態;它只緩衝這些 實體的標識符屬性的值、以及各實值型別的結果。需要將列印sql語句與最近的cache內容相比較,將不同之處修改到cache中,所以查詢快取通常會和二 級緩衝一起使用。而query cache雖然能提高效能,但是按照hibernate in action的話來說適用情境較少。
而如果我們放大query cache的概念,那麼對於即時性要求不是很高的資料,它就能極大的提高效能,比如說首頁,如果首頁更新的時間為5分鐘,那麼query cache就能發揮其所長了。