由于近期的工作主要集中在資料處理上,而性能問題時而暴露出來,我對需要處理的資料進行了一下簡單的分析,發現存在大量的重複資料,這自然讓我想到了去建立一個二級緩存把曾經處理過的資料緩存起來,避免重複處理。我們業務上其實就是對最近處理過的資料重複出現幾率比較高,是以有一個幾百兆的記憶體空間用LRU的政策進行去重應該就足夠了。
其實可以選擇的方案有很多,初步篩選了一下,我決定在對Java支援度比較好且應用廣泛的OSCache和EHCache中選一個。上了官網一查,發現OSCache在幾年前就停止更新了,而EHCache則一直有公司在維護,是以自然標明了後者。
先讓同僚研究了一下,弄的是1.7.0的版本,這個版本比較好的是對其他包的依賴很少,很快就把自己的demo建立起來了。然後上官網下載下傳了最新的版本ehcache-core-2.3.1-distribution,裡面其實就多了兩個slf4j的包,原來的代碼一行沒動就可以運作起來了。這裡新版本加入的内容比較多,jar包就是1.7.0的3倍大,看了下官方文檔的說明,主要加入的就是對分布式的支援,當然還有很多新特性。新特性以後慢慢研究吧,我目前也用不到什麼進階功能,既然新版本使用起來也很友善,那就用這個好了。
這裡簡單解釋一下,我們原先想試一下他提供的FIFO和LRU的政策,結果剛開始測試輸出的結果和預期居然不一緻,官方文檔上也沒看到相應的解釋。經過反複測試,感覺他不是嚴格按照這個政策來,可能是算法有些問題吧。
另外補充一下參數設定的經驗。
- 對于存儲對象個數的設定:由于配置檔案中隻能指定maxElementsInMemory,這就會有可能存入的對象太多而超出VM的heap大小,當然你可以通過jvm參數增大heap大小,但這總還是有可能溢出。這裡可以把maxElementsInMemory值設定到一個比較安全的大小,自己預先測試一下最好。如果記憶體仍然存不下你需要存的對象個數,那麼可以開啟overflowToDisk來增加可以存儲的Element個數。這裡要注意一下,EHCache不會自動幫助你去把記憶體對象寫入到磁盤,當超過maxElementsInMemory程式會自動把更多的部分開始往硬碟寫,但是記憶體的對象其實并沒有清出去,這時需要手動使用Cache.flush()方法來把記憶體對象清出去。 對于是否需要用磁盤擴充緩存,這個還是根據自己應用判斷。
- 重建上一次運作的緩存:這個需求肯定比較普遍,我們當然不希望一旦程式退出,整個緩存就要重建了。開啟diskPersistent功能,隻要使用的是CacheManager單例模式,下一次啟動的時候就會調用上一次運作的緩存。比較麻煩的是寫入磁盤的時間還是要自己調用 Cache.flush()方法。如果僅僅考慮到程式重新開機的話,我建議這裡把diskStore寫入到一個ramfs,這樣性能就更高了,但重新開機電腦的話就不得不重建緩存了。
- 索引的建立:如果你測試的maxElementsOnDisk量比較大的話,我本地設定成100000,那麼你會在臨時檔案目錄下看到有index檔案,這個檔案顯然是對磁盤上的檔案建立索引,加快查詢速度。是以這個索引是否建立是EHCache自動幫你做的,你不用操心了。
- 寫入緩存的速度:在本地不斷調整參數,驚訝的發現有時寫入緩存的速度奇慢。因為開啟了 diskPersistent功能,程式運作時經常會卡在backOffIfDiskSpoolFull方法上,作用是If the disk store spool is full wait a short time to give it a chance to catch up。這個參數對應的是diskSpoolBufferSizeMB,預設是30MB,如果存儲的對象比較多,強烈建議調大,或者直接把 diskPersistent關掉。
- 緩存内容寫入磁盤的速度:如果你把maxElementsOnDisk參數配置的遠小于maxElementsInMemory值的話,你會發現速度又變得很慢,這是因為程式一直要去找到底哪些内容應該寫入磁盤。建議這 maxElementsOnDisk值的配置應該不小于 maxElementsInMemory,其實正常用應該也會這麼配置,隻是我測試無聊試了下出問題了。
- 記憶體Element數量不能控制:一旦開啟了 diskPersistent,驚訝的發現居然設定的 maxElementsInMemory無效了,記憶體的 Element數量一直在增長。反複測試,問題穩定重制,又找不到解釋,無奈之下給官方提了個bug,等答複吧,汗...
這裡還是給出測試代碼,主要用到的就是一個ehcache.xml的配置檔案,定義了Cache的具體政策。程式調用的時候非常友善,就是典型key/value的方式。
- <?xml version="1.0" encoding="UTF-8"?>
- <ehcache>
- <diskStore path="java.io.tmpdir" />
- <defaultCache
- maxElementsInMemory="10000"
- eternal="false"
- timeToIdleSeconds="120"
- timeToLiveSeconds="120"
- overflowToDisk="true"
- diskPersistent="false"
- diskExpiryThreadIntervalSeconds="120"
- memoryStoreEvictionPolicy="LRU"
- />
- <cache name="sample"
- maxElementsInMemory="5"
- maxElementsOnDisk = "5"
- eternal="false"
- timeToIdleSeconds="1440"
- timeToLiveSeconds="2880"
- overflowToDisk="false"
- memoryStoreEvictionPolicy="FIFO"
- statistics="true"
- </ehcache>
以下就是測試代碼:
- package ehcache;
- import org.apache.log4j.Logger;
- import net.sf.ehcache.Cache;
- import net.sf.ehcache.CacheManager;
- import net.sf.ehcache.Element;
- public class EhcacheTest {
- private static final Logger logger = Logger.getLogger(EhcacheTest.class);
- private static Cache sampleCache = null;
- public static void main(String[] args) {
- init();
- test();
- }
- private static void test() {
- logger.info(sampleCache.getMemoryStoreEvictionPolicy());
- for(int i=0; i<10; i++){
- //寫入緩存
- sampleCache.put(new Element(i, "v" + i));
- //列印目前緩存的所有值
- logger.info(sampleCache.getKeys());
- //讀取緩存
- Element e = sampleCache.get(i);
- logger.info(e.getValue());
- }
- //列印命中統計
- logger.info(sampleCache.getStatistics());
- private static void init() {
- CacheManager manager = CacheManager.create();
- // manager.addCache("sample"); //已經在配置檔案定義過了
- sampleCache = manager.getCache("sample");
- }