Jive的緩存機(jī)制
Jive論壇的一個(gè)主要特點(diǎn)就是其性能速度快,因此很多巨大訪問量的網(wǎng)站都采用了Jive論壇。這些都是由于Jive采取了高速緩存機(jī)制。

緩存(Cache)機(jī)制是提高系統(tǒng)運(yùn)行性能必不可少的技術(shù)。緩存機(jī)制從原理上講比較簡單,就是在原始數(shù)據(jù)第一次讀取后
保存在內(nèi)存中,下次讀取時(shí),就直接從內(nèi)存中讀取。原始數(shù)據(jù)有可能保存在持久化介質(zhì)或網(wǎng)絡(luò)上。緩存機(jī)制也是
代理模式的一種實(shí)現(xiàn)。

 緩存原理和實(shí)現(xiàn)
Jive的Cache總體來說實(shí)現(xiàn)得不是非常精簡和有效。它是針對(duì)每個(gè)具體數(shù)據(jù)對(duì)象逐個(gè)實(shí)現(xiàn)緩沖,這種“窮盡”的辦法不是實(shí)
踐所推薦的用法。通過使用動(dòng)態(tài)代理模式,可以根據(jù)具體方法的不同來實(shí)現(xiàn)緩存是值得推薦的做法。Jive的緩存實(shí)現(xiàn)
得比較簡單,可以用來學(xué)習(xí)和研究緩存機(jī)制。

Jive中的Cache實(shí)現(xiàn)了緩存機(jī)制的大部分行為,它是將對(duì)象用惟一的關(guān)鍵字Key作標(biāo)識(shí)保存在HashMap或Hashtable中。
當(dāng)然,必須知道這些對(duì)象的大小,這個(gè)前提條件的設(shè)定可以保證緩存增長時(shí)不會(huì)超過規(guī)定的最大值。

如果緩存增長得太大,一些不經(jīng)常被訪問的對(duì)象將首先從緩存中刪除。如果設(shè)置了對(duì)象的最大生命周期時(shí)間,即使這個(gè)
對(duì)象被反復(fù)頻繁訪問,也將從緩存中刪除。這個(gè)特性可以適用于一些周期性需要刷新的數(shù)據(jù),如來自數(shù)據(jù)庫的數(shù)據(jù)。

在Cach中除了getObject()方法的性能依據(jù)緩存大小,其他方法的性能都是比較快的。一個(gè)HashMap用來實(shí)現(xiàn)快速尋找,
兩個(gè)LinkedList中一個(gè)以一定的訪問順序來保存對(duì)象,叫accessed LinkedList;另外一個(gè)以它們加入緩存的順序保存
這些對(duì)象,這種保存對(duì)象只是保存對(duì)象的引用,叫 age LinkedList。注意,這里的LinkedList不是JDK中的LinkedList
,而是Jive自己定義的LinkedList。

當(dāng)對(duì)象被加入緩存時(shí),首先被CacheObject封裝。封裝有以下信息:對(duì)象大小(以字節(jié)計(jì)算),一個(gè)指向accessed LinkedList的
引用,一個(gè)指向age LinkedList的引用。

當(dāng)從緩存中獲取一個(gè)對(duì)象如ObjectA時(shí),首先,HashMap尋找到指向封裝ObjectA等信息的CacheObject對(duì)象。然后,這個(gè)
對(duì)象將被移動(dòng)到accessed LinkedList的前面,還有其他一些動(dòng)作如緩存清理、刪除、過期失效等都是在這個(gè)動(dòng)作中一起
觸發(fā)實(shí)現(xiàn)的。



public class Cache implements Cacheable {
    
/**
     * 因?yàn)镾ystem.currentTimeMillis()執(zhí)行非常耗費(fèi)性能,因此如果get操作都執(zhí)行
     * 這條語句將會(huì)形成性能瓶頸, 通過一個(gè)全局時(shí)間戳來實(shí)現(xiàn)每秒更新
     * 當(dāng)然,這意味著在緩存過期時(shí)間計(jì)算上有一到幾秒的誤差
     
*/


    
protected static long currentTime = CacheTimer.currentTime;

    
//CacheObject對(duì)象
    protected HashMap cachedObjectsHash;

    
//accessed LinkedList 最經(jīng)常訪問的排列在最前面
    protected LinkedList lastAccessedList;

    
//以緩存加入順序排列,最后加入排在最前面;越早加入的排在最后面
    protected LinkedList ageList;

    
//緩存最大限制 默認(rèn)是128k 可根據(jù)內(nèi)存設(shè)定,越大性能越高
    protected int maxSize =  128 * 1024;

    
//當(dāng)前緩存的大小
    protected int size = 0;

    
//最大生命周期時(shí)間,默認(rèn)是沒有
    protected long maxLifetime = -1;

    
//緩存的擊中率,用于評(píng)測緩存效率
    protected long cacheHits, cacheMisses = 0L;

    
public Cache() {
        
// 構(gòu)造HashMap. 默認(rèn)capacity 是11
        
// 如果實(shí)際大小超過11,HashMap將自動(dòng)擴(kuò)充,但是每次擴(kuò)充都
        
// 是性能開銷,因此期初要設(shè)置大一點(diǎn)

        cachedObjectsHash 
= new HashMap(103);
        lastAccessedList 
= new LinkedList();
        ageList 
= new LinkedList();
    }


    
public Cache(int maxSize) {
        
this();
        
this.maxSize = maxSize;
    }


    
public Cache(long maxLifetime) {
        
this();
        
this.maxLifetime = maxLifetime;
    }


    
public Cache(int maxSize, long maxLifetime) {
        
this();
        
this.maxSize = maxSize;
        
this.maxLifetime = maxLifetime;
    }


    
public int getSize() {        return size;    }

    
public int getMaxSize() {        return maxSize;    }

 
    
public void setMaxSize(int maxSize) {
        
this.maxSize = maxSize;
        
// 有可能緩存大小超過最大值,需要激活刪除清理動(dòng)作
        cullCache();
    }


    
public synchronized int getNumElements() {
        
return cachedObjectsHash.size();
    }


    
/**
     * 增加一個(gè)Cacheable對(duì)象
     * 因?yàn)镠ashMap不是線程安全的,所以操作方法要使用同步
     * 如果使用Hashtable就不必同步
     
*/


    
public synchronized void add(Object key, Cacheable object) {
        
// 刪除已經(jīng)存在的key
        remove(key);
        
int objectSize = object.getSize();
        
// 如果被緩存對(duì)象的大小超過最大值,就放棄
        if (objectSize > maxSize * .90{            return;        }
        size 
+= objectSize;
        
//創(chuàng)建一個(gè)CacheObject對(duì)象
        CacheObject cacheObject = new CacheObject(object, objectSize);
        cachedObjectsHash.put(key, cacheObject);  
//保存這個(gè)CacheObject
        
// 加入accessed LinkedList,Jive自己的LinkedList在加入時(shí)可以返回值
        LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
        
// 保存引用
        cacheObject.lastAccessedListNode = lastAccessedNode;
        
// 加入到age LinkedList
        LinkedListNode ageNode = ageList.addFirst(key);
        
// 這里直接調(diào)用System.currentTimeMillis();用法值得討論
        ageNode.timestamp = System.currentTimeMillis();
        
// 保存引用
        cacheObject.ageListNode = ageNode;
        
// 做一些清理工作
        cullCache();
    }


    
/**
     * 從緩存中獲得一個(gè)被緩存的對(duì)象,這個(gè)方法在下面兩種情況返回空
     *    <li>該對(duì)象引用從來沒有被加入緩存中
     *    <li>對(duì)象引用因?yàn)檫^期被清除</ul>
     
*/


    
public synchronized Cacheable get(Object key) {
        
// 清除過期緩存
        deleteExpiredEntries();
        
//以Key從緩存中獲取一個(gè)對(duì)象引用
        CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);
        
if (cacheObject == null{
            
// 不存在,增加未命中率
            cacheMisses++;
            
return null;
        }

        
// 存在,增加命中率
        cacheHits++;
        
// 從accessed LinkedList中將對(duì)象從當(dāng)前位置刪除
        
// 重新插入在第一個(gè)
        cacheObject.lastAccessedListNode.remove();
        lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
        
return cacheObject.object;
    }

    …
}

在Cache中,關(guān)鍵字Key是一個(gè)對(duì)象,為了再次提高性能,可以進(jìn)一步將Key確定為一個(gè)long類型的整數(shù)。 

 緩存使用
建立LongCache只是為了提高原來的Cache性能,本身無多大意義,可以將LongCache看成與Cache一樣的類。
LongCache的關(guān)鍵字Key是Forum、ForumThread以及 ForumMessage等long類型的ID,值Value是Forum、ForumThread以及
ForumMessage等的對(duì)象。這些基本是通過DatabaseCacheManager實(shí)現(xiàn)完成,在主要類DbForumFactory的初始化構(gòu)造時(shí),
同時(shí)構(gòu)造了DatabaseCacheManager的實(shí)例cacheManager。

前面過濾器功能分析中,Message對(duì)象獲得方法的第一句如下:
protected ForumMessage getMessage(long messageID, long threadID, long forumID) throws
      ForumMessageNotFoundException 
{
      DbForumMessage message 
= cacheManager.messageCache.get(messageID);
    …
}


其中,cacheManager是DatabaseCacheManager的實(shí)例,DatabaseCacheManager是一個(gè)緩存Facade類。在其中包含了5種
類型的緩存,都是針對(duì) Jive的5個(gè)主要對(duì)象,DatabaseCacheManager主要代碼如下:

public class DatabaseCacheManager {

    
public UserCache userCache;                          //用戶資料緩存
    public GroupCache groupCache;                       //組資料緩存
    public ForumCache forumCache;                       //Forum論壇緩存
    public ForumThreadCache threadCache;                //Thread主題緩存
    public ForumMessageCache messageCache;          //Message緩存
    public UserPermissionsCache userPermsCache;     //用戶權(quán)限緩存
    public DatabaseCacheManager(DbForumFactory factory) {
        …
        forumCache 
= 
            
new ForumCache(new LongCache(forumCacheSize, 6*HOUR), factory);
        threadCache 
= 
            
new ForumThreadCache(
                  
new LongCache(threadCacheSize, 6*HOUR), factory);
        messageCache 
= new ForumMessageCache(
                  
new LongCache(messageCacheSize, 6*HOUR), factory);
        userCache 
= new UserCache(
                  
new LongCache(userCacheSize, 6*HOUR), factory);
        groupCache 
= new GroupCache(
                 
new LongCache(groupCacheSize, 6*HOUR), factory);
        userPermsCache 
= new UserPermissionsCache(
                
new UserPermsCache(userPermCacheSize, 24*HOUR), factory
        );
    }

    …
}

從以上代碼看出,F(xiàn)orumCache等對(duì)象生成都是以LongCache為基礎(chǔ)構(gòu)建的,以ForumCache為例,代碼如下:
public class ForumCache extends DatabaseCache {
    
//以Cache構(gòu)建ID緩存
    protected Cache forumIDCache = new Cache(128*10246*JiveGlobals.HOUR);

    
//以LongCache構(gòu)建整個(gè)對(duì)象緩存
    public ForumCache(LongCache cache, DbForumFactory forumFactory) {
        
super(cache, forumFactory);
    }

    
public DbForum get(long forumID) throws ForumNotFoundException {
        …
        DbForum forum 
= (DbForum)cache.get(forumID);
        
if (forum == null{    //如果緩存沒有從數(shù)據(jù)庫中獲取
            forum = new DbForum(forumID, factory);
            cache.add(forumID, forum); 
        }

        
return forum;
    }


public Forum get(String name) throws ForumNotFoundException {
    
//以name為key,從forumIDCache中獲取ID
   CacheableLong forumIDLong = (CacheableLong)forumIDCache.get(name);
        
if (forumIDLong == null//如果緩存沒有 從數(shù)據(jù)庫獲得
            long forumID = factory.getForumID(name);
            forumIDLong 
= new CacheableLong(forumID); //生成一個(gè)緩存對(duì)象
            forumIDCache.add(name, forumIDLong);
        }

        
return get(forumIDLong.getLong());
    }

    …
}

 

由此可以看到,LongCache封裝了Cache的核心功能,而ForumCache等類則是在LongCache核心外又包裝了與應(yīng)用系統(tǒng)相關(guān)
的操作,這有點(diǎn)類似裝飾(Decorator)模式。

從中也可以看到Cache和LongCache兩種緩存的用法。
使用Cache時(shí)的關(guān)鍵字Key是任何字段。如上面代碼中的String name,如果用戶大量帖子主題查詢中,Key是query + blockID,見
DbForum中的getThreadBlock方法;而值Value則是Long類型的ID,如ForumID或ThreadID等。


LongCache的關(guān)鍵字Key是Long類型的ID,如ForumID或ThreadID等;而值Value則是Forum、ForumThread或ForumMessage等
主要具體對(duì)象。

在實(shí)際使用中,大多數(shù)是根據(jù)ID獲得對(duì)象。但有時(shí)并不是這樣,因此根據(jù)應(yīng)用區(qū)分了兩種Cache,這其實(shí)類似數(shù)據(jù)庫的數(shù)據(jù)
表,除了主關(guān)鍵字外還有其他關(guān)鍵字。


 小結(jié)
緩存中對(duì)象是原對(duì)象的映射,如何確保緩存中對(duì)象和原對(duì)象的一致性?即當(dāng)原對(duì)象發(fā)生變化時(shí),緩存中的對(duì)象也必須立即
更新。這是緩存機(jī)制需要解決的另外一個(gè)基本技術(shù)問題。

Jive中是在原對(duì)象發(fā)生變化時(shí),立即進(jìn)行清除緩存中對(duì)象,如ForumMessage對(duì)象的創(chuàng)建。在DbForumThread的AddMessage
方法中有下列語句:

factory.cacheManager.threadCache.remove(this.id);
factory.cacheManager.forumCache.remove(this.forumID);

即當(dāng)有新的帖子加入時(shí),將ForumThreadCache和ForumCache相關(guān)緩沖全部清除。這樣,當(dāng)有相關(guān)對(duì)象讀取時(shí),將直接從數(shù)
據(jù)庫中讀取,這是一種非常簡單的緩存更新方式。

在復(fù)雜的系統(tǒng),例如有一臺(tái)以上的服務(wù)器運(yùn)行著Jive系統(tǒng)。如果一個(gè)用戶登陸一臺(tái)服務(wù)器后,通過這臺(tái)服務(wù)器增加新帖。
那么按照上述原理,只能更新本服務(wù)器JVM中的緩存數(shù)據(jù),而其他服務(wù)器則無從得知這種改變,這就需要一種分布式的
緩存機(jī)制。

 


圖3-7  Jive主要對(duì)象的訪問
到目前可以發(fā)現(xiàn),整個(gè)Jive系統(tǒng)其實(shí)是圍繞Forum、ForumThread和ForumMessage等這些主要對(duì)象展開的讀取、修改或創(chuàng)建
等操作。由于這些對(duì)象原先持久化保存在數(shù)據(jù)庫中,為了提高性能和加強(qiáng)安全性,Jive在這些對(duì)象外面分別實(shí)現(xiàn)兩層包裝
,如圖3-7所示。

客戶端如果需要訪問這些對(duì)象,首先要經(jīng)過它們的代理對(duì)象。進(jìn)行訪問權(quán)限的檢查,然后再從緩存中獲取該對(duì)象。只有
緩存不存在時(shí),才會(huì)從數(shù)據(jù)庫中獲取。

這套機(jī)制是大多數(shù)應(yīng)用系統(tǒng)都面臨的必須解決的基本功能,因此完全可以做成一個(gè)通用的可重復(fù)使用的框架。這樣在
具體應(yīng)用時(shí),不必每個(gè)應(yīng)用系統(tǒng)都架設(shè)開發(fā)這樣的機(jī)制。其實(shí)EJB就是這樣一套框架,實(shí)體Bean都由緩存機(jī)制支持,
而通過設(shè)定ejb-jar.xml可以實(shí)現(xiàn)訪問權(quán)限控制,這些工作都直接由EJB容器實(shí)現(xiàn)了,不必在代碼中自己來實(shí)現(xiàn)。剩余
的工作是調(diào)整EJB容器的參數(shù),使之適合應(yīng)用系統(tǒng)的具體要求,這些將在以后章節(jié)中討論。

在Jive中,圖3-7的機(jī)制是通過不同方式實(shí)現(xiàn)的。基本上是一配二模式:一個(gè)對(duì)象有一個(gè)緩沖對(duì)象和一個(gè)代理對(duì)象,這樣
做的一個(gè)缺點(diǎn)是導(dǎo)致對(duì)象太多,系統(tǒng)變得復(fù)雜。這點(diǎn)在閱讀Jive源碼時(shí)可能已經(jīng)發(fā)現(xiàn)。

如果建立一個(gè)對(duì)象工廠,工廠內(nèi)部封裝了圖3-7機(jī)制實(shí)現(xiàn)過程,客戶端可以根據(jù)不同的工廠輸入?yún)?shù)獲得具體不同的
對(duì)象。這樣也許代碼結(jié)構(gòu)要更加抽象和緊湊,Java的動(dòng)態(tài)代理API也許是實(shí)現(xiàn)這個(gè)工廠的主要技術(shù)基礎(chǔ)。有興趣者可以
進(jìn)一步研究提煉。