閱讀目錄 開始 Cache的基本用途 Cache的定義 Cache常見用法 Cache類的特點 快取項目的到期時間 快取項目的依賴關係 - 依賴其它快取項目 快取項目的依賴關係 - 檔案依賴 快取項目的移除優先順序 快取項目的移除通知 巧用快取項目的移除通知 實現【延遲操作】 巧用快取項目的移除通知 實現【自動載入設定檔】 檔案監視技術的選擇 各種緩衝方案的共存
許多做過程式效能最佳化的人,或者關注過程程式效能的人,應該都使用過各類緩衝技術。 而我今天所說的Cache是專指ASP.NET的Cache,我們可以使用HttpRuntime.Cache訪問到的那個Cache,而不是其它的緩衝技術。
以前我在【我心目中的Asp.net核心對象】 這篇部落格中簡單地提過它,今天我打算為它寫篇專題部落格,專門來談談它,因為它實在是太重要了。在這篇部落格中, 我不僅要介紹它的一些常見用法,還將介紹它的一些進階用法。 在上篇部落格【在.net中讀寫config檔案的各種方法】 的結尾處,我給大家留了一個問題,今天,我將在這篇部落格中給出一個我認為較為完美的答案。
本文提到的【延遲操作】方法(如:延遲合并寫入資料庫)屬於我的經驗總結,希望大家能喜歡這個思路。 回到頂部 Cache的基本用途
提到Cache,不得不說說它的主要功能:改善程式效能。
ASP.NET是一種動態網頁面技術,用ASP.NET技術做出來的網頁幾乎都是動態,所謂動態是指:頁面的內容會隨著不同的使用者或者持續更新的資料, 而呈現出不同的顯示結果。既然是動態,那麼這些動態內容是從哪裡來的呢。我想絕大多數網站都有自己的資料來源, 程式通過訪問資料來源擷取頁面所需的資料,然後根據一些商務規則的計算處理,最後變成適合頁面展示的內容。
由於這種動態網頁面技術通常需要從資料來源擷取資料,並經過一些計算邏輯,最終變成一些HTML代碼發給用戶端顯示。而這些計算過程顯然也是有成本的。 這些處理成本最直接可表現為影響伺服器的響應速度,尤其是當資料的處理過程變得複雜以及訪問量變大時,會變得比較明顯。 另一方面,有些資料並非時刻在發生變化,如果我們可以將一些變化不頻繁的資料的最終計算結果(包括頁面輸出)緩衝起來, 就可以非常明顯地提升程式的效能,緩衝的最常見且最重要的用途就體現在這個方面。 這也是為什麼一說到效能最佳化時,一般都將緩衝擺在第一位的原因。 我今天要說到的ASP.NET Cache也是可以實現這種緩衝的一種技術。 不過,它還有其它的一些功能,有些是其它緩衝技術所沒有的。
回到頂部 Cache的定義
在介紹Cache的用法前,我們先來看一下Cache的定義:(說明:我忽略了一些意義不大的成員)
ASP.NET為了方便我們訪問Cache,在HttpRuntime類中加了一個靜態屬性Cache,這樣,我們就可以在任意地方使用Cache的功能。 而且,ASP.NET還給它增加了二個“捷徑”:Page.Cache, HttpContext.Cache,我們通過這二個對象也可以訪問到HttpRuntime.Cache, 注意:這三者是在訪問同一個對象。Page.Cache訪問了HttpContext.Cache,而HttpContext.Cache又直接存取HttpRuntime.Cache 回到頂部 Cache常見用法
通常,我們使用Cache時,一般只有二個操作:讀,寫。
要從Cache中擷取一個快取項目,我們可以調用Cache.Get(key)方法,要將一個對象放入緩衝,我們可以調用Add, Insert方法。 然而,Add, Insert方法都有許多參數,有時我們或許只是想簡單地放入緩衝,一切接受預設值,那麼還可以調用它的預設索引器, 我們來看一下這個索引器是如何工作的:
public object this[string key]{ get { return this.Get(key); } set { this.Insert(key, value); }}
可以看到:讀緩衝,其實是在調用Get方法,而寫緩衝則是在調用Insert方法的最簡單的那個重載版本。
注意了:Add方法也可以將一個對象放入緩衝,這個方法有7個參數,而Insert也有一個簽名類似的重載版本, 它們有著類似的功能:將指定項添加到 System.Web.Caching.Cache 對象,該對象具有依賴項、到期和優先順序策略以及一個委託(可用於在從 Cache 移除插入項時通知應用程式)。 然而,它們有一點小的區別:當要加入的快取項目已經在Cache中存在時,Insert將會覆蓋原有的快取項目,而Add則不會修改原有快取項目。
也就是說:如果您希望某個快取項目一旦放入緩衝後,就不要再被修改,那麼調用Add確實可以防止後來的修改操作。 而調用Insert方法,則永遠會覆蓋已存在項(哪怕以前是調用Add加入的)。
從另一個角度看,Add的效果更像是 static readonly 的行為,而Insert的效果則像 static 的行為。
注意:我只是說【像】,事實上它們比一般的static成員有著更靈活的用法。
由於快取項目可以讓我們隨時訪問,看起來確實有點static成員的味道,但它們有著更進階的特性,比如: 緩衝到期(絕對到期,滑動到期),緩衝依賴(依賴檔案,依賴其它快取項目),移除優先順序,緩衝移除前後的通知等等。 後面我將會分別介紹這四大類特性。 回到頂部 Cache類的特點
Cache類有一個很難得的優點,用MSDN上的說話就是:
此類型是安全執行緒的。
為什麼這是個難得的優點呢。因為在.net中,絕大多數類在實現時,都只是保證靜態類型的方法是安全執行緒, 而不考慮執行個體方法是安全執行緒。這也算是一條基本的.NET設計規範原則。
對於那些類型,MSDN通常會用這樣的話來描述:
此類型的公用靜態(在 Visual Basic 中為 Shared)成員是安全執行緒的。但不能保證任何執行個體成員是安全執行緒的。
所以,這就意味著我們可以在任何地方讀寫Cache都不用擔心Cache的資料在多線程環境下的資料同步問題。 多線程編程中,最複雜的問題就是資料的同步問題,而Cache已經為我們解決了這些問題。
不過我要提醒您:ASP.NET本身就是一個多線程的編程模型,所有的請求是由線程池的線程來處理的。 通常,我們在多線程環境中為瞭解決資料同步問題,一般是採用鎖來保證資料同步, 自然地,ASP.NET也不例外,它為瞭解決資料的同步問題,內部也是採用了鎖。
說到這裡,或許有些人會想:既然只一個Cache的靜態執行個體,那麼這種鎖會不會影響並發。
答案是肯定的,有鎖肯定會在一定程度上影響並發,這是沒有辦法的事情。
然而,ASP.NET在實現Cache時,會根據CPU的個數建立多個緩衝容器,盡量可能地減小衝突, 以下就是Cache建立的核心過程:
internal static CacheInternal Create(){ CacheInternal internal2; int numSingleCaches = 0; if( numSingleCaches == 0 ) { uint numProcessCPUs = (uint)SystemInfo.GetNumProcessCPUs(); numSingleCaches = 1; for( numProcessCPUs -= 1; numProcessCPUs > 0; numProcessCPUs = numProcessCPUs >> 1 ) { numSingleCaches = numSingleCaches << 1; } } CacheCommon cacheCommon = new CacheCommon(); if( numSingleCaches == 1 ) { internal2 = new CacheSingle(cacheCommon, null, 0); } else { internal2 = new CacheMultiple(cacheCommon, numSingleCaches); } cacheCommon.SetCacheInternal(internal2); cacheCommon.ResetFromConfigSettings(); return internal2;}
說明:CacheInternal是個內部用的封裝類,Cache的許多操作都要由它來完成。
在上面的代碼中,numSingleCaches的計算過程很重要,如果上面代碼不容易理解,那麼請看我下面的範例程式碼:
static void Main(){ for( uint i = 1; i <= 20; i++ ) ShowCount(i); }static void ShowCount(uint numProcessCPUs){ int numSingleCaches = 1; for( numProcessCPUs -= 1; numProcessCPUs > 0; numProcessCPUs = numProcessCPUs >> 1 ) { numSingleCaches = numSingleCaches << 1; } Console.Write(numSingleCaches + ",");}
程式將會輸出:
1,2,4,4,8,8,8,8,16,16,16,16,16,16,16,16,32,32,32,32
CacheMultiple的建構函式如下:
internal CacheMultiple(CacheCommon cacheCommon, int numSingleCaches) : base(cacheCommon){ this._cacheIndexMask = numSingleCaches - 1; this._caches = new CacheSingle[numSingleCaches]; for (int i = 0; i < numSingleCaches; i++) { this._caches[i] = new CacheSingle(cacheCommon, this, i); }}
現在您應該明白了吧:CacheSingle其實是ASP.NET內部使用的緩衝容器,多個CPU時,它會建立多個緩衝容器。
在寫入時,它是如何定位這些容器的呢。請繼續看代碼:
internal CacheSingle GetCacheSingle(int hashCode){ hashCode = Math.Abs(hashCode); int index = hashCode & this._cacheIndexMask; return this._caches[index];}
說明:參數中的hashCode是直接調用我們傳的key.GetHashCode() ,GetHashCode是由Object類定義的。
所以,從這個角度看,雖然ASP.NET的Cache只有一個HttpRuntime.Cache靜態成員,但它的內部卻可能會包含多個緩衝容器, 這種設計可以在一定程度上減少並發的影響。
不管如何設計,在多線程環境下,共用一個容器,衝突是免不了的。如果您只是希望簡單的緩衝一些資料, 不需要Cache的許多進階特性,那麼,可以考慮不用Cache 。 比如:可以建立一個Dictionary或者Hashtable的靜態執行個體,它也可以完成一些基本的緩衝工作, 不過,我要提醒您:您要自己處理多線程訪問資料時的資料同步問題。
順便說一句:Hashtable.Synchronized(new Hashtable())也是一個安全執行緒的集合,如果想簡單點,可以考慮它。
接下來,我們來看一下Cache的進階特性,這些都是Dictionary或者Hashtable不能完成的。 回到頂部 快取項目的到期時間
ASP.NET支援二種快取項目的到期策略:絕對到期和滑動到期。
1. 絕對到期,這個容易理解:就是在緩衝放入Cache時,指定一個具體的時間。當時間到達指定的時間的時,快取項目自動從Cache中移除。
2. 滑動到期:某些快取項目,我們可能只希望在有使用者在訪問時,就盡量保留在緩衝中,只有當一段時間內使用者不再訪問該快取項目時,才移除它, 這樣可以最佳化記憶體的使用,因為這種策略可以保證緩衝的內容都是【很熱門】的。 作業系統的記憶體以及磁碟的緩衝不都是這樣設計的嗎。而這一非常有用的特性,Cache也為我們準備好了,只要在將快取項目放入緩衝時, 指定一個滑動到期時間就可以實現了。
以上二個選項分別對應Add, Insert方法中的DateTime absoluteExpiration, TimeSpan slidingExpiration這二個參數。
注意:這二個參數都是成對使用的,但不能同時指定它們為一個【有效】值,最多隻能一個參數值有效。 當不使用另一個參數項時,請用Cache類定義二個static readonly欄位賦值。
這二個參數比較簡單,我就不多說了,只說一句:如果都使用Noxxxxx這二個選項,那麼快取項目就一直儲存在緩衝中。(或許也會被移除)
回到頂部 快取項目的依賴關係 - 依賴其它快取項目
ASP.NET Cache有個很強大的功能,那就是緩衝依賴。一個快取項目可以依賴於另一個快取項目。 以下範例程式碼建立了二個快取項目,且它們間有依賴關係。首先請看頁面代碼:
<body> <p>Key1 的緩衝內容:<%= HttpRuntime.Cache["key1"] %></p> <hr /> <form action="CacheDependencyDemo.aspx" method="post"> <input type="submit" name="SetKey1Cache" value="設定Key1的值" /> <input type="submit" name="SetKey2Cache" value="設定Key2的值" /> </form></body>
頁面後台代碼:
public partial class CacheDependencyDemo : System.Web.UI.Page{ [SubmitMethod(AutoRedirect=true)] private void SetKey1Cache() { SetKey2Cache(); CacheDependency dep = new CacheDependency(null, new string