緩存( Cache )機(jī)制是提高系統(tǒng)運(yùn)行性能必不可少的技術(shù)。緩存機(jī)制從原理上講比較簡(jiǎn)單,就是在原始數(shù)據(jù)第一次讀取后保存在內(nèi)存中,下次讀取時(shí),就直接從內(nèi)存中讀取。原始數(shù)據(jù)有可能保存在持久化介質(zhì)或網(wǎng)絡(luò)上。緩存機(jī)制也是代理模式的一種實(shí)現(xiàn)。 4.1 緩存原理和實(shí)現(xiàn)Jive 的 Cache 總體來(lái)說(shuō)實(shí)現(xiàn)得不是非常精簡(jiǎn)和有效。它是針對(duì)每個(gè)具體數(shù)據(jù)對(duì)象逐個(gè)實(shí)現(xiàn)緩沖,這種“窮盡”的辦法不是實(shí)踐所推薦的用法。通過(guò)使用動(dòng)態(tài)代理模式,可以根據(jù)具體方法的不同來(lái)實(shí)現(xiàn)緩存是值得推薦的做法。 Jive 的緩存實(shí)現(xiàn)得比較簡(jiǎn)單,可以用來(lái)學(xué)習(xí)和研究緩存機(jī)制。 Jive 中的 Cache 實(shí)現(xiàn)了緩存機(jī)制的大部分行為,它是將對(duì)象用惟一的關(guān)鍵字 Key 作標(biāo)識(shí)保存在 HashMap 或 Hashtable 中。當(dāng)然,必須知道這些對(duì)象的大小,這個(gè)前提條件的設(shè)定可以保證緩存增長(zhǎng)時(shí)不會(huì)超過(guò)規(guī)定的最大值。 如果緩存增長(zhǎng)得太大,一些不經(jīng)常被訪問(wèn)的對(duì)象將首先從緩存中刪除。如果設(shè)置了對(duì)象的最大生命周期時(shí)間,即使這個(gè)對(duì)象被反復(fù)頻繁訪問(wèn),也將從緩存中刪除。這個(gè)特性可以適用于一些周期性需要刷新的數(shù)據(jù),如來(lái)自數(shù)據(jù)庫(kù)的數(shù)據(jù)。 在 Cach 中除了 getObject() 方法的性能依據(jù)緩存大小,其他方法的性能都是比較快的。一個(gè) HashMap 用來(lái)實(shí)現(xiàn)快速尋找,兩個(gè) LinkedList 中一個(gè)以一定的訪問(wèn)順序來(lái)保存對(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)作如緩存清理、刪除、過(guò)期失效等都是在這個(gè)動(dòng)作中一起觸發(fā)實(shí)現(xiàn)的。 public class Cache implements Cacheable { /** * 因?yàn)?/span> System.currentTimeMillis() 執(zhí)行非常耗費(fèi)性能,因此如果 get 操作都執(zhí)行 * 這條語(yǔ)句將會(huì)形成性能瓶頸, 通過(guò)一個(gè)全局時(shí)間戳來(lái)實(shí)現(xiàn)每秒更新 * 當(dāng)然,這意味著在緩存過(guò)期時(shí)間計(jì)算上有一到幾秒的誤差 */ protected static long currentTime = CacheTimer.currentTime; //CacheObject 對(duì)象 protected HashMap cachedObjectsHash; //accessed LinkedList 最經(jīng)常訪問(wèn)的排列在最前面 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)是沒(méi)有 protected long maxLifetime = -1; // 緩存的擊中率,用于評(píng)測(cè)緩存效率 protected long cacheHits, cacheMisses = 0L;
public Cache() { // 構(gòu)造 HashMap. 默認(rèn) capacity 是 11 // 如果實(shí)際大小超過(guò) 11 , HashMap 將自動(dòng)擴(kuò)充,但是每次擴(kuò)充都 // 是性能開(kāi)銷(xiāo),因此期初要設(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; // 有可能緩存大小超過(guò)最大值,需要激活刪除清理動(dòng)作 cullCache(); } public synchronized int getNumElements() { return cachedObjectsHash.size(); }
/** * 增加一個(gè) Cacheable 對(duì)象 * 因?yàn)?/span> HashMap 不是線程安全的,所以操作方法要使用同步 * 如果使用 Hashtable 就不必同步 */ public synchronized void add(Object key, Cacheable object) { // 刪除已經(jīng)存在的 key remove(key); int objectSize = object.getSize(); // 如果被緩存對(duì)象的大小超過(guò)最大值,就放棄 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ì)象引用從來(lái)沒(méi)有被加入緩存中 * <li> 對(duì)象引用因?yàn)檫^(guò)期被清除 </ul> */ public synchronized Cacheable get(Object key) { // 清除過(guò)期緩存 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 類(lèi)型的整數(shù)。 4.2 緩存使用建立 LongCache 只是為了提高原來(lái)的 Cache 性能,本身無(wú)多大意義,可以將 LongCache 看成與 Cache 一樣的類(lèi)。 LongCache 的關(guān)鍵字 Key 是 Forum 、 ForumThread 以及 ForumMessage 等 long 類(lèi)型的 ID ,值 Value 是 Forum 、 ForumThread 以及 ForumMessage 等的對(duì)象。這些基本是通過(guò) DatabaseCacheManager 實(shí)現(xiàn)完成,在主要類(lèi) DbForumFactory 的初始化構(gòu)造時(shí),同時(shí)構(gòu)造了 DatabaseCacheManager 的實(shí)例 cacheManager 。 前面過(guò)濾器功能分析中, 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 類(lèi)。在其中包含了 5 種類(lèi)型的緩存,都是針對(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 ); } … } 從以上代碼看出, ForumCache 等對(duì)象生成都是以 LongCache 為基礎(chǔ)構(gòu)建的,以 ForumCache 為例,代碼如下: public class ForumCache extends DatabaseCache { // 以 Cache 構(gòu)建 ID 緩存 protected Cache forumIDCache = new Cache(128*1024, 6*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) { // 如果緩存沒(méi)有從數(shù)據(jù)庫(kù)中獲取 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) { // 如果緩存沒(méi)有 從數(shù)據(jù)庫(kù)獲得 long forumID = factory.getForumID(name); forumIDLong = new CacheableLong(forumID); // 生成一個(gè)緩存對(duì)象 forumIDCache.add(name, forumIDLong); } return get(forumIDLong.getLong()); } … } 由此可以看到, LongCache 封裝了 Cache 的核心功能,而 ForumCache 等類(lèi)則是在 LongCache 核心外又包裝了與應(yīng)用系統(tǒng)相關(guān)的操作,這有點(diǎn)類(lèi)似裝飾( Decorator )模式。 從中也可以看到 Cache 和 LongCache 兩種緩存的用法。 使用 Cache 時(shí)的關(guān)鍵字 Key 是任何字段。如上面代碼中的 String name ,如果用戶大量帖子主題查詢(xún)中, Key 是 query + blockID ,見(jiàn) DbForum 中的 getThreadBlock 方法;而值 Value 則是 Long 類(lèi)型的 ID ,如 ForumID 或 ThreadID 等。 LongCache 的關(guān)鍵字 Key 是 Long 類(lèi)型的 ID ,如 ForumID 或 ThreadID 等;而值 Value 則是 Forum 、 ForumThread 或 ForumMessage 等主要具體對(duì)象。 在實(shí)際使用中,大多數(shù)是根據(jù) ID 獲得對(duì)象。但有時(shí)并不是這樣,因此根據(jù)應(yīng)用區(qū)分了兩種 Cache ,這其實(shí)類(lèi)似數(shù)據(jù)庫(kù)的數(shù)據(jù)表,除了主關(guān)鍵字外還有其他關(guān)鍵字。 4.3 小結(jié)緩存中對(duì)象是原對(duì)象的映射,如何確保緩存中對(duì)象和原對(duì)象的一致性?即當(dāng)原對(duì)象發(fā)生變化時(shí),緩存中的對(duì)象也必須立即更新。這是緩存機(jī)制需要解決的另外一個(gè)基本技術(shù)問(wèn)題。 Jive 中是在原對(duì)象發(fā)生變化時(shí),立即進(jìn)行清除緩存中對(duì)象,如 ForumMessage 對(duì)象的創(chuàng)建。在 DbForumThread 的 AddMessage 方法中有下列語(yǔ)句: 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ù)庫(kù)中讀取,這是一種非常簡(jiǎn)單的緩存更新方式。 在復(fù)雜的系統(tǒng),例如有一臺(tái)以上的服務(wù)器運(yùn)行著 Jive 系統(tǒng)。如果一個(gè)用戶登陸一臺(tái)服務(wù)器后,通過(guò)這臺(tái)服務(wù)器增加新帖。那么按照上述原理,只能更新本服務(wù)器 JVM 中的緩存數(shù)據(jù),而其他服務(wù)器則無(wú)從得知這種改變,這就需要一種分布式的緩存機(jī)制。
到目前可以發(fā)現(xiàn) , 整個(gè) Jive 系統(tǒng)其實(shí)是圍繞 Forum 、 ForumThread 和 ForumMessage 等這些主要對(duì)象展開(kāi)的讀取、修改或創(chuàng)建等操作。由于這些對(duì)象原先持久化保存在數(shù)據(jù)庫(kù)中,為了提高性能和加強(qiáng)安全性, Jive 在這些對(duì)象外面分別實(shí)現(xiàn)兩層包裝,如圖 3-7 所示。 客戶端如果需要訪問(wèn)這些對(duì)象,首先要經(jīng)過(guò)它們的代理對(duì)象。進(jìn)行訪問(wèn)權(quán)限的檢查,然后再?gòu)木彺嬷蝎@取該對(duì)象。只有緩存不存在時(shí),才會(huì)從數(shù)據(jù)庫(kù)中獲取。 這套機(jī)制是大多數(shù)應(yīng)用系統(tǒng)都面臨的必須解決的基本功能,因此完全可以做成一個(gè)通用的可重復(fù)使用的框架。這樣在具體應(yīng)用時(shí),不必每個(gè)應(yīng)用系統(tǒng)都架設(shè)開(kāi)發(fā)這樣的機(jī)制。其實(shí) EJB 就是這樣一套框架,實(shí)體 Bean 都由緩存機(jī)制支持,而通過(guò)設(shè)定 ejb-jar.xml 可以實(shí)現(xiàn)訪問(wèn)權(quán)限控制,這些工作都直接由 EJB 容器實(shí)現(xiàn)了,不必在代碼中自己來(lái)實(shí)現(xiàn)。剩余的工作是調(diào)整 EJB 容器的參數(shù),使之適合應(yīng)用系統(tǒng)的具體要求,這些將在以后章節(jié)中討論。 在 Jive 中,圖 3-7 的機(jī)制是通過(guò)不同方式實(shí)現(xiàn)的?;旧鲜且慌涠J剑阂粋€(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)過(guò)程,客戶端可以根據(jù)不同的工廠輸入?yún)?shù)獲得具體不同的對(duì)象。這樣也許代碼結(jié)構(gòu)要更加抽象和緊湊, Java 的動(dòng)態(tài)代理 API 也許是實(shí)現(xiàn)這個(gè)工廠的主要技術(shù)基礎(chǔ)。有興趣者可以進(jìn)一步研究提煉。 |
|
來(lái)自: bluecrystal > 《java》