Jive的緩存機制
Jive論壇的一個主要特點就是其性能速度快,因此很多巨大訪問量的網站都采用了Jive論壇。這些都是由于Jive采取了高速緩存機制。
緩存(Cache)機制是提高系統運行性能必不可少的技術。緩存機制從原理上講比較簡單,就是在原始數據第一次讀取后
保存在內存中,下次讀取時,就直接從內存中讀取。原始數據有可能保存在持久化介質或網絡上。緩存機制也是
代理模式的一種實現。
緩存原理和實現
Jive的Cache總體來說實現得不是非常精簡和有效。它是針對每個具體數據對象逐個實現緩沖,這種“窮盡”的辦法不是實
踐所推薦的用法。通過使用動態代理模式,可以根據具體方法的不同來實現緩存是值得推薦的做法。Jive的緩存實現
得比較簡單,可以用來學習和研究緩存機制。
Jive中的Cache實現了緩存機制的大部分行為,它是將對象用惟一的關鍵字Key作標識保存在HashMap或Hashtable中。
當然,必須知道這些對象的大小,這個前提條件的設定可以保證緩存增長時不會超過規定的最大值。
如果緩存增長得太大,一些不經常被訪問的對象將首先從緩存中刪除。如果設置了對象的最大生命周期時間,即使這個
對象被反復頻繁訪問,也將從緩存中刪除。這個特性可以適用于一些周期性需要刷新的數據,如來自數據庫的數據。
在Cach中除了getObject()方法的性能依據緩存大小,其他方法的性能都是比較快的。一個HashMap用來實現快速尋找,
兩個LinkedList中一個以一定的訪問順序來保存對象,叫accessed LinkedList;另外一個以它們加入緩存的順序保存
這些對象,這種保存對象只是保存對象的引用,叫 age LinkedList。注意,這里的LinkedList不是JDK中的LinkedList
,而是Jive自己定義的LinkedList。
當對象被加入緩存時,首先被CacheObject封裝。封裝有以下信息:對象大小(以字節計算),一個指向accessed LinkedList的
引用,一個指向age LinkedList的引用。
當從緩存中獲取一個對象如ObjectA時,首先,HashMap尋找到指向封裝ObjectA等信息的CacheObject對象。然后,這個
對象將被移動到accessed LinkedList的前面,還有其他一些動作如緩存清理、刪除、過期失效等都是在這個動作中一起
觸發實現的。



public class Cache implements Cacheable
{

/** *//**
* 因為System.currentTimeMillis()執行非常耗費性能,因此如果get操作都執行
* 這條語句將會形成性能瓶頸, 通過一個全局時間戳來實現每秒更新
* 當然,這意味著在緩存過期時間計算上有一到幾秒的誤差
*/

protected static long currentTime = CacheTimer.currentTime;

//CacheObject對象
protected HashMap cachedObjectsHash;

//accessed LinkedList 最經常訪問的排列在最前面
protected LinkedList lastAccessedList;

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

//緩存最大限制 默認是128k 可根據內存設定,越大性能越高
protected int maxSize = 128 * 1024;

//當前緩存的大小
protected int size = 0;

//最大生命周期時間,默認是沒有
protected long maxLifetime = -1;

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


public Cache()
{
// 構造HashMap. 默認capacity 是11
// 如果實際大小超過11,HashMap將自動擴充,但是每次擴充都
// 是性能開銷,因此期初要設置大一點

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;
// 有可能緩存大小超過最大值,需要激活刪除清理動作
cullCache();
}


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


/** *//**
* 增加一個Cacheable對象
* 因為HashMap不是線程安全的,所以操作方法要使用同步
* 如果使用Hashtable就不必同步
*/


public synchronized void add(Object key, Cacheable object)
{
// 刪除已經存在的key
remove(key);
int objectSize = object.getSize();
// 如果被緩存對象的大小超過最大值,就放棄

if (objectSize > maxSize * .90)
{ return; }
size += objectSize;
//創建一個CacheObject對象
CacheObject cacheObject = new CacheObject(object, objectSize);
cachedObjectsHash.put(key, cacheObject); //保存這個CacheObject
// 加入accessed LinkedList,Jive自己的LinkedList在加入時可以返回值
LinkedListNode lastAccessedNode = lastAccessedList.addFirst(key);
// 保存引用
cacheObject.lastAccessedListNode = lastAccessedNode;
// 加入到age LinkedList
LinkedListNode ageNode = ageList.addFirst(key);
// 這里直接調用System.currentTimeMillis();用法值得討論
ageNode.timestamp = System.currentTimeMillis();
// 保存引用
cacheObject.ageListNode = ageNode;
// 做一些清理工作
cullCache();
}


/** *//**
* 從緩存中獲得一個被緩存的對象,這個方法在下面兩種情況返回空
* <li>該對象引用從來沒有被加入緩存中
* <li>對象引用因為過期被清除</ul>
*/


public synchronized Cacheable get(Object key)
{
// 清除過期緩存
deleteExpiredEntries();
//以Key從緩存中獲取一個對象引用
CacheObject cacheObject = (CacheObject)cachedObjectsHash.get(key);

if (cacheObject == null)
{
// 不存在,增加未命中率
cacheMisses++;
return null;
}
// 存在,增加命中率
cacheHits++;
// 從accessed LinkedList中將對象從當前位置刪除
// 重新插入在第一個
cacheObject.lastAccessedListNode.remove();
lastAccessedList.addFirst(cacheObject.lastAccessedListNode);
return cacheObject.object;
}
…
}
在Cache中,關鍵字Key是一個對象,為了再次提高性能,可以進一步將Key確定為一個long類型的整數。
緩存使用
建立LongCache只是為了提高原來的Cache性能,本身無多大意義,可以將LongCache看成與Cache一樣的類。
LongCache的關鍵字Key是Forum、ForumThread以及 ForumMessage等long類型的ID,值Value是Forum、ForumThread以及
ForumMessage等的對象。這些基本是通過DatabaseCacheManager實現完成,在主要類DbForumFactory的初始化構造時,
同時構造了DatabaseCacheManager的實例cacheManager。
前面過濾器功能分析中,Message對象獲得方法的第一句如下:
protected ForumMessage getMessage(long messageID, long threadID, long forumID) throws

ForumMessageNotFoundException
{
DbForumMessage message = cacheManager.messageCache.get(messageID);
…
}

其中,cacheManager是DatabaseCacheManager的實例,DatabaseCacheManager是一個緩存Facade類。在其中包含了5種
類型的緩存,都是針對 Jive的5個主要對象,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; //用戶權限緩存

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
);
}
…
}
從以上代碼看出,ForumCache等對象生成都是以LongCache為基礎構建的,以ForumCache為例,代碼如下:

public class ForumCache extends DatabaseCache
{
//以Cache構建ID緩存
protected Cache forumIDCache = new Cache(128*1024, 6*JiveGlobals.HOUR);

//以LongCache構建整個對象緩存

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)
{ //如果緩存沒有從數據庫中獲取
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)
{ //如果緩存沒有 從數據庫獲得
long forumID = factory.getForumID(name);
forumIDLong = new CacheableLong(forumID); //生成一個緩存對象
forumIDCache.add(name, forumIDLong);
}
return get(forumIDLong.getLong());
}
…
}
由此可以看到,LongCache封裝了Cache的核心功能,而ForumCache等類則是在LongCache核心外又包裝了與應用系統相關
的操作,這有點類似裝飾(Decorator)模式。
從中也可以看到Cache和LongCache兩種緩存的用法。
使用Cache時的關鍵字Key是任何字段。如上面代碼中的String name,如果用戶大量帖子主題查詢中,Key是query + blockID,見
DbForum中的getThreadBlock方法;而值Value則是Long類型的ID,如ForumID或ThreadID等。
LongCache的關鍵字Key是Long類型的ID,如ForumID或ThreadID等;而值Value則是Forum、ForumThread或ForumMessage等
主要具體對象。
在實際使用中,大多數是根據ID獲得對象。但有時并不是這樣,因此根據應用區分了兩種Cache,這其實類似數據庫的數據
表,除了主關鍵字外還有其他關鍵字。
小結
緩存中對象是原對象的映射,如何確保緩存中對象和原對象的一致性?即當原對象發生變化時,緩存中的對象也必須立即
更新。這是緩存機制需要解決的另外一個基本技術問題。
Jive中是在原對象發生變化時,立即進行清除緩存中對象,如ForumMessage對象的創建。在DbForumThread的AddMessage
方法中有下列語句:
factory.cacheManager.threadCache.remove(this.id);
factory.cacheManager.forumCache.remove(this.forumID);
即當有新的帖子加入時,將ForumThreadCache和ForumCache相關緩沖全部清除。這樣,當有相關對象讀取時,將直接從數
據庫中讀取,這是一種非常簡單的緩存更新方式。
在復雜的系統,例如有一臺以上的服務器運行著Jive系統。如果一個用戶登陸一臺服務器后,通過這臺服務器增加新帖。
那么按照上述原理,只能更新本服務器JVM中的緩存數據,而其他服務器則無從得知這種改變,這就需要一種分布式的
緩存機制。
圖3-7 Jive主要對象的訪問
到目前可以發現,整個Jive系統其實是圍繞Forum、ForumThread和ForumMessage等這些主要對象展開的讀取、修改或創建
等操作。由于這些對象原先持久化保存在數據庫中,為了提高性能和加強安全性,Jive在這些對象外面分別實現兩層包裝
,如圖3-7所示。
客戶端如果需要訪問這些對象,首先要經過它們的代理對象。進行訪問權限的檢查,然后再從緩存中獲取該對象。只有
緩存不存在時,才會從數據庫中獲取。
這套機制是大多數應用系統都面臨的必須解決的基本功能,因此完全可以做成一個通用的可重復使用的框架。這樣在
具體應用時,不必每個應用系統都架設開發這樣的機制。其實EJB就是這樣一套框架,實體Bean都由緩存機制支持,
而通過設定ejb-jar.xml可以實現訪問權限控制,這些工作都直接由EJB容器實現了,不必在代碼中自己來實現。剩余
的工作是調整EJB容器的參數,使之適合應用系統的具體要求,這些將在以后章節中討論。
在Jive中,圖3-7的機制是通過不同方式實現的?;旧鲜且慌涠J剑阂粋€對象有一個緩沖對象和一個代理對象,這樣
做的一個缺點是導致對象太多,系統變得復雜。這點在閱讀Jive源碼時可能已經發現。
如果建立一個對象工廠,工廠內部封裝了圖3-7機制實現過程,客戶端可以根據不同的工廠輸入參數獲得具體不同的
對象。這樣也許代碼結構要更加抽象和緊湊,Java的動態代理API也許是實現這個工廠的主要技術基礎。有興趣者可以
進一步研究提煉。