ASP.NET 緩存技術分析

緩存功能是大型網站設計一個很重要的部分。由數據庫驅動的Web應用程序,如果需要改善其性能,最好的方法是使用緩存功能。可能的情況下儘量使用緩 存,從內存中返回數據的速度始終比去數據庫查的速度快,因而可以大大提供應用程序的性能。畢竟現在內存非常便宜,用空間換取時間效率應該是非常划算的。尤 其是對耗時比較長的、需要建立網絡鏈接的數據庫查詢操作等。 對於web頁面的緩存,WebForm與ASP.NET MVC有不同的語法。在WebForm中, <%@ OutputCache Duration="60" VaryByParam="none" %>  來進行頁面緩存或者局部頁面緩存,在ASP.NET MVC中,針對Action使用Attribute進行頁面緩存。這些都是Web框架封裝好的,而對於Web Application來講,需要緩存的不僅僅是頁面,很多時候還需要定製的去緩存一些內容,這時候就需要用到HttpRun.Cache對象。

以下內容轉自   細說 ASP.NET Cache 及其高級用法

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

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 SP1、3.0 SP1、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更新後,被依賴的緩存結果將會自動清除。這樣就徹底地解決了模塊間的緩存數據依賴問題。

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

我希望在用戶修改了配置文件後,程序能立刻以最新的參數運行,而且不用重啓網站。
首先,我要說明一點,雖然解決方案與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中, 可是,過期時間和緩存依賴也很有用哦。如何能二者兼得呢?

與 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文件,服務端代碼。先來看頁面代碼: 

頁面的顯示效果如下:

處理頁面中二個按鈕的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集合不具備的。 因此,合理地同時使用它們,會讓程序有着最好的性能,也同時擁有更強大的功能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章