JPress的ehcache緩存方案、以及踩過ehcache的坑...

最近爲了提升JPress的性能,減小數據查詢的次數,JPress大量使用了ehcache緩存做爲起內置緩存,同時session也是基於ehcache從新實現的支持分部署的session解決方案。java

由於JPress是基於JFinal快速開發框架,而JFinal又內置了ehcache的插件,使用起來及其簡單.web

一、JFinal裏配置ehcachePlugin插件。spring

public void configPlugin(Plugins me) {
        me.add(new EhCachePlugin());
        
        //添加其餘插件
}

二、在classPath下添加ehcache的配置文件ehcache.xmlsql

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="true">
         
    <diskStore path="java.io.tmpdir"/>
    
    <defaultCache
           maxEntriesLocalHeap="10000"
           eternal="false"
           overflowToDisk="true"
           timeToIdleSeconds="20"
           timeToLiveSeconds="60">
    </defaultCache>
 
    <!--
    Sample cache named sampleCache1
    This cache contains a maximum in memory of 10000 elements, and will expire
    an element if it is idle for more than 5 minutes and lives for more than
    10 minutes.
 
    If there are more than 10000 elements it will overflow to the
    disk cache, which in this configuration will go to wherever java.io.tmp is
    defined on your system. On a standard Linux system this will be /tmp"
    -->
    <cache name="sampleCache1"
           maxEntriesLocalHeap="10000"
           maxEntriesLocalDisk="1000"
           eternal="false"
           overflowToDisk="true"
           diskSpoolBufferSizeMB="20"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           memoryStoreEvictionPolicy="LFU"
           transactionalMode="off"
            />
            
    <!--
    Sample cache named sampleCache2
    This cache has a maximum of 1000 elements in memory. There is no overflow to disk, so 1000
    is also the maximum cache size. Note that when a cache is eternal, timeToLive and
    timeToIdle are not used and do not need to be specified.
    -->
    <cache name="sampleCache2"
           maxEntriesLocalHeap="1000"
           eternal="true"
           overflowToDisk="false"
           memoryStoreEvictionPolicy="FIFO"
            />
 
    <!--
    Sample cache named sampleCache3. This cache overflows to disk. The disk store is
    persistent between cache and VM restarts. The disk expiry thread interval is set to 10
    minutes, overriding the default of 2 minutes.
    -->
    <cache name="sampleCache3"
           maxEntriesLocalHeap="500"
           eternal="false"
           overflowToDisk="true"
           timeToIdleSeconds="300"
           timeToLiveSeconds="600"
           diskPersistent="true"
           diskExpiryThreadIntervalSeconds="1"
           memoryStoreEvictionPolicy="LFU"
            />
               
</ehcache>

三、直接使用EhcacheKit操做緩存。數據庫

public void yourMethod() {
       Cachekit.put("cacheName","key","value")
 
}

到此,一切很順利的進行着,但隨着JPress在大量的使用ehcache,ehcache的緩存數據操做與更新就變成了一個棘手的問題,更新數據庫數據了,緩存若得不到及時更新,就會致使程序在運行的過程當中有大量的bug、各類莫名其妙的問題。此時、緩存數據的更新,就須要一個良好更新的計劃和方案。編程

首先,是數據顆粒度的問題,咱們在緩存數據的時候,多是根據數據庫的ID,對單個model(單條數據)進行緩存,這種緩存以model的ID做爲key進行緩存,這種緩存的顆粒度極細。緩存

所以,咱們在作數據的更新的時候很是簡單,只需在model的更新和刪除的時候從ehcache刪除該ID便可。針對這一的問題,咱們只須要重寫Model的update和delete方法,刪除其緩存。tomcat

代碼以下:服務器

@Table(tableName = "content", primaryKey = "id")
public class Content extends BaseContent<Content> {
 
    private static final long serialVersionUID = 1L;
 
    @Override
    public boolean update() {
        removeCache(getId());//移除ehcache緩存
        return super.update();
    }
 
    @Override
    public boolean delete() {
        removeCache(getId());//移除ehcache緩存
        return super.delete();
    }
 
}

經過這種方式,咱們在經過ID來查詢該數據的時候,不用擔憂緩存於數據庫不一樣步的問題,由於咱們在更新、刪除的時候就已經把ehcache的緩存數據給清除掉了,當查詢的時候發現ehcache裏沒有數據,自動會去數據庫會獲取,從而保證了ehcache的數據與數據庫保持一致。session

可是,咱們在緩存數據的時候,不僅是對單個model進行緩存,在程序的各類業務場景中,大量會使用到列表的查詢,所以咱們在存儲的時候,確定也會多列表進行緩存。

例如:

public List<Content> findByModule(final String module, final BigInteger parentId, String orderby) {
        final StringBuilder sqlBuilder = new StringBuilder("select * from content c");
        sqlBuilder.append(" sql ....");
 
        return DAO.getFromListCache("cacheName","key", new IDataLoader() {
            @Override
            public Object load() {
                return DAO.find(sqlBuilder.toString(), module,parentId);
            }
        });
        
    }

可是,一旦緩存了列表,問題就來了?這個列表的數據何時會被更新呢?這個緩存到ehcache的某條數據可能會被隨時更新或刪除了,怎麼來同步?

一種粗糙的方案是:把全部緩存都緩存到同一個cacheName中,而後在model的update或delete的時候,對這個cacheName無論三七二十一直接所有清除。

以下代碼:

@Table(tableName = "content", primaryKey = "id")
public class Content extends BaseContent<Content> {
 
    private static final long serialVersionUID = 1L;
 
    @Override
    public boolean update() {
        removeCache(getId());//移除ehcache緩存
        removeAllListCache(); //移除全部保存列表數據的緩存
        return super.update();
    }
 
    @Override
    public boolean delete() {
        removeCache(getId());//移除ehcache緩存
         removeAllListCache(); //移除全部保存列表數據的緩存
        return super.delete(); 
    }
 
}

雖然這是一種粗糙的方案,可是也是有效解決了列表數據不一樣步的問題;其粗糙的緣由是,當咱們清除數據的時候,把全部的列表都刪除了,這樣會致使不少沒有沒有該列表的數據也被清楚了...

因此,更有效的解決方案應該是保留和該ID沒有關係的數據,而只清除有關的數據。

那問題來了,什麼數據纔是該ID有關的數據呢?

一、列表有該ID的數據。

    二、列表的排序等會受到該ID影響的數據,好比謀條數據的orderby_number更新了,可能某個緩存的列表數據雖然沒有該ID,可是該ID更新後,多是orderby_number,因爲緩存的列表數據是根據orderby_number來排序的,此時該數據應該出如今列表裏。

    三、分頁數據,好比某條數據被刪除或更新了,可能分頁的頁碼數據就會被改變。

那如何才能找到該ID關聯的數據呢?

這是一個困難的問題,每一個業務系統不同,關聯的數據確定也不同。在JPress裏,每一個content都有一個module字段,表示該數據所屬的模型。

所以,在JPress的內容分類裏,JPress針對某種類型的數據,都按照必定的規則來創建這個存儲的key,好比文章模型的列表在存儲的時候,存儲的key值大概爲:module:article-xxx-xxx這樣的key。

當文章模型的數據被更新的時候,會去便利全部列表數據的key,若是發現key是以module:article開頭,表示該數據是文章列表的緩存數據,應該清除。

因而,就有了以下的代碼:

public void removeAllListCache() {
        List<Object> list = CacheKit.getKeys(CACHE_NAME);
        if (list != null && list.size() > 0) {
            for (Object keyObj : list) {
                String keyStr = (String) keyObj;
 
                if (!keyStr.startsWith("module:")) {
                    CacheKit.remove(CACHE_NAME, keyStr);
                    continue;
                }
 
                // 不清除其餘模型的內容
                if (keyStr.startsWith("module:" + getModule())) {
                    CacheKit.remove(CACHE_NAME, keyStr);
                }
            }
        }
    }

大功告成,測試、運行。

然而,踩坑纔剛剛開始。

ehcache的坑1:getKeys("cacheName")爲空數據。

本覺得理想的解決了個人方案,興高采烈的查看測試結果,然而發現了一個致命的問題,更新或刪除單條數據後,緩存的列表數據沒有被更新,debug後才發現,經過CacheKit.getKeys("cacheName")獲得的數據老是不正確,絕大多數的狀況下返回了空列表,開始覺得是JFinal的問題,而後跟進源代碼後,JFinal根本沒有對getKeys進行任何的操做,而直接返回了。

在查詢資料的過程當中,也曾發如今oschina上有人提供類型的問題:http://www.oschina.net/questi... ,而後沒有一個較好的答覆。在spring的網站上(https://jira.spring.io/browse... 找到了這麼一句話。

Consider a cache with 100k items - if you ask for the keys, most likely you'll end up with an OOM.

大概意思是,若是保存了不少數據,當去獲取全部數據的keys的時候,可能會形成內存溢出。但不管如何,我始終以爲這是ehcache的一個大坑,若是ehcache的做者始終這麼考慮的話,徹底不用提供這個getKeys這個方法好了,爲毛還要提供出來呢?

那getKeys這條路行不通,那咱們就必須本身去維護這個keys。也就是本身來就來我記錄我存了哪些key。

因而,在保存到cache的時候,有了以下的代碼:

public List<Content> findByModule(final String module, final BigInteger parentId, String orderby) {
        final StringBuilder sqlBuilder = new StringBuilder("select * from content c");
        sqlBuilder.append(" sql ....");
 
        return DAO.getFromListCache("cacheName","key", new IDataLoader() {
            @Override
            public Object load() {
                return DAO.find(sqlBuilder.toString(), module,parentId);
            }
        });
        
    }
 
public <T> T getFromListCache(Object key, IDataLoader dataloader) {
        List<String> inCacheKeys = CacheKit.get(CACHE_NAME, "cachekeys");
 
        List<String> cacheKeyList = new ArrayList<String>();
        if (inCacheKeys != null) {
            cacheKeyList.addAll(inCacheKeys);
        }
 
        cacheKeyList.add(key.toString());
        CacheKit.put(CACHE_NAME, "cachekeys", cacheKeyList);
 
        return CacheKit.get("content_list", key, dataloader);
    }

在保存的時候,把keys所有保存到一個單獨的緩存裏面;

在刪除緩存的時候,不經過getKeys了,而是去這個緩存裏面查看有哪些key。

代碼以下:

public void removeAllListCache() {
        List<Object> list = CacheKit.get(CACHE_NAME, "cachekeys");
        if (list != null && list.size() > 0) {
            for (Object keyObj : list) {
                String keyStr = (String) keyObj;
 
                if (!keyStr.startsWith("module:")) {
                    CacheKit.remove("taxonomy_list", keyStr);
                    continue;
                }
 
                // 不清除其餘模型的內容
                if (keyStr.startsWith("module:" + getModule())) {
                    CacheKit.remove("taxonomy_list", keyStr);
                }
            }
        }
    }

到此,ehcache的getKeys坑總算是告了一個段落。

ehcache的坑2:存儲的list列表數據當心複用(或不能複用)。

大喜之餘,Ehcache的坑又接踵而來。在使用的過程當中,莫名其妙的不定時的出了一個錯誤....

net.sf.ehcache.CacheException: Failed to serialize element due to ConcurrentModificationException. This is frequently the result of inappropriately sharing thread unsafe object (eg. ArrayList, HashMap, etc) between threads
    at net.sf.ehcache.store.disk.DiskStorageFactory.serializeElement(DiskStorageFactory.java:405)
    at net.sf.ehcache.store.disk.DiskStorageFactory.write(DiskStorageFactory.java:385)
    at net.sf.ehcache.store.disk.DiskStorageFactory$DiskWriteTask.call(DiskStorageFactory.java:477)
    at net.sf.ehcache.store.disk.DiskStorageFactory$PersistentDiskWriteTask.call(DiskStorageFactory.java:1071)
    at net.sf.ehcache.store.disk.DiskStorageFactory$PersistentDiskWriteTask.call(DiskStorageFactory.java:1055)

一看,麻蛋!!多線程的問題啊...錯誤log沒有具體到我本身項目中的哪一行代碼,此項想到在JPress的設計中,因爲爲了解耦,JPress自行開發了一套消息機制,默認狀況下全是開闢新的線程去執行的...... 此時,想哭。

抽了根菸後,腦子中靈光乍現,不對啊,在tomcat對servlet的處理模型中,每一個請求其實都是開闢了新的線程去處理單獨的請求,每一個請求也都有可能對ehcache進行操做....不多是多線程的問題。

此時,已是深夜2點。

趕忙打開電腦,看看stackoverflow(一個國外知名的編程問答網站)上是否有有人遇到過相似的問題。通過半小時的檢索閱讀後,終於在http://stackoverflow.com/ques... 找到了蛛絲馬跡。

因爲咱們存儲到ehcache的數據列表多是一個list數據,此時的list數據可能還保存在內存裏,讀取的代碼以下:

public List<Content> findByModule(final String module, final BigInteger parentId, String orderby) {
        final StringBuilder sqlBuilder = new StringBuilder("select * from content c");
        sqlBuilder.append(" sql ....");
 
        return DAO.getFromListCache("cacheName","key", new IDataLoader() {
            @Override
            public Object load() {
                return DAO.find(sqlBuilder.toString(), module,parentId);
            }
        });
        
    }

在如上的代碼中,DAO.getFromListCache可能獲得的是內存裏的數據,然而調用這個方法的controller不少,每一個controller都有本身的業務邏輯,也就是說每一個controller都有可能對保存在ehcache內存裏的list進行操做(修改、刪除、添加),於是出現了 "net.sf.ehcache.CacheException: Failed to serialize element due to ConcurrentModificationException. This is frequently the result of inappropriately sharing thread unsafe object (eg. ArrayList, HashMap, etc) between threads" 這個錯誤。

若是真的是這樣,就好辦了....趕忙修改代碼測試。

public List<Content> findByModule(final String module, final BigInteger parentId, String orderby) {
        final StringBuilder sqlBuilder = new StringBuilder("select * from content c");
        sqlBuilder.append(" sql ... ");
 
        // 略...
        List<Content> data = DAO.getFromListCache(buildKey(module, parentId, orderby), new IDataLoader() {
            @Override
            public Object load() {
                return DAO.find(sqlBuilder.toString(), params.toArray());
            }
        });
        if (data == null)
            return null;
        
        return new ArrayList<Content>(data);
    }

若是可以從緩存中獲得數據,從新new一個新的list返回。

通過兩個小時的測試後,這個問題再也沒有出現。

在使用ehcache中,記得一個小夥伴又給我反饋了一個問題,就是在他的一臺服務器裏,部署了多個JPress,致使後來出現了ehcache數據重合的狀況,JPress應用A讀到了JPress應用B的緩存數據。

不開源、不知道,開源嚇一跳。

雖然是一個"小"問題,可是也很棘手,兩個應用同時使用了一份ehcache的數據,緣由就是ehcache把數據存儲到磁盤的時候,存儲到了同一個地方了,在ehcache的配置文件ehcache.xml中。

以下代碼:

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:noNamespaceSchemaLocation="ehcache.xsd"
         updateCheck="false" monitoring="autodetect"
         dynamicConfig="true">
         
    <diskStore path="java.io.tmpdir"/>
    
    <!--其餘略...-->
               
</ehcache>

在ehcache的配置中,diskStore都是指向了同一個地方,若是這個同窗在部署的時候本身修改了diskStore,指定到具體的位置就能夠。可是有沒有什麼辦法讓每一個JPress應用的ehcache緩存保存在本身應用的webRoot目錄下呢?這樣,就無需用戶本身去配置了。

趕忙去ehcache官網看看,怎麼配置diskStore,才能讓緩存保存在本身的webRoot目錄....

在ehcache的官方文檔:http://www.ehcache.org/genera...的第15頁中,找到了以下的內容:
圖片描述

能夠配置 user.home(用戶的家目錄)、user.dir(用戶當前的工做目錄)、java.io.tmpdir(默認的臨時目錄)、ehcache.disk.store.dir(ehcache的配置目錄)和具體的目錄,卻不能配置成webRoot的目錄....

因而,我想到了本身去加載這個配置文件,而後自由指定diskStore的目錄;

因而,在JFinal的配置文件中,就有了以下的代碼:

public void configPlugin(Plugins plugins) {
        plugins.add(createEhCachePlugin());
 
        //其餘插件略...
    }
 
    public EhCachePlugin createEhCachePlugin() {
        String ehcacheDiskStorePath = PathKit.getWebRootPath();
        File pathFile = new File(ehcacheDiskStorePath, ".ehcache");
 
        Configuration cfg = ConfigurationFactory.parseConfiguration();
        cfg.addDiskStore(new DiskStoreConfiguration().path(pathFile.getAbsolutePath()));
        return new EhCachePlugin(cfg);
    }

成功的把ehcache的存儲目錄保存在了webRoot的.ehcache目錄下.... 此時,也感嘆JFinal的ehcachePlugin插件的足夠靈活。

到此,JPress在遇到的ehcache坑中解決完畢,終於鬆了一口氣,美美吃上了老婆給我準備的早餐....

文章來至楊福海的博客:http://www.yangfuhai.com 歡迎轉載。

相關文章
相關標籤/搜索