[轉]細說 ASP.NET Cache 及其高級用法

本文轉自:http://www.cnblogs.com/fish-li/archive/2011/12/27/2304063.htmlhtml

許多作過程序性能優化的人,或者關注過程程序性能的人,應該都使用過各種緩存技術。 而我今天所說的Cache是專指ASP.NET的Cache,咱們可使用HttpRuntime.Cache訪問到的那個Cache,而不是其它的緩存技術。ajax

之前我在【我心目中的Asp.net核心對象】 這篇博客中簡單地提過它,今天我打算爲它寫篇專題博客,專門來談談它,由於它實在是過重要了。在這篇博客中, 我不只要介紹它的一些常見用法,還將介紹它的一些高級用法。 在上篇博客【在.net中讀寫config文件的各類方法】 的結尾處,我給你們留了一個問題,今天,我將在這篇博客中給出一個我認爲較爲完美的答案。數據庫

本文提到的【延遲操做】方法(如:延遲合併寫入數據庫)屬於個人經驗總結,但願你們能喜歡這個思路。編程

Cache的基本用途

提到Cache,不得不說說它的主要功能:改善程序性能。 ASP.NET是一種動態頁面技術,用ASP.NET技術作出來的網頁幾乎都是動態的,所謂動態是指:頁面的內容會隨着不一樣的用戶或者持續更新的數據, 而呈現出不一樣的顯示結果。既然是動態的,那麼這些動態的內容是從哪裏來的呢?我想絕大多數網站都有本身的數據源, 程序經過訪問數據源獲取頁面所需的數據,而後根據一些業務規則的計算處理,最後變成適合頁面展現的內容。緩存

因爲這種動態頁面技術一般須要從數據源獲取數據,並通過一些計算邏輯,最終變成一些HTML代碼發給客戶端顯示。而這些計算過程顯然也是有成本的。 這些處理成本最直接可表現爲影響服務器的響應速度,尤爲是當數據的處理過程變得複雜以及訪問量變大時,會變得比較明顯。 另外一方面,有些數據並不是時刻在發生變化,若是咱們能夠將一些變化不頻繁的數據的最終計算結果(包括頁面輸出)緩存起來, 就能夠很是明顯地提高程序的性能,緩存的最多見且最重要的用途就體如今這個方面。 這也是爲何一說到性能優化時,通常都將緩存擺在第一位的緣由。 我今天要說到的ASP.NET Cache也是能夠實現這種緩存的一種技術。 不過,它還有其它的一些功能,有些是其它緩存技術所沒有的。安全

 

Cache的定義

在介紹Cache的用法前,咱們先來看一下Cache的定義:(說明:我忽略了一些意義不大的成員) 性能優化

// 實現用於 Web 應用程序的緩存。沒法繼承此類。
public sealed class Cache : IEnumerable { // 用於 Cache.Insert(...) 方法調用中的 absoluteExpiration 參數中以指示項從不過時。 public static readonly DateTime NoAbsoluteExpiration; // 用做 Cache.Insert(...) 或 Cache.Add(...) // 方法調用中的 slidingExpiration 參數,以禁用可調過時。 public static readonly TimeSpan NoSlidingExpiration; // 獲取或設置指定鍵處的緩存項。 public object this[string key] { get; set; } // 將指定項添加到 System.Web.Caching.Cache 對象,該對象具備依賴項、過時和優先級策略 // 以及一個委託(可用於在從 Cache 移除插入項時通知應用程序)。 public object Add(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback); // 從 System.Web.Caching.Cache 對象檢索指定項。 // key: 要檢索的緩存項的標識符。 // 返回結果: 檢索到的緩存項,未找到該鍵時爲 null。 public object Get(string key); public void Insert(string key, object value); public void Insert(string key, object value, CacheDependency dependencies); public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration); // 摘要: // 向 System.Web.Caching.Cache 對象中插入對象,後者具備依賴項、過時和優先級策略 // 以及一個委託(可用於在從 Cache 移除插入項時通知應用程序)。 // // 參數: // key: // 用於引用該對象的緩存鍵。 // // value: // 要插入緩存中的對象。 // // dependencies: // 該項的文件依賴項或緩存鍵依賴項。當任何依賴項更改時,該對象即無效, // 並從緩存中移除。若是沒有依賴項,則此參數包含 null。 // // absoluteExpiration: // 所插入對象將過時並被從緩存中移除的時間。 // 若是使用絕對過時,則 slidingExpiration 參數必須爲 Cache.NoSlidingExpiration。 // // slidingExpiration: // 最後一次訪問所插入對象時與該對象過時時之間的時間間隔。若是該值等效於 20 分鐘, // 則對象在最後一次被訪問 20 分鐘以後將過時並被從緩存中移除。若是使用可調過時,則 // absoluteExpiration 參數必須爲 System.Web.Caching.Cache.NoAbsoluteExpiration。 // // priority: // 該對象相對於緩存中存儲的其餘項的成本,由 System.Web.Caching.CacheItemPriority 枚舉表示。 // 該值由緩存在退出對象時使用;具備較低成本的對象在具備較高成本的對象以前被從緩存移除。 // // onRemoveCallback: // 在從緩存中移除對象時將調用的委託(若是提供)。 // 當從緩存中刪除應用程序的對象時,可以使用它來通知應用程序。 // // 異常: // System.ArgumentException: // 爲要添加到 Cache 中的項設置 absoluteExpiration 和 slidingExpiration 參數。 // // System.ArgumentNullException: // key 或 value 參數爲 null。 // // System.ArgumentOutOfRangeException: // 將 slidingExpiration 參數設置爲小於 TimeSpan.Zero 或大於一年的等效值。 public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback); // 從應用程序的 System.Web.Caching.Cache 對象移除指定項。 public object Remove(string key); // 將對象與依賴項策略、到期策略和優先級策略 // 以及可用來在從緩存中移除項【以前】通知應用程序的委託一塊兒插入到 Cache 對象中。 // 注意:此方法受如下版本支持:3.5 SP一、3.0 SP一、2.0 SP1 public void Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemUpdateCallback onUpdateCallback); } 

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[] { "key2" }); HttpRuntime.Cache.Insert("key1", DateTime.Now.ToString(), dep, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration); } [SubmitMethod(AutoRedirect=true)] private void SetKey2Cache() { HttpRuntime.Cache.Insert("key2", Guid.NewGuid().ToString()); } } 

當運行這個示例頁面時,運行結果以下圖所示, 點擊按鈕【設置Key1的值】時,將會出現緩存項的內容(左圖)。點擊按鈕【設置Key2的值】時,此時將獲取不到緩存項的內容(右圖)。

根據結果並分析代碼,咱們能夠看出,在建立Key1的緩存項時,咱們使用了這種緩存依賴關係:

CacheDependency dep = new CacheDependency(null, new string[] { "key2" }); 

因此,當咱們更新Key2的緩存項時,Key1的緩存就失效了(不存在)。

不要小看了這個示例。的確,僅看這幾行示例代碼,或許它們實在是沒有什麼意義。 那麼,我就舉個實際的使用場景來講明它的使用價值。

上面這幅圖是我寫的一個小工具。在示意圖中,左下角是一個緩存表CacheTable,它由一個叫Table1BLL的類來維護。 CacheTable的數據來源於Table1,由Table1.aspx頁面顯示出來。 同時,ReportA, ReportB的數據也主要來源於Table1,因爲Table1的訪問幾乎絕大多數都是讀多寫少,因此,我將Table1的數據緩存起來了。 並且,ReportA, ReportB這二個報表採用GDI直接畫出(由報表模塊生成,可認是Table1BLL的上層類),鑑於這二個報表的瀏覽次數較多且數據源是讀多寫少, 所以,這二個報表的輸出結果,我也將它們緩存起來。

在這個場景中,咱們能夠想像一下:若是但願在Table1的數據發生修改後,如何讓二個報表的緩存結果失效? 讓Table1BLL去通知那二個報表模塊,仍是Table1BLL去直接刪除二個報表的緩存? 其實,無論是選擇前者仍是後者,當之後還須要在Table1的CacheTable上作其它的緩存實現時(多是其它的新報表), 那麼,勢必都要修改Table1BLL,那絕對是個失敗的設計。 這也算是模塊間耦合的所帶來的惡果。

幸虧,ASP.NET Cache支持一種叫作緩存依賴的特性,咱們只須要讓Table1BLL公開它緩存CacheTable的KEY就能夠了(假設KEY爲 CacheTableKey), 而後,其它的緩存結果若是要基於CacheTable,設置一下對【CacheTableKey】的依賴就能夠實現這樣的效果: 當CacheTable更新後,被依賴的緩存結果將會自動清除。這樣就完全地解決了模塊間的緩存數據依賴問題。

緩存項的依賴關係 - 文件依賴

在上篇博客【在.net中讀寫config文件的各類方法】的結尾, 我給你們留了一個問題: 我但願在用戶修改了配置文件後,程序能馬上以最新的參數運行,並且不用重啓網站。 今天我就來回答這個問題,並給出所需的所有實現代碼。

首先,我要說明一點:上次博客的問題,雖然解決方案與Cache的文件依賴有關,但還需與緩存的移除通知配合使用才能完美的解決問題。 爲了便於內容的安排,我先使用Cache的文件依賴來簡單的實現一個粗糙的版本,在本文的後續部分再來完善這個實現。

先來看個粗糙的版本。假如個人網站中有這樣一個配置參數類型:

/// <summary>
/// 模擬網站所需的運行參數 /// </summary> public class RunOptions { public string WebSiteUrl; public string UserName; } 

我能夠將它配置在這樣一個XML文件中:

<?xml version="1.0" encoding="utf-8"?> <RunOptions xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <WebSiteUrl>http://www.cnblogs.com/fish-li</WebSiteUrl> <UserName>fish li</UserName> </RunOptions> 

再來一個用於顯示運行參數的頁面:

<body> <p>WebSiteUrl: <%= WebSiteApp.RunOptions.WebSiteUrl %></p> <p>UserName: <%= WebSiteApp.RunOptions.UserName %></p> </body> 

下面的代碼就能夠實現:在XML修改後,瀏覽頁面就能當即看到最新的參數值

public static class WebSiteApp { private static readonly string RunOptionsCacheKey = Guid.NewGuid().ToString(); public static RunOptions RunOptions { get { // 首先嚐試從緩存中獲取運行參數 RunOptions options = HttpRuntime.Cache[RunOptionsCacheKey] as RunOptions; if( options == null ) { // 緩存中沒有,則從文件中加載 string path = HttpContext.Current.Server.MapPath("~/App_Data/RunOptions.xml"); options = RwConfigDemo.XmlHelper.XmlDeserializeFromFile<RunOptions>(path, Encoding.UTF8); // 把從文件中讀到的結果放入緩存,並設置與文件的依賴關係。 CacheDependency dep = new CacheDependency(path); // 若是您的參數較複雜,與多個文件相關,那麼也可使用下面的方式,傳遞多個文件路徑。 //CacheDependency dep = new CacheDependency(new string[] { path }); HttpRuntime.Cache.Insert(RunOptionsCacheKey, options, dep); } return options; } } } 

注意:這裏仍然是在使用CacheDependency,只是咱們如今是給它的構造函數的第一個參數傳遞要依賴的文件名。

在即將結束對緩存的依賴介紹以前,還要補充二點: 1. CacheDependency還支持【嵌套】,即:CacheDependency的構造函數中支持傳入其它的CacheDependency實例,這樣能夠構成一種很是複雜的樹狀依賴關係。 2. 緩存依賴的對象還能夠是SQL SERVER,具體可參考SqlCacheDependency

緩存項的移除優先級

緩存的作法有不少種,一個靜態變量也能夠稱爲是一個緩存。一個靜態的集合就是一個緩存的容器了。 我想不少人都用Dictionary,List,或者Hashtable作過緩存容器,咱們可使用它們來保存各類數據,改善程序的性能。 通常狀況下,若是咱們直接使用這類集合去緩存各種數據,那麼,那些數據所佔用的內存將不會被回收,哪怕它們的使用機會並非不少。 當緩存數據愈來愈多時,它們所消耗的內存天然也會愈來愈多。那麼,能不能在內存不充足時,釋放掉一些訪問不頻繁的緩存項呢?

這個問題也確實是個較現實的問題。雖然,使用緩存會使用程序運行更快,可是,咱們數據會無限大,不可能通通緩存起來, 畢竟,內存空間是有限的。所以,咱們可使用前面所說的基於一段時間內再也不訪問就刪除的策略來解決這個問題。 然而,在咱們編碼時,根本不知道咱們的程序會運行在什麼配置標準的計算機上,所以,根本不可能會對內存的大小做出任何假設, 此時,咱們可能會但願當緩存佔用過多的內存時,且當內存不夠時,能自動移除一些不過重要的緩存項,這或許也比較有意義。

對於這個需求,在.net framework提供了二種解決辦法,一種是使用WeakReference類,另外一種是使用Cache 。 不過,既然咱們是在使用ASP.NET,選擇Cache固然會更方便。 在Cache的Add, Insert方法的某些重載版本中,能夠指定緩存項的保存優先級策略,由參數CacheItemPriority priority來傳入。 其中,CacheItemPriority是一個枚舉類型,它包含了以下枚舉值:

// 指定 Cache 對象中存儲的項的相對優先級。
public enum CacheItemPriority { // 在服務器釋放系統內存時,具備該優先級級別的緩存項最有可能被從緩存刪除。 Low = 1, // 在服務器釋放系統內存時,具備該優先級級別的緩存項比分配了 CacheItemPriority.Normal // 優先級的項更有可能被從緩存刪除。 BelowNormal = 2, // 在服務器釋放系統內存時,具備該優先級級別的緩存項頗有可能被從緩存刪除, // 其被刪除的可能性僅次於具備 CacheItemPriority.Low // 或 CacheItemPriority.BelowNormal 優先級的那些項。這是默認選項。 Normal = 3, // 緩存項優先級的默認值爲 CacheItemPriority.Normal。 Default = 3, // 在服務器釋放系統內存時,具備該優先級級別的緩存項被刪除的可能性 // 比分配了 CacheItemPriority.Normal 優先級的項要小。 AboveNormal = 4, // 在服務器釋放系統內存時,具備該優先級級別的緩存項最不可能被從緩存刪除。 High = 5, // 在服務器釋放系統內存時,具備該優先級級別的緩存項將不會被自動從緩存刪除。 // 可是,具備該優先級級別的項會根據項的絕對到期時間或可調整到期時間與其餘項一塊兒被移除。 NotRemovable = 6, } 

說明:當咱們調用Cache的Add, Insert方法時,若是不指定CacheItemPriority選項,最終使用Normal所表明的優先級。 若是咱們但願將某個可能不過重要的數據放入緩存時,能夠指定優先級爲Low或者BelowNormal。 若是想讓緩存項在內存不足時,也不會被移除(除非到期或者依賴項有改變),可以使用NotRemovable。

顯然,咱們可使用這個特性來控制緩存對內存壓力的影響。 其它的緩存方案,如static Collection + WeakReference也較難實現這樣靈活的控制。

緩存項的移除通知

ASP.NET Cache與一些static變量所實現的緩存效果並不相同,它的緩存項是能夠根據一些特定的條件失效的,那些失效的緩存將會從內存中移除。 雖然,某些移除條件並非由咱們的代碼直接解發的,但ASP.NET仍是提供一種方法讓咱們能夠在緩存項在移除時,能通知咱們的代碼。

注意哦:ASP.NET Cache支持移除【前】通知 和 移除【後】通知二種通知方式。

咱們能夠在調用Add, Insert方法時,經過參數onRemoveCallback傳遞一個CacheItemRemovedCallback類型的委託,以便在移除指定的緩存項時, 可以通知咱們。這個委託的定義以下:

/// <summary>
/// 定義在從 System.Web.Caching.Cache 移除緩存項時通知應用程序的回調方法。 /// </summary> /// <param name="key">從緩存中移除的鍵(當初由Add, Insert傳入的)。</param> /// <param name="value">與從緩存中移除的鍵關聯的緩存項(當初由Add, Insert傳入的)。</param> /// <param name="reason">從緩存移除項的緣由。 </param> public delegate void CacheItemRemovedCallback(string key, object value, CacheItemRemovedReason reason); // 指定從 System.Web.Caching.Cache 對象移除項的緣由。 public enum CacheItemRemovedReason { // 該項是經過指定相同鍵的 Cache.Insert(System.String,System.Object) // 方法調用或 Cache.Remove(System.String) 方法調用從緩存中移除的。 Removed = 1, // 從緩存移除該項的緣由是它已過時。 Expired = 2, // 之因此從緩存中移除該項,是由於系統要經過移除該項來釋放內存。 Underused = 3, // 從緩存移除該項的緣由是與之關聯的緩存依賴項已更改。 DependencyChanged = 4, } 

委託的各個參數的含義以及移除緣由,在註釋中都有明確的解釋,我也再也不重複了。 我想:有不少人知道Cache的Add, Insert方法有這個參數,也知道有這個委託,可是,它們有什麼用呢?  在後面的二個小節中,我將提供二個示例來演示這一強大的功能。

一般,咱們會如下面這種方式從Cache中獲取結果:

RunOptions options = HttpRuntime.Cache[RunOptionsCacheKey] as RunOptions; if( options == null ) { // 緩存中沒有,則從文件中加載 // .................................. HttpRuntime.Cache.Insert(RunOptionsCacheKey, options, dep); } return options; 

這其實也是一個慣用法了:先嚐試從緩存中獲取,若是沒有,則從數據源中加載,並再次放入緩存。

爲何會在訪問Cache時返回null呢?答案無非就是二種緣由:1. 根本沒有放入Cache,2. 緩存項失效被移除了。 這種寫法自己是沒有問題,但是,若是從數據源中加載數據的時間較長,狀況會怎樣呢? 顯然,會影響後面第一次的訪問請求。您有沒有想過,若是緩存項能一直放在Cache中,那不就能夠了嘛。 是的,一般來講,只要您在將一個對象放入Cache時,不指定過時時間,不指定緩存依賴,且設置爲永不移除,那麼對象確實會一直在Cache中, 但是,過時時間和緩存依賴也頗有用哦。如何能兩者兼得呢?

爲了解決這個問題,微軟在.net framework的3.5 SP一、3.0 SP一、2.0 SP1版本中,加入了【移除前通知】功能,不過,這個方法僅受Insert支持, 隨之而來的還有一個委託和一個移除緣由的枚舉定義:

/// <summary>
/// 定義一個回調方法,用於在從緩存中移除緩存項以前通知應用程序。 /// </summary> /// <param name="key">要從緩存中移除的項的標識符。</param> /// <param name="reason">要從緩存中移除項的緣由。</param> /// <param name="expensiveObject">此方法返回時,包含含有更新的緩存項對象。</param> /// <param name="dependency">此方法返回時,包含新的依賴項的對象。</param> /// <param name="absoluteExpiration">此方法返回時,包含對象的到期時間。</param> /// <param name="slidingExpiration">此方法返回時,包含對象的上次訪問時間和對象的到期時間之間的時間間隔。</param> public delegate void CacheItemUpdateCallback(string key, CacheItemUpdateReason reason, out object expensiveObject, out CacheDependency dependency, out DateTime absoluteExpiration, out TimeSpan slidingExpiration); /// <summary> /// 指定要從 Cache 對象中移除緩存項的緣由。 /// </summary> public enum CacheItemUpdateReason { /// <summary> /// 指定要從緩存中移除項的緣由是絕對到期或可調到期時間間隔已到期。 /// </summary> Expired = 1, /// <summary> /// 指定要從緩存中移除項的緣由是關聯的 CacheDependency 對象發生了更改。 /// </summary> DependencyChanged = 2, } 

注意:CacheItemUpdateReason這個枚舉只有二項。緣由請看MSDN的解釋:

與 CacheItemRemovedReason 枚舉不一樣,此枚舉不包含 Removed 或 Underused 值。可更新的緩存項是不可移除的,於是毫不會被 ASP.NET 自動移除,即便須要釋放內存也是如此。

再一次提醒:有時咱們確實須要緩存失效這個特性,可是,緩存失效後會被移除。 雖然咱們可讓後續的請求在獲取不到緩存數據時,從數據源中加載,也能夠在CacheItemRemovedCallback回調委託中, 從新加載緩存數據到Cache中,可是在數據的加載過程當中,Cache並不包含咱們所指望的緩存數據,若是加載時間越長,這種【空缺】效果也會越明顯。 這樣會影響(後續的)其它請求的訪問。爲了保證讓咱們所指望的緩存數據可以一直存在於Cahce中,且仍有失效機制,咱們可使用【移除前通知】功能。

巧用緩存項的移除通知 實現【延遲操做】

我看過一些ASP.NET的書,也看過一些人寫的關於Cache方面的文章,基本上,要麼是一帶而過,要麼只是舉個毫無實際意義的示例。 惋惜啊,這麼強大的特性,我不多見到有人把它用起來。

今天,我就舉個有實際意義的示例,再現Cache的強大功能!

我有這樣一個頁面,可讓用戶調整(上下移動)某個項目分支記錄的上線順序:

當用戶須要調整某條記錄的位置時,頁面會彈出一個對話框,要求輸入一個調整緣由,並會發郵件通知全部相關人員。

因爲界面的限制,一次操做(點擊上下鍵頭)只是將一條記錄移動一個位置,當要對某條記錄執行跨越多行移動時,必須進行屢次移動。 考慮到操做的方便性以及不受重複郵件的影響,程序須要實現這樣一個需求: 頁面只要求輸入一次緣由即可以對一條記錄執行屢次移動操做,而且不要屢次發重複郵件,並且要求將最後的移動結果在郵件中發出來。

這個需求很合理,畢竟誰都但願操做簡單。

那麼如何實現這個需求呢?這裏要從二個方面來實現,首先,在頁面上咱們應該要完成這個功能,對一條記錄只彈一次對話框。 因爲頁面與服務端的交互所有采用Ajax方式進行(不刷新),狀態能夠採用JS變量來維持,因此這個功能在頁面中是很容易實現。 再來看一下服務端,因爲服務端並無任何狀態,固然也能夠由頁面把它的狀態傳給服務端,可是,哪次操做是最後一次呢? 顯然,這是沒法知道的,最後只能修改需求,若是用戶在2分鐘以內再也不操做某條記錄時,便將最近一次操做視爲最後一次操做。

基於新的需求,程序必須記錄用戶的最近一次操做,以便在2分鐘不操做後,發出一次郵件,但要包含第一次輸入的緣由, 還應包含最後的修改結果哦。

該怎麼實現這個需求呢? 我當即就想到了ASP.NET Cache,由於我瞭解它,知道它能幫我完成這個功能。下面我來講說在服務端是如何實現的。

整個實現的思路是: 1. 客戶端頁面仍是每次將記錄的RowGuid, 調整方向,調整緣由,這三個參數發到服務端。 2. 服務端在處理完順序調整操做後,將要發送的郵件信息Insert到Cache中,同時提供slidingExpiration和onRemoveCallback參數。 3. 在CacheItemRemovedCallback回調委託中,忽略CacheItemRemovedReason.Removed的通知,若是是其它的通知,則發郵件。

爲了便於理解,我特地爲你們準備了一個示例。整個示例由三部分組成:一個頁面,一個JS文件,服務端代碼。先來看頁面代碼:

<body> <p> 爲了簡單,示例頁面只處理一條記錄,且將記錄的RowGuid直接顯示出來。<br /> 實際場景中,這個RowGuid應該能夠從一個表格的【當前選擇行】中獲取到。 </p> <p> 當前選擇行的 RowGuid = <span id="spanRowGuid"><%= Guid.NewGuid().ToString() %></span><br /> 當前選擇行的 Sequence= <span id="spanSequence">0</span> </p> <p><input type="button" id="btnMoveUp" value="上移" /> <input type="button" id="btnMoveDown" value="下移" /> </p> </body> 

頁面的顯示效果以下:

處理頁面中二個按鈕的JS代碼以下:

// 用戶輸入的調整記錄的緣由
var g_reason = null; $(function(){ $("#btnMoveUp").click( function() { MoveRec(-1); } ); $("#btnMoveDown").click( function() { MoveRec(1); } ); }); function MoveRec(direction){ if( ~~($("#spanSequence").text()) + direction < 0 ){ alert("已經不能上移了。"); return; } if( g_reason == null ){ g_reason = prompt("請輸入調整記錄順序的緣由:", "因爲什麼什麼緣由,我要調整..."); if( g_reason == null ) return; } $.ajax({ url: "/AjaxDelaySendMail/MoveRec.fish", data: { RowGuid: $("#spanRowGuid").text(), Direction: direction, Reason: g_reason }, type: "POST", dataType: "text", success: function(responseText){ $("#spanSequence").text(responseText); } }); } 

說明:在服務端,我使用了我在【用Asp.net寫本身的服務框架】那篇博客中提供的服務框架, 服務端的所有代碼是這個樣子的:(注意代碼中的註釋)

/// <summary>
/// 移動記錄的相關信息。 /// </summary> public class MoveRecInfo { public string RowGuid; public int Direction; public string Reason; } [MyService] public class AjaxDelaySendMail { [MyServiceMethod] public int MoveRec(MoveRecInfo info) { // 這裏就不驗證從客戶端傳入的參數了。實際開發中這個是必須的。 // 先來調整記錄的順序,示例程序沒有數據庫,就用Cache來代替。 int sequence = 0; int.TryParse(HttpRuntime.Cache[info.RowGuid] as string, out sequence); // 簡單地示例一下調整順序。 sequence += info.Direction; HttpRuntime.Cache[info.RowGuid] = sequence.ToString(); string key = info.RowGuid +"_DelaySendMail"; // 這裏我不直接發郵件,而是把這個信息放入Cache中,並設置2秒的滑過過時時間,並指定移除通知委託 // 將操做信息放在緩存,而且以覆蓋形式放入,這樣即可以實現保存最後狀態。 // 注意:這裏我用Insert方法。 HttpRuntime.Cache.Insert(key, info, null, Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(2.0), CacheItemPriority.NotRemovable, MoveRecInfoRemovedCallback); return sequence; } private void MoveRecInfoRemovedCallback(string key, object value, CacheItemRemovedReason reason) { if( reason == CacheItemRemovedReason.Removed ) return; // 忽略後續調用HttpRuntime.Cache.Insert()所觸發的操做 // 能運行到這裏,就表示是確定是緩存過時了。 // 換句話說就是:用戶2分鐘再也沒操做過了。 // 從參數value取回操做信息 MoveRecInfo info = (MoveRecInfo)value; // 這裏能夠對info作其它的處理。 // 最後發一次郵件。整個延遲發郵件的過程就處理完了。 MailSender.SendMail(info); } } 

爲了能讓JavaScript能直接調用C#中的方法,還須要在web.config中加入以下配置:

<httpHandlers> <add path="*.fish" verb="*" validate="false" type="MySimpleServiceFramework.AjaxServiceHandler"/> </httpHandlers> 

好了,示例代碼就是這些。若是您有興趣,能夠在本文的結尾處下載這些示例代碼,本身親自感覺一下利用Cache實現的【延遲處理】的功能。

其實這種【延遲處理】的功能是頗有用的,好比還有一種適用場景:有些數據記錄可能須要頻繁更新,若是每次更新都去寫數據庫,確定會對數據庫形成必定的壓力, 但因爲這些數據也不是特別重要,所以,咱們能夠利用這種【延遲處理】來將寫數據庫的時機進行合併處理, 最終咱們能夠實現:將屢次的寫入變成一次或者少許的寫入操做,我稱這樣效果爲:延遲合併寫入

這裏我就對數據庫的延遲合併寫入提供一個思路:將須要寫入的數據記錄放入Cache,調用Insert方法並提供slidingExpiration和onRemoveCallback參數, 而後在CacheItemRemovedCallback回調委託中,模仿我前面的示例代碼,將屢次變成一次。不過,這樣可能會有一個問題:若是數據是一直在修改,那麼就一直不會寫入數據庫。 最後若是網站重啓了,數據可能會丟失。若是擔憂這個問題,那麼,能夠在回調委託中,遇到CacheItemRemovedReason.Removed時,使用計數累加的方式,當到達必定數量後, 再寫入數據庫。好比:遇到10次CacheItemRemovedReason.Removed我就寫一次數據庫,這樣就會將原來須要寫10次的數據庫操做變成一次了。 固然了,若是是其它移除緣由,寫數據庫老是必要的。注意:對於金額這類敏感的數據,絕對不要使用這種方法。

再補充二點: 1. 當CacheItemRemovedCallback回調委託被調用時,緩存項已經不在Cache中了。 2. 在CacheItemRemovedCallback回調委託中,咱們還能夠將緩存項從新放入緩存。 有沒有想過:這種設計能夠構成一個循環?若是再結合參數slidingExpiration即可實現一個定時器的效果。

關於緩存的失效時間,我要再提醒一點:經過absoluteExpiration, slidingExpiration參數所傳入的時間,當緩存時間生效時,緩存對象並不會當即移除, ASP.NET Cache大約以20秒的頻率去檢查這些已過期的緩存項。

巧用緩存項的移除通知 實現【自動加載配置文件】

在本文的前部分的【文件依賴】小節中,有一個示例演示了:當配置文件更新後,頁面能夠顯示最新的修改結果。 在那個示例中,爲了簡單,我直接將配置參數放在Cache中,每次使用時再從Cache中獲取。 若是配置參數較多,這種作法或許也會影響性能,畢竟配置參數並不會常常修改,若是能直接訪問一個靜態變量就能獲取到,應該會更快。 一般,咱們可能會這樣作:

private static RunOptions s_RunOptions; public static RunOptions RunOptions { // s_RunOptions 的初始化放在Init方法中了,會在Global.asax的Application_Start事件中調用。 get { return s_RunOptions; } } public static RunOptions LoadRunOptions() { string path = Path.Combine(AppDataPath, "RunOptions.xml"); return RwConfigDemo.XmlHelper.XmlDeserializeFromFile<RunOptions>(path, Encoding.UTF8); } 

可是,這種作法有一缺點就是:不能在配置文件更新後,自動加載最新的配置結果。

爲了解決這個問題,咱們可使用Cache提供的文件依賴以及移除通知功能。 前面的示例演示了移除後通知功能,這裏我再演示一下移除前通知功能。 說明:事實上,完成這個功能,能夠仍然使用移除後通知,只是移除前通知我尚未演示,然而,這裏使用移除前通知並無顯示它的獨有的功能。

下面的代碼演示了在配置文件修改後,自動更新運行參數的實現方式:(注意代碼中的註釋)

private static int s_RunOptionsCacheDependencyFlag = 0; public static RunOptions LoadRunOptions() { string path = Path.Combine(AppDataPath, "RunOptions.xml"); // 注意啦:訪問文件是可能會出現異常。不要學我,我寫的是示例代碼。 RunOptions options = RwConfigDemo.XmlHelper.XmlDeserializeFromFile<RunOptions>(path, Encoding.UTF8); int flag = System.Threading.Interlocked.CompareExchange(ref s_RunOptionsCacheDependencyFlag, 1, 0); // 確保只調用一次就能夠了。 if( flag == 0 ) { // 讓Cache幫咱們盯住這個配置文件。 CacheDependency dep = new CacheDependency(path); HttpRuntime.Cache.Insert(RunOptionsCacheKey, "Fish Li", dep, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, RunOptionsUpdateCallback); } return options; } public static void RunOptionsUpdateCallback( string key, CacheItemUpdateReason reason, out object expensiveObject, out CacheDependency dependency, out DateTime absoluteExpiration, out TimeSpan slidingExpiration) { // 注意哦:在這個方法中,不要出現【未處理異常】,不然緩存對象將被移除。 // 說明:這裏我並不關心參數reason,由於我根本就沒有使用過時時間 // 因此,只有一種緣由:依賴的文件發生了改變。 // 參數key我也不關心,由於這個方法是【專用】的。 expensiveObject = "http://www.cnblogs.com/fish-li/"; dependency = new CacheDependency(Path.Combine(AppDataPath, "RunOptions.xml")); absoluteExpiration = Cache.NoAbsoluteExpiration; slidingExpiration = Cache.NoSlidingExpiration; // 從新加載配置參數 s_RunOptions = LoadRunOptions(); } 

改動很小,只是LoadRunOptions方法作了修改了而已,可是效果卻很酷。

還記得我在上篇博客【在.net中讀寫config文件的各類方法】的結尾處留下來的問題嗎? 這個示例就是個人解決方案。

文件監視技術的選擇

對於文件監視,我想有人或許會想到FileSystemWatcher。正好我就來講說關於【文件監視技術】的選擇問題。 說明,本文全部結論均爲我我的的觀點,僅供參考。

這個組件,早在作WinForm開發時就用過了,對它也是印象比較深的。 它有一個包裝很差的地方是:事件會重複發出。好比:一次文件的保存操做,它卻引起了二次事件。 什麼,你不信? 正好,我還準備了一個示例程序。

說明:圖片中顯示了發生過二次事件,但我只是在修改了文件後,作了一次保存操做而已。 本文的結尾處有個人示例程序,您能夠本身去試一下。這裏爲了方便,仍是貼出相關代碼:

private void Form1_Shown(object sender, EventArgs e) { this.fileSystemWatcher1.Path = Environment.CurrentDirectory; this.fileSystemWatcher1.Filter = "RunOptions.xml"; this.fileSystemWatcher1.NotifyFilter = System.IO.NotifyFilters.LastWrite; this.fileSystemWatcher1.EnableRaisingEvents = true; } private void fileSystemWatcher1_Changed(object sender, System.IO.FileSystemEventArgs e) { string message = string.Format("{0} {1}.", e.Name, e.ChangeType); this.listBox1.Items.Add(message); } 

對於這個類的使用,只想說一點:會引起的事件不少,所以必定要注意過濾。如下引用MSDN的一段說明:

Windows 操做系統在 FileSystemWatcher 建立的緩衝區中通知組件文件發生更改。若是短期內有不少更改,則緩衝區可能會溢出。這將致使組件失去對目錄更改的跟蹤,而且它將只提供通常性通知。使用 InternalBufferSize 屬性來增長緩衝區大小的開銷較大,由於它來自沒法換出到磁盤的非頁面內存,因此應確保緩衝區大小適中(儘可能小,但也要有足夠大小以便不會丟失任何文件更改事件)。若要避免緩衝區溢出,請使用 NotifyFilter 和 IncludeSubdirectories 屬性,以即可以篩選掉不想要的更改通知。

幸運的是,ASP.NET Cache並無使用這個組件,咱們不用擔憂文件依賴而引起的重複操做問題。 它直接依賴於webengine.dll所提供的API,所以,建議在ASP.NET應用程序中,優先使用Cache所提供的文件依賴功能。

各類緩存方案的共存

ASP.NET Cache是一種緩存技術,然而,咱們在ASP.NET程序中還可使用其它的緩存技術, 這些不一樣的緩存也各有各自的長處。因爲ASP.NET Cache不能提供對外訪問能力,所以,它不可能取代以memcached爲表明的分佈式緩存技術, 但它因爲是不須要跨進程訪問,效率也比分佈式緩存的速度更快。若是將ASP.NET Cache設計成【一級緩存】, 分佈式緩存設計成【二級緩存】,就像CPU的緩存那樣,那麼將能同時利用兩者的全部的優勢,實現更完美的功能以及速度。

其實緩存是沒有一個明肯定義的技術,一個static變量也是一個緩存,一個static集合就是一個緩存容器了。 這種緩存與ASP.NET Cache相比起來,顯然static變量的訪問速度會更快,若是static集合不是設計得不好的話, 併發的衝突也可能會比ASP.NET Cache小,也正是由於這一點,static集合也有着普遍的使用。 然而,ASP.NET Cache的一些高級功能,如:過時時間,緩存依賴(包含文件依賴),移除通知,也是static集合不具有的。 所以,合理地同時使用它們,會讓程序有着最好的性能,也同時擁有更強大的功能。

 

點擊此處下載示例代碼

相關文章
相關標籤/搜索