?
回復此消息
前言
Jive是一個廣受歡迎的開放的源碼的論壇項目,它有很多值得我們學習的地方。這篇文章談的就是Jive緩存機制的實現,希望對大家有所幫助。
簡介
我們知道,在兩個存取速度差距很大的對象(比如數據庫和內存)之間,通常要加一個緩存,來匹配二者的速度。因此,緩存機制在我們實際項目中還是經常遇到的。同樣Jive也使用緩存來加快貼子的顯示。如果你正試圖編寫一個類似的東西,不妨研究一下Jive源碼,可能對你大有幫助。
在Jive2.1.2中,涉及jive的緩存機制的java類大致可以分為以下四個部分(為了簡化起見,本文只討論帖子的緩存機制的實現,用戶名和權限的存取雖然也用到了的緩存,但其實現機制與前者類似,因此不再贅述):
第一部分:提供HashMap,LinkedListedlist等數據結構以便實現緩存機制,其中HashMap是JDK提供的,其Key類型為Object。你可以在com.jivesoftware.util包中找到他們,包括的類有Cache類,?LinkedList類,LinkedListNode類和Casheable接口,CacheObject類,CacheableBoolean類,CacheableInt類,CacheableLong類,CacheableLongArray類,CacheableString類,CacheSizes類,CacheTimer類;
第二部分:提供LongHashMap,LongLinkedListedlist等數據結構以便實現緩存機制,與第一部分不同的是,它的HashMap是自己編寫的,其Key類型為Long,因此被冠以LongHashMap的名稱。你同樣可以在com.jivesoftware.util包中找到他們,包括的類有LongHashMap類,LongCache類,?LongCacheObject類,
LongLinkedList類和LongLinkedListNode類;還有第一部分中的Casheable接口,它的各種數據類型的實現以及CacheSizes類和CacheTimer類,也可歸于這部分,它們可看作是第一部分和第二部分的交集;
第三部分:調用底層數據結構以提供論壇對象的緩存,你可以在com.jivesoftware.forum.database包中找到他們,包括的類主要有DatabaseCacheManager類,DbForumFactory類,DbForum類,DbForumThread類,DbForumMessage?類,DatabaseCache類,ForumCache類,?ForumThreadCache類,ForumMessageCache類;?
第四部分:向Jsp頁面提供訪問接口,同樣在com.jivesoftware.forum.database包中,包括類有ForumThreadBlockIterator和ForumMessageBlockIterator類,第三部分的DbForum類,DbForumThread類和DbForumMessage?類也可以包括進來,實際上,這三個類是第三和第四部分聯系的紐帶,在com.jivesoftware.util包中還有一個LongList類,它用來將ForumThreadBlockIterator和ForumMessageBlockIterator轉化成Long型數組,因此也應算在這部分;
因此緩存機制也可以劃分為三層,即第一、二部分的底層數據結構、第三部分的中間層和第四部分的上層訪問接口,下面分別討論:
底層數據結構
??jive緩存機制的原理其實很簡單,就是把所要緩存的對象加到HashMap哈希映射表中,用兩個LinkedListedlist雙向鏈表分別維持著緩存對象和每個緩存對象的生命周期,如果一個緩存對象被訪問到,那么就把它放到鏈表的最前面,然后不定時的把要緩存對象的對象加入鏈表中,把過期對象刪除,如此反復。我們比較一下第一和第二部分就可以發現,他們的代碼幾乎完全相同。差別就在第二部分的哈希映射表沒有采用JDK提供的類,而是采用了作者自己編寫的一個類,他將原來哈希映射表的Key的類型由Object改為Long,這樣做雖然在一定程度上加快了緩存的速度并減小了緩存的大小,但無形之中也減低了程序的穩定性和可讀性,因此不推薦大家仿效。值得一提的是,在Jive1.0.2版中,所有Forum,Thread,Message的ID和他們的內容的緩存都是用第一部分的Java類實現的,在升級到后面的版本時,其內容采用了第二部分的Java類實現,但其ID仍用第一部分的Java類實現,這也是Jive容易混淆的一個地方。好了,我們先來看第一部分的Java類實現。
我們先來看LinkedListNode類的源碼:
public?class?LinkedListNode?{
public?LinkedListNode?previous;
????public?LinkedListNode?next;
????public?Object?object;
????public?long?timestamp;

public?LinkedListNode(Object?object,?LinkedListNode?next,
????????????LinkedListNode?previous)
????{
????????this.object?=?object;
????????this.next?=?next;
????????this.previous?=?previous;
????}
public?void?remove()?{
????????previous.next?=?next;
????????next.previous?=?previous;
????}
public?String?toString()?{
????????return?object.toString();
????}
}
很明顯,這是一個雙向鏈表的節點類,previous,next分別記錄前后節點的指針,object用于記錄所需緩存的對象,timestamp用于記錄當前節點被創建時的時間戳。當該時間戳超過該節點的生存周期時,它就會被remove()方法刪除掉。就是由LinkedListNode構成了LinkedList鏈表,而LinkedList類中只是實現了getFirst(),getLast(),addFirst(),addLast(),clear()等鏈表的基本方法,沒有其他內容。
再來看Cacheable接口和它的一個實現類CacheableInt:

public?interface?Cacheable?{
????public?int?getSize();
}
public?class?CacheableInt?implements?Cacheable?{
????private?int?intValue;
????public?CacheableInt(int?intValue)?{
????????this.intValue?=?intValue;
????}
????public?int?getInt()?{
????????return?intValue;
????}
????public?int?getSize()?{
????????return?CacheSizes.sizeOfObject()?+?CacheSizes.sizeOfInt();
????}
}
從上面的代碼可以看到Cacheable接口只有一個方法getSize(),它要求繼承類實現該方法匯報占用緩存的大小,以便實施管理。Integer,Boolean,Long,LongArray,String類型都有其對應的類,CacheSizes類則把各種類型的長度封裝起來,便于修改和移植。那么為什么CacheableInt.?getSize()得到的是sizeOfObject()+sizeOfInt()呢,聰明的讀者一定想到了,因為任何都繼承自Object,計算空間時當然也要把它給算上。
還有一個CacheObject類,它是緩存的基本元素,我們來看代碼:
public?final?class?CacheObject?{
public?Cacheable?object;
??????public?int?size;
??????public?LinkedListNode?lastAccessedListNode;
public?LinkedListNode?ageListNode;

??????public?CacheObject(Cacheable?object,?int?size)?{
????????this.object?=?object;
????????this.size?=?size;
}
}
lastAccessedListNode記錄著一個緩存節點的Key,是構成lastAccessedList鏈表的基本元素,在lastAccessedList鏈表中,經常被訪問到的節點總是在最前面。而ageListNode記錄著緩存節點的加入時間,是構成ageList鏈表的基本元素,而ageList鏈表則是按時間先后排序,先加入的節點總是在最后面。lastAccessedListNode和ageListNode本來可以寫成兩個類,畢竟lastAccessedListNode并不需要ageListNode的成員變量timestamp,但是為了簡化程序,Jive把他們寫成了一個類,這也是容易混淆的一個地方。
現在來看緩存機制中最關鍵的一個類Cache的部分代碼,主要是add()和get()方法:
public?class?Cache?implements?Cacheable?{
protected?static?long?currentTime?=?CacheTimer.currentTime;
protected?HashMap?cachedObjectsHash;
protected?LinkedList?lastAccessedList;
protected?LinkedList?ageList;
//緩存元素的最大尺寸128kbit,可修改

protected?int?maxSize?=??128?*?1024;?
//整個緩存的大小
protected?int?size?=?0;
//緩存元素的最大保存時間,用Cache(long?maxLifetime)初始化
protected?long?maxLifetime?=?-1;
//記錄cache的命中次數和未命中次數
protected?long?cacheHits,?cacheMisses?=?0L;?
……
//向哈希表中添加一個關鍵字為Key的緩存對象object
public?synchronized?void?add(Object?key,?Cacheable?object)?{
????????//先把原來的對象remove掉
????????remove(key);
int?objectSize?=?object.getSize();
????????//如果對象太大,則不加入緩沖存
????????if?(objectSize?>?maxSize?*?.90)?{
????????????return;
????????}
????????size?+=?objectSize;
????????//新建一個緩存對象,并放入哈希表中
????????CacheObject?cacheObject?=?new?CacheObject(object,?objectSize);
????????cachedObjectsHash.put(key,?cacheObject);
????????//?把緩存元素的Key放到lastAccessed?List鏈表的最前面
????????LinkedListNode?lastAccessedNode?=?lastAccessedList.addFirst(key);
????????cacheObject.lastAccessedListNode?=?lastAccessedNode;
????????//把緩存元素的Key放到ageList鏈表的最前面,并記下當前時間
????????LinkedListNode?ageNode?=?ageList.addFirst(key);
????????ageNode.timestamp?=?System.currentTimeMillis();
????????cacheObject.ageListNode?=?ageNode;
//?在cullCache()中,先調用deleteExpiredEntries()把過期對象刪掉,如果緩存還是太滿,則掉用remove(lastAccessedList.getLast().object)把lastAccessedList中不常訪問的對象刪掉
????????cullCache();
????}
//在哈希表中得到一個關鍵字為Key的緩存對象object
public?synchronized?Cacheable?get(Object?key)?{
????????//?清理過期對象
????????deleteExpiredEntries();

????????CacheObject?cacheObject?=?(CacheObject)cachedObjectsHash.get(key);
????????if?(cacheObject?==?null)?{
????????????//沒找到則未命中次數加一
????????????cacheMisses++;
????????????return?null;
????????}

????????//找到則命中次數加一
????????cacheHits++;

????????//將該緩存對象從lastAccessedList鏈表中取下并插入到
????????//鏈表頭部
????????cacheObject.lastAccessedListNode.remove();
????????lastAccessedList.addFirst(cacheObject.lastAccessedListNode);

????????return?cacheObject.object;
????}
到這里第一部分的Java類實現就說完了,正如上文提到的那樣,第二部分的Java類實現與第一部分基本上沒有什么差別,因此就不再贅述。下面給出第二部分的類圖,以供讀者參考。
[img]http://www.javaresearch.org/members/{tomjava}
中間層
?????中間層是聯系上層訪問接口和低層數據結構的紐帶。它的主要功能就是根據ID(對應于數據庫中的編號)到緩存中去找相應的對象,如果緩存中有該對象就直接得到,沒有則去讀數據庫生成一個新的對象,再把該對象放入緩存中,以便下次訪問時能直接得到。下面是相關類的類圖:
[img]http://www.javaresearch.org/members/{tomjava}
(注:Forum表示論壇,Thread表示論壇貼子的線索,Message表示論壇貼子,它們的關系是這樣的?:Forum包括數條Thread,Thread包括數條Message。)
由上圖可見,DbForum類,DbForumThread類和DbForumMessage?類的實例對象都包含一個?DbForumFactory類的實例對象factory。DbForum,DbForumThread和DbForumMessage被DbForumFactory生產出來,同時他們也通過DbForumFactory來訪問緩存。而在DbForumFactory中則包含一個DatabaseCacheManager類的實例對象cacheManager,它負責管理所有的緩存對象,如圖,這些緩存對象就是ForumCache類,?ForumThreadCache類和ForumMessageCache類的實例。ForumCache類,?ForumThreadCache類和ForumMessageCache類繼承自同一個抽象類DatabaseCache,而在DatabaseCache類中,有一個LongCache型的成員變量cache,就這樣,中間層就和低層的數據結構結合起來了。
我們以thread線索對象的獲得為例來說明中間層是如何運作的,請看代碼摘要:
DbForum.java
public?class?DbForum?implements?Forum,?Cacheable?{
????。。。?。。。
public?ForumThread?getThread(long?threadID)
????????????throws?ForumThreadNotFoundException
????{
????????return?factory.getThread(threadID,?this);
????}
。。。?。。。
}

DbForumFactory.java
public?class?DbForumFactory?extends?ForumFactory?{
????。。。?。。。
protected?DbForumThread?getThread(long?threadID,?DbForum?forum)?throws
????????????ForumThreadNotFoundException
????{
????????DbForumThread?thread?=?cacheManager.threadCache.get(threadID);
????????return?thread;
}
。。。?。。。
}

ForumThreadCache.java
public?class?ForumThreadCache?extends?DatabaseCache?{
????。。。?。。。
public?DbForumThread?get(long?threadID)
????????????throws?ForumThreadNotFoundException
{ //緩存中尋找以threadID為編號的DbForumThread對象
DbForumThread?thread?=?(DbForumThread)cache.get(threadID);
????????if?(thread?==?null)?{
      //如果在緩存中找不到該對象
????????????//新建一個以threadID為編號的DbForumThread對象
thread?=?new?DbForumThread(threadID,?factory);
//將新建對象加入緩存
????????????cache.add(threadID,?thread);
????????}
????????return?thread;
}
???。。。?。。。
}

DbForumThread.java
public?class?DbForumThread?implements?ForumThread,?Cacheable?{
???。。。?。。。
protected?DbForumThread(long?id,?DbForumFactory?factory)
????????????throws?ForumThreadNotFoundException
????{
????????this.id?=?id;
????????this.factory?=?factory;
//讀取數據庫,其中id對應數據庫中的jiveThreadProp表中的threadID字段
????????loadFromDb();
????????isReadyToSave?=?true;
}
???。。。?。。。
}
???從上面的代碼我們可以看到,當我們調用DbForum類?的getThread(long?threadID)方法去獲得一個編號為threadID的線索對象時,我們實際上調用的是DbForumFactory類中的getThread(long?threadID,?DbForum?forum)方法,而它則是調用ForumThreadCache類的方法來完成任務的。那么ForumThreadCache類里get(long?threadID)方法有做了什么呢?代碼已經很清楚了,就是根據threadID到緩存中去找相應的線索對象,如果緩存中有該對象就直接得到,沒有則新建一個DbForumThread對象,再把該對象放入緩存中。看到這里,有點讓人奇怪,好象程序中根本沒有連接數據庫的語句。再看DbForumThread類的代碼,終于恍然大悟,原來Jive在新建一個DbForumThread對象就已經用loadFromDb()方法把數據讀出來了;另一方面,如果在緩存中找了DbForumThread對象,程序根本就不會新建DbForumThread對象,因而就好象沒有數據庫的操作,這實際上就是我們通過緩存機制所要達到的目的。
????Message帖子對象的獲得與Thread對象的獲得類似,因此就不再重復了。從上面我們可以看到,只要我們得到了論壇線索的編號threadID,我們就可以得到對應的線索對象,不管它是從緩存中來,還是從數據庫中來。那么threadID是如何從Jsp頁面傳到中間層的呢?讓我們來看上層訪問接口的運行機制吧。
上層訪問接口
????上層訪問接口的主要功能連接Jsp頁面和中間層,換句話說,就是把Jsp頁面中要調用的Thread、Message對象的ID傳遞到中間層。下面給出訪問Thread的相關類的類圖(訪問Message機制類似,故省略)。其中的forum.jsp是顯示論壇內容的頁面,在這里,我們把forum.jsp看成是一個特殊的類,它里面有一個ForumThreadIterator類的實例變量threads和DbForum類的實例變量forum,故它和ForumThreadIterator類及DbForum類的關系應是關聯關系。
[img]http://www.javaresearch.org/members/{tomjava}
先來看forum.jsp和DbForum?類的部分代碼:
forum.jsp
<%
//?ResultFilter結果過濾類
ResultFilter?filter?=?new?ResultFilter();
????filter.setStartIndex(start);
????filter.setNumResults(range);
//調用Dbforum的threads()方法,獲得ForumThreadIterator對象實例
ForumThreadIterator?threads?=?forum.threads(filter);
。。。?。。。
while?(threads.hasNext())?{
????????//對thead進行遍歷
????????ForumThread?thread?=?(ForumThread)threads.next();
????????//得到thread的ID
????????long?threadID?=?thread.getID();
????????//得到線索的根帖子rootMessage
????????ForumMessage?rootMessage?=?thread.getRootMessage();
????????//得到帖子主題和作者等信息
????????String?subject?=?rootMessage.getSubject();
????????User?author?=?rootMessage.getUser();
。。。?。。。
}
%>
DbForum.java
public?class?DbForum?implements?Forum,?Cacheable?{
。。。?。。。
public?ForumThreadIterator?threads(ResultFilter?resultFilter)?{
????//生成SQL語句
????????String?query?=?getThreadListSQL(resultFilter,?false);
????????//得到threadID塊
long?[]?threadBlock?=?getThreadBlock(query.toString(),
?resultFilter.getStartIndex());
????????。。。?。。。
????????//返回ForumThreadBlockIterator對象
????????return?new?ForumThreadBlockIterator(threadBlock,?query.toString(),
????????????????startIndex,?endIndex,?this.id,?factory);
}
protected?long[]?getThreadBlock(String?query,?int?startIndex)?{
????????int?blockID?=?startIndex?/?THREAD_BLOCK_SIZE;
????????int?blockStart?=?blockID?*?THREAD_BLOCK_SIZE;??
????????String?key?=?query?+?blockID;?????
?//根據Key的值到緩存中取得ThreadID的數組
????????CacheableLongArray?longArray?=?
(CacheableLongArray)threadListCache.get(key);
????????//在緩存中則返回
????????if?(longArray?!=?null)?{
????????????long?[]?threads?=?longArray.getLongArray();
????????????return?threads;
????????}
????????//?否則到數據庫中取ThreadID的塊,以數組形式返回
????????else?{
????????????LongList?threadsList?=?new?LongList(THREAD_BLOCK_SIZE);
????????????Connection?con?=?null;
????????????Statement?stmt?=?null;
????????????。。。數據庫操作?。。。
????????????}
????????????long?[]?threads?=?threadsList.toArray();
????????????//將?ThreadID的塊加入緩存
threadListCache.add(key,?new?CacheableLongArray(threads));
????????????return?threads;
????????}
?????}
。。。?。。。
}
在forum.jsp中有一個ResultFilter類的實例resultFilter,它給出頁面顯示Thread的起始位置和數量,它作為參數傳入forum.threads()中,用于構造相關的SQL語句。當調用forum.threads(filter)時,程序將生成的SQL語句傳入到getThreadBlock()方法中去得到一個threadID的塊,也就是一組threadID。之所以要讀threadID塊,是因為顯示論壇時并不是顯示一條線索就行了,而是一下顯示十幾條,這樣做可以避免反復讀數據庫,再說threadID不是thread對象,并不怎么占空間。
應該說,使用了塊以后,減輕了數據庫的訪問量,因而論壇的效率有了很大的提高。然而好戲還在后頭,Jive又把塊放入了緩存中。在getThreadBlock()方法里,Jive用Cache類的實例對象threadListCache來緩存threadID塊,而關鍵字就是SQL語句加上blockID,也就是說,主要SQL語句和blockID相同,就可以在緩存中取出相同的threadID塊。當然,緩存中找不到,還是要到數據庫中讀出來加入緩存的。這樣論壇的效率又得到了進一步的提升。
ForumThreadBlockIterator類繼承自ForumThreadIterator抽象類,而ForumThreadIterator類又實現了Iterator接口,因此得到ForumThreadBlockIterator的實例對象threads后,就可以在用threads.next()方法對它進行編歷了。ForumThreadBlockIterator類的代碼想都可以想得到,就是逐個讀取ThreadID,然后根據ThreadID返回Thread對象,由此上層訪問接口就和中間層銜接起來了。
小結
Jive的緩存機制值得我們學習的地方有很多,比如讀取線索時不是讀一條而是讀一個block;顯示線索的起始位置和數量用專門的一個類來管理,動態生成SQL語句;用一個專門的類來負責管理緩存;把論壇緩存對象的功能抽象出來形成一個緩存的抽象類DatabaseCache,讓它去跟低層數據結構聯系起來。這些都體現了面向對象的設計原則即提高軟件的可維護性和可復用性。
同時,Jive也告訴我們,要想編好程序,只懂條件語句和循環語句可不行;必須選擇好數據結構,掌握好面向對象的設計原則,熟悉設計模式思想方法,這樣才能編寫出強壯高效的代碼。