《解剖PetShop》系列之四-PetShop之ASP.NET緩存

四 PetShop之ASP.NET緩存

如果對微型計算機硬件系統有足夠的瞭解,那麼我們對於Cache這個名詞一定是耳熟能詳的。在CPU以及主板的芯片中,都引入了這種名爲高速緩衝存儲器(Cache)的技術。因爲Cache的存取速度比內存快,因而引入Cache能夠有效的解決CPU與內存之間的速度不匹配問題。硬件系統可以利用Cache存儲CPU訪問概率高的那些數據,當CPU需要訪問這些數據時,可以直接從Cache中讀取,而不必訪問存取速度相對較慢的內存,從而提高了CPU的工作效率。軟件設計借鑑了硬件設計中引入緩存的機制以改善整個系統的性能,尤其是對於一個數據庫驅動的Web應用程序而言,緩存的利用是不可或缺的,畢竟,數據庫查詢可能是整個Web站點中調用最頻繁但同時又是執行最緩慢的操作之一,我們不能被它老邁的雙腿拖緩我們前進的征程。緩存機制正是解決這一缺陷的加速器。

4.1  ASP.NET緩存概述

作爲.Net框架下開發Web應用程序的主打產品,ASP.NET充分考慮了緩存機制。通過某種方法,將系統需要的數據對象、Web頁面存儲在內存中,使得Web站點在需要獲取這些數據時,不需要經過繁瑣的數據庫連接、查詢和複雜的邏輯運算,就可以“觸手可及”,如“探囊取物”般容易而快速,從而提高整個Web系統的性能。

ASP.NET提供了兩種基本的緩存機制來提供緩存功能。一種是應用程序緩存,它允許開發者將程序生成的數據或報表業務對象放入緩存中。另外一種緩存機制是頁輸出緩存,利用它,可以直接獲取存放在緩存中的頁面,而不需要經過繁雜的對該頁面的再次處理。

應用程序緩存其實現原理說來平淡無奇,僅僅是通過ASP.NET管理內存中的緩存空間。放入緩存中的應用程序數據對象,以鍵/值對的方式存儲,這便於用戶在訪問緩存中的數據項時,可以根據key值判斷該項是否存在緩存中。

放入在緩存中的數據對象其生命週期是受到限制的,即使在整個應用程序的生命週期裏,也不能保證該數據對象一直有效。ASP.NET可以對應用程序緩存進行管理,例如當數據項無效、過期或內存不足時移除它們。此外,調用者還可以通過CacheItemRemovedCallback委託,定義回調方法使得數據項被移除時能夠通知用戶。

在.Net Framework中,應用程序緩存通過System.Web.Caching.Cache類實現。它是一個密封類,不能被繼承。對於每一個應用程序域,都要創建一個Cache類的實例,其生命週期與應用程序域的生命週期保持一致。我們可以利用Add或Insert方法,將數據項添加到應用程序緩存中,如下所示:
Cache["First"] = "First Item";
Cache.Insert("Second", "Second Item");

我們還可以爲應用程序緩存添加依賴項,使得依賴項發生更改時,該數據項能夠從緩存中移除:
string[] dependencies = {"Second"};
Cache.Insert("Third", "Third Item",
new System.Web.Caching.CacheDependency(null, dependencies));

與之對應的是緩存中數據項的移除。前面提到ASP.NET可以自動管理緩存中項的移除,但我們也可以通過代碼編寫的方式顯式的移除相關的數據項:
Cache.Remove("First");

相對於應用程序緩存而言,頁輸出緩存的應用更爲廣泛。它可以通過內存將處理後的ASP.NET頁面存儲起來,當客戶端再一次訪問該頁面時,可以省去頁面處理的過程,從而提高頁面訪問的性能,以及Web服務器的吞吐量。例如,在一個電子商務網站裏,用戶需要經常查詢商品信息,這個過程會涉及到數據庫訪問以及搜索條件的匹配,在數據量較大的情況下,如此的搜索過程是較爲耗時的。此時,利用頁輸出緩存就可以將第一次搜索得到的查詢結果頁存儲在緩存中。當用戶第二次查詢時,就可以省去數據查詢的過程,減少頁面的響應時間。

頁輸出緩存分爲整頁緩存和部分頁緩存。我們可以通過@OutputCache指令完成對Web頁面的輸出緩存。它主要包含兩個參數:Duration和VaryByParam。Duration參數用於設置頁面或控件進行緩存的時間,其單位爲秒。如下的設置表示緩存在60秒內有效:
<%@ OutputCache Duration=“60“ VaryByParam=“none“ %>

只要沒有超過Duration設置的期限值,當用戶訪問相同的頁面或控件時,就可以直接在緩存中獲取。
使用VaryByParam參數可以根據設置的參數值建立不同的緩存。例如在一個輸出天氣預報結果的頁面中,如果需要爲一個ID爲txtCity的TextBox控件建立緩存,其值將顯示某城市的氣溫,那麼我們可以進行如下的設置:
<%@ OutputCache Duration=”60” VaryByParam=”txtCity” %>

如此一來,ASP.NET會對txtCity控件的值進行判斷,只有輸入的值與緩存值相同,才從緩存中取出相應的值。這就有效地避免了因爲值的不同而導致輸出錯誤的數據。

利用緩存的機制對性能的提升非常明顯。通過ACT(Application Center Test)的測試,可以發現設置緩存後執行的性能比未設置緩存時的性能足足提高三倍多。

引入緩存看來是提高性能的“完美”解決方案,然而“金無足赤,人無完人”,緩存機制也有缺點,那就是數據過期的問題。一旦應用程序數據或者頁面結果值發生的改變,那麼在緩存有效期範圍內,你所獲得的結果將是過期的、不準確的數據。我們可以想一想股票系統利用緩存所帶來的災難,當你利用錯誤過期的數據去分析股市的風雲變幻時,你會發現獲得的結果真可以說是“失之毫釐,謬以千里”,看似大好的局面就會像美麗的泡沫一樣,用針一戳,轉眼就消失得無影無蹤。

那麼我們是否應該爲了追求高性能,而不顧所謂“數據過期”所帶來的隱患呢?顯然,在類似於股票系統這種數據更新頻繁的特定場景下,數據過期的糟糕表現甚至比低效的性能更讓人難以接受。故而,我們需要在性能與數據正確性間作出權衡。所幸的是,.Net Framework 2.0引入了一種新的緩存機制,它爲我們的“魚與熊掌兼得”帶來了技術上的可行性。

.Net 2.0引入的自定義緩存依賴項,特別是基於MS-SQL Server的SqlCacheDependency特性,使得我們可以避免“數據過期”的問題,它能夠根據數據庫中相應數據的變化,通知緩存,並移除那些過期的數據。事實上,在PetShop 4.0中,就充分地利用了SqlCacheDependency特性。

4.2 SqlCacheDependency特性

SqlCacheDependency特性實際上是通過System.Web.Caching.SqlCacheDependency類來體現的。通過該類,可以在所有支持的SQL Server版本(7.0,2000,2005)上監視特定的SQL Server數據庫表,並創建依賴於該表以及表中數據行的緩存項。當數據表或表中特定行的數據發生更改時,具有依賴項的數據項就會失效,並自動從Cache中刪除該項,從而保證了緩存中不再保留過期的數據。
由於版本的原因,SQL Server 2005完全支持SqlCacheDependency特性,但對於SQL Server 7.0和SQL Server 2000而言,就沒有如此幸運了。畢竟這些產品出現在.Net Framework 2.0之前,因此它並沒有實現自動監視數據表數據變化,通知ASP.NET的功能。解決的辦法就是利用輪詢機制,通過ASP.NET進程內的一個線程以指定的時間間隔輪詢SQL Server數據庫,以跟蹤數據的變化情況。

要使得7.0或者2000版本的SQL Server支持SqlCacheDependency特性,需要對數據庫服務器執行相關的配置步驟。有兩種方法配置SQL Server:使用aspnet_regsql命令行工具,或者使用SqlCacheDependencyAdmin類。

4.2.1  利用aspnet_regsql工具

aspnet_regsql工具位於Windows/Microsoft.NET/Framework/[版本]文件夾中。如果直接雙擊該工具的執行文件,會彈出一個嚮導對話框,提示我們完成相應的操作:

4-1.gif
圖4-1 aspnet_regsql工具

如圖4-1所示中的提示信息,說明該向導主要用於配置SQL Server數據庫,如membership,profiles等信息,如果要配置SqlCacheDependency,則需要以命令行的方式執行。以PetShop 4.0爲例,數據庫名爲MSPetShop4,則命令爲:
aspnet_regsql -S localhost -E -d MSPetShop4 -ed

以下是該工具的命令參數說明:
-?  顯示該工具的幫助功能;
-S  後接的參數爲數據庫服務器的名稱或者IP地址;
-U  後接的參數爲數據庫的登陸用戶名;
-P  後接的參數爲數據庫的登陸密碼;
-E  當使用windows集成驗證時,使用該功能;
-d  後接參數爲對哪一個數據庫採用SqlCacheDependency功能;
-t  後接參數爲對哪一個表採用SqlCacheDependency功能;
-ed  允許對數據庫使用SqlCacheDependency功能;
-dd  禁止對數據庫採用SqlCacheDependency功能;
-et  允許對數據表採用SqlCacheDependency功能;
-dt  禁止對數據表採用SqlCacheDependency功能;
-lt  列出當前數據庫中有哪些表已經採用sqlcachedependency功能。

以上面的命令爲例,說明將對名爲MSPetShop4的數據庫採用SqlCacheDependency功能,且SQL Server採用了windows集成驗證方式。我們還可以對相關的數據表執行aspnet_regsql命令,如:
aspnet_regsql -S localhost -E -d MSPetShop4 -t Item -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Product -et
aspnet_regsql -S localhost -E -d MSPetShop4 -t Category -et

當執行上述的四條命令後,aspnet_regsql工具會在MSPetShop4數據庫中建立一個名爲AspNet_SqlCacheTablesForChangeNotification的新數據庫表。該數據表包含三個字段。字段tableName記錄要追蹤的數據表的名稱,例如在PetShop 4.0中,要記錄的數據表就包括Category、Item和Product。notificationCreated字段記錄開始追蹤的時間。changeId作爲一個類型爲int的字段,用於記錄數據表數據發生變化的次數。如圖4-2所示:

4-2.gif
圖4-2 AspNet_SqlCacheTablesForChangeNotification數據表

除此之外,執行該命令還會爲MSPetShop4數據庫添加一組存儲過程,爲ASP.NET提供查詢追蹤的數據表的情況,同時還將爲使用了SqlCacheDependency的表添加觸發器,分別對應Insert、Update、Delete等與數據更改相關的操作。例如Product數據表的觸發器:
CREATE TRIGGER dbo.[Product_AspNet_SqlCacheNotification_Trigger] ON [Product]
    FOR INSERT, UPDATE, DELETE AS BEGIN
    SET NOCOUNT ON
    EXEC dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure N'Product'
END

其中,AspNet_SqlCacheUpdateChangeIdStoredProcedure即是工具添加的一組存儲過程中的一個。當對Product數據表執行Insert、Update或Delete等操作時,就會激活觸發器,然後執行AspNet_SqlCacheUpdateChangeIdStoredProcedure存儲過程。其執行的過程就是修改AspNet_SqlCacheTablesForChangeNotification數據表的changeId字段值:
CREATE PROCEDURE dbo.AspNet_SqlCacheUpdateChangeIdStoredProcedure
             @tableName NVARCHAR(450)
         AS
         BEGIN
             UPDATE dbo.AspNet_SqlCacheTablesForChangeNotification WITH (ROWLOCK) SET changeId = changeId + 1
             WHERE tableName = @tableName
         END  
GO

4.2.2  利用SqlCacheDependencyAdmin類

我們也可以利用編程的方式來來管理數據庫對SqlCacheDependency特性的使用。該類包含了五個重要的方法:

DisableNotifications
爲特定數據庫禁用 SqlCacheDependency對象更改通知
DisableTableForNotifications
爲數據庫中的特定表禁用SqlCacheDependency對象更改通知
EnableNotifications
爲特定數據庫啓用SqlCacheDependency對象更改通知
EnableTableForNotifications
爲數據庫中的特定表啓用SqlCacheDependency對象更改通知
GetTablesEnabledForNotifications
返回啓用了SqlCacheDependency對象更改通知的所有表的列表

表4-1 SqlCacheDependencyAdmin類的主要方法

假設我們定義瞭如下的數據庫連接字符串:
const string connectionStr = "Server=localhost;Database=MSPetShop4";

那麼爲數據庫MSPetShop4啓用SqlCacheDependency對象更改通知的實現爲:
protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack)
   {
       SqlCacheDependencyAdmin.EnableNotifications(connectionStr);
   }
}

爲數據表Product啓用SqlCacheDependency對象更改通知的實現則爲:
SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr, "Product");

如果要調用表4-1中所示的相關方法,需要注意的是訪問SQL Server數據庫的帳戶必須具有創建表和存儲過程的權限。如果要調用EnableTableForNotifications方法,還需要具有在該表上創建SQL Server觸發器的權限。

雖然說編程方式賦予了程序員更大的靈活性,但aspnet_regsql工具卻提供了更簡單的方法實現對SqlCacheDependency的配置與管理。PetShop 4.0採用的正是aspnet_regsql工具的辦法,它編寫了一個文件名爲InstallDatabases.cmd的批處理文件,其中包含了對aspnet_regsql工具的執行,並通過安裝程序去調用該文件,實現對SQL Server的配置。

4.3 在PetShop 4.0中ASP.NET緩存的實現

PetShop作爲一個B2C的寵物網上商店,需要充分考慮訪客的用戶體驗,如果因爲數據量大而導致Web服務器的響應不及時,頁面和查詢數據遲遲得不到結果,會因此而破壞客戶訪問網站的心情,在耗盡耐心的等待後,可能會失去這一部分客戶。無疑,這是非常糟糕的結果。因而在對其進行體系架構設計時,整個系統的性能就顯得殊爲重要。然而,我們不能因噎廢食,因爲專注於性能而忽略數據的正確性。在PetShop 3.0版本以及之前的版本,因爲ASP.NET緩存的侷限性,這一問題並沒有得到很好的解決。PetShop 4.0則引入了SqlCacheDependency特性,使得系統對緩存的處理較之以前大爲改觀。

4.3.1  CacheDependency接口

PetShop 4.0引入了SqlCacheDependency特性,對Category、Product和Item數據表對應的緩存實施了SQL Cache Invalidation技術。當對應的數據表數據發生更改後,該技術能夠將相關項從緩存中移除。實現這一技術的核心是SqlCacheDependency類,它繼承了CacheDependency類。然而爲了保證整個架構的可擴展性,我們也允許設計者建立自定義的CacheDependency類,用以擴展緩存依賴。這就有必要爲CacheDependency建立抽象接口,並在web.config文件中進行配置。

在PetShop 4.0的命名空間PetShop.ICacheDependency中,定義了名爲IPetShopCacheDependency接口,它僅包含了一個接口方法:
public interface IPetShopCacheDependency
{      
    AggregateCacheDependency GetDependency();
}

AggregateCacheDependency是.Net Framework 2.0新增的一個類,它負責監視依賴項對象的集合。當這個集合中的任意一個依賴項對象發生改變時,該依賴項對象對應的緩存對象都將被自動移除。
AggregateCacheDependency類起到了組合CacheDependency對象的作用,它可以將多個CacheDependency對象甚至於不同類型的CacheDependency對象與緩存項建立關聯。由於PetShop需要爲Category、Product和Item數據表建立依賴項,因而IPetShopCacheDependency的接口方法GetDependency()其目的就是返回建立了這些依賴項的AggregateCacheDependency對象。

4.3.2  CacheDependency實現

CacheDependency的實現正是爲Category、Product和Item數據表建立了對應的SqlCacheDependency類型的依賴項,如代碼所示:
public abstract class TableDependency : IPetShopCacheDependency
{
    // This is the separator that's used in web.config
    protected char[] configurationSeparator = new char[] { ',' };

    protected AggregateCacheDependency dependency = new AggregateCacheDependency();
    protected TableDependency(string configKey)
    {
        string dbName = ConfigurationManager.AppSettings["CacheDatabaseName"];
        string tableConfig = ConfigurationManager.AppSettings[configKey];
        string[] tables = tableConfig.Split(configurationSeparator);

        foreach (string tableName in tables)
            dependency.Add(new SqlCacheDependency(dbName, tableName));
    }
    public AggregateCacheDependency GetDependency()
   {
        return dependency;
    }
}

需要建立依賴項的數據庫與數據表都配置在web.config文件中,其設置如下:
<add key="CacheDatabaseName" value="MSPetShop4"/>
<add key="CategoryTableDependency" value="Category"/>
<add key="ProductTableDependency" value="Product,Category"/>
<add key="ItemTableDependency" value="Product,Category,Item"/>

根據各個數據表間的依賴關係,因而不同的數據表需要建立的依賴項也是不相同的,從配置文件中的value值可以看出。然而不管建立依賴項的多寡,其創建的行爲邏輯都是相似的,因而在設計時,抽象了一個共同的類TableDependency,並通過建立帶參數的構造函數,完成對依賴項的建立。由於接口方法GetDependency()的實現中,返回的對象dependency是在受保護的構造函數創建的,因此這裏的實現方式也可以看作是Template Method模式的靈活運用。例如TableDependency的子類Product,就是利用父類的構造函數建立了Product、Category數據表的SqlCacheDependency依賴:
public class Product : TableDependency
{
    public Product() : base("ProductTableDependency") { }
}

如果需要自定義CacheDependency,那麼創建依賴項的方式又有不同。然而不管是創建SqlCacheDependency對象,還是自定義的CacheDependency對象,都是將這些依賴項添加到AggregateCacheDependency類中,因而我們也可以爲自定義CacheDependency建立專門的類,只要實現IPetShopCacheDependency接口即可。

4.3.3  CacheDependency工廠

繼承了抽象類TableDependency的Product、Category和Item類均需要在調用時創建各自的對象。由於它們的父類TableDependency實現了接口IPetShopCacheDependency,因而它們也間接實現了IPetShopCacheDependency接口,這爲實現工廠模式提供了前提。

在PetShop 4.0中,依然利用了配置文件和反射技術來實現工廠模式。命名空間PetShop.CacheDependencyFactory中,類DependencyAccess即爲創建IPetShopCacheDependency對象的工廠類:
public static class DependencyAccess
{       
    public static IPetShopCacheDependency CreateCategoryDependency()
    {
        return LoadInstance("Category");
    }
    public static IPetShopCacheDependency CreateProductDependency()
    {
        return LoadInstance("Product");
    }
    public static IPetShopCacheDependency CreateItemDependency()
    {
        return LoadInstance("Item");
    }
    private static IPetShopCacheDependency LoadInstance(string className)
    {
        string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
        string fullyQualifiedClass = path + "." + className;
        return (IPetShopCacheDependency)Assembly.Load(path).CreateInstance(fullyQualifiedClass);
    }
}
整個工廠模式的實現如圖4-3所示:

4-3.gif
 圖4-3 CacheDependency工廠

雖然DependencyAccess類創建了實現了IPetShopCacheDependency接口的類Category、Product、Item,然而我們之所以引入IPetShopCacheDependency接口,其目的就在於獲得創建了依賴項的AggregateCacheDependency類型的對象。我們可以調用對象的接口方法GetDependency(),如下所示:
AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();

爲了方便調用者,似乎我們可以對DependencyAccess類進行改進,將原有的CreateCategoryDependency()方法,修改爲創建AggregateCacheDependency類型對象的方法。

然而這樣的做法擾亂了作爲工廠類的DependencyAccess的本身職責,且創建IPetShopCacheDependency接口對象的行爲仍然有可能被調用者調用,所以保留原有的DependencyAccess類仍然是有必要的。

在PetShop 4.0的設計中,是通過引入Facade模式以方便調用者更加簡單地獲得AggregateCacheDependency類型對象。

4.3.4  引入Facade模式

利用Facade模式可以將一些複雜的邏輯進行包裝,以方便調用者對這些複雜邏輯的調用。就好像提供一個統一的門面一般,將內部的子系統封裝起來,統一爲一個高層次的接口。一個典型的Facade模式示意圖如下所示:

4-4.gif
圖4-4 Facade模式

Facade模式的目的並非要引入一個新的功能,而是在現有功能的基礎上提供一個更高層次的抽象,使得調用者可以直接調用,而不用關心內部的實現方式。以CacheDependency工廠爲例,我們需要爲調用者提供獲得AggregateCacheDependency對象的簡便方法,因而創建了DependencyFacade類:
public static class DependencyFacade
{
    private static readonly string path = ConfigurationManager.AppSettings["CacheDependencyAssembly"];
    public static AggregateCacheDependency GetCategoryDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateCategoryDependency().GetDependency();
        else
            return null;
    }
    public static AggregateCacheDependency GetProductDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateProductDependency().GetDependency();
        else
            return null;
        }
    public static AggregateCacheDependency GetItemDependency()
    {
        if (!string.IsNullOrEmpty(path))
            return DependencyAccess.CreateItemDependency().GetDependency();
        else
            return null;
    }
}

DependencyFacade類封裝了獲取AggregateCacheDependency類型對象的邏輯,如此一來,調用者可以調用相關方法獲得創建相關依賴項的AggregateCacheDependency類型對象:
AggregateCacheDependency dependency = DependencyFacade.GetCategoryDependency();

比起直接調用DependencyAccess類的GetDependency()方法而言,除了方法更簡單之外,同時它還對CacheDependencyAssembly配置節進行了判斷,如果其值爲空,則返回null對象。

在PetShop.Web的App_Code文件夾下,靜態類WebUtility的GetCategoryName()和GetProductName()方法調用了DependencyFacade類。例如GetCategoryName()方法:
public static string GetCategoryName(string categoryId)
{
     Category category = new Category();
     if (!enableCaching)
            return category.GetCategory(categoryId).Name;

     string cacheKey = string.Format(CATEGORY_NAME_KEY, categoryId);

     // 檢查緩存中是否存在該數據項;
     string data = (string)HttpRuntime.Cache[cacheKey];
     if (data == null)
     {
           // 通過web.config的配置獲取duration值;
           int cacheDuration = int.Parse(ConfigurationManager.AppSettings["CategoryCacheDuration"]);
           // 如果緩存中不存在該數據項,則通過業務邏輯層訪問數據庫獲取;
           data = category.GetCategory(categoryId).Name;
           // 通過Facade類創建AggregateCacheDependency對象;
           AggregateCacheDependency cd = DependencyFacade.GetCategoryDependency();
           // 將數據項以及AggregateCacheDependency 對象存儲到緩存中;
           HttpRuntime.Cache.Add(cacheKey, data, cd, DateTime.Now.AddHours(cacheDuration), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
      }
      return data;
}

GetCategoryName()方法首先會檢查緩存中是否已經存在CategoryName數據項,如果已經存在,就通過緩存直接獲取數據;否則將通過業務邏輯層調用數據訪問層訪問數據庫獲得CategoryName,在獲得了CategoryName後,會將新獲取的數據連同DependencyFacade類創建的AggregateCacheDependency對象添加到緩存中。

WebUtility靜態類被表示層的許多頁面所調用,例如Product頁面:
public partial class Products : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        Page.Title = WebUtility.GetCategoryName(Request.QueryString["categoryId"]);
    }
}

顯示頁面title的邏輯是放在Page_Load事件方法中,因而每次打開該頁面都要執行獲取CategoryName的方法。如果沒有采用緩存機制,當Category數據較多時,頁面的顯示就會非常緩慢。

4.3.5  引入Proxy模式

業務邏輯層BLL中與Product、Category、Item有關的業務方法,其實現邏輯是調用數據訪問層(DAL)對象訪問數據庫,以獲取相關數據。爲了改善系統性能,我們就需要爲這些實現方法增加緩存機制的邏輯。當我們操作增加了緩存機制的業務對象時,對於調用者而言,應與BLL業務對象的調用保持一致。也即是說,我們需要引入一個新的對象去控制原來的BLL業務對象,這個新的對象就是Proxy模式中的代理對象。

以PetShop.BLL.Product業務對象爲例,PetShop爲其建立了代理對象ProductDataProxy,並在GetProductByCategory()等方法中,引入了緩存機制,例如:
public static class ProductDataProxy
{

    private static readonly int productTimeout = int.Parse(ConfigurationManager.AppSettings["ProductCacheDuration"]);
    private static readonly bool enableCaching = bool.Parse(ConfigurationManager.AppSettings["EnableCaching"]);
       
    public static IList
GetProductsByCategory(string category)
    {
        Product product = new Product();

        if (!enableCaching)
            return product.GetProductsByCategory(category);

        string key = "product_by_category_" + category;
        IList data = (IList )HttpRuntime.Cache[key];

        // Check if the data exists in the data cache
        if (data == null)
        {
            data = product.GetProductsByCategory(category);

            // Create a AggregateCacheDependency object from the factory
            AggregateCacheDependency cd = DependencyFacade.GetProductDependency();

            // Store the output in the data cache, and Add the necessary AggregateCacheDependency object
            HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(productTimeout), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
        }
        return data;
    }
}

與業務邏輯層Product對象的GetProductsByCategory()方法相比,增加了緩存機制。當緩存內不存在相關數據項時,則直接調用業務邏輯層Product的GetProductsByCategory()方法來獲取數據,並將其與對應的AggregateCacheDependency對象一起存儲在緩存中。

引入Proxy模式,實現了在緩存級別上對業務對象的封裝,增強了對業務對象的控制。由於暴露在對象外的方法是一致的,因而對於調用方而言,調用代理對象與真實對象並沒有實質的區別。

從職責分離與分層設計的角度分析,我更希望這些Proxy對象是被定義在業務邏輯層中,而不像在PetShop的設計那樣,被劃分到表示層UI中。此外,如果需要考慮程序的可擴展性與可替換性,我們還可以爲真實對象與代理對象建立統一的接口或抽象類。然而,單以PetShop的表示層調用來看,採用靜態類與靜態方法的方式,或許更爲合理。我們需要謹記,“過度設計”是軟件設計的警戒線。

如果需要對UI層採用緩存機制,將應用程序數據存放到緩存中,就可以調用這些代理對象。以ProductsControl用戶控件爲例,調用方式如下:
productsList.DataSource = ProductDataProxy.GetProductsByCategory(categoryKey);

productsList對象屬於自定義的CustomList類型,這是一個派生自System.Web.UI.WebControls.DataList控件的類,它的DataSource屬性可以接受IList集合對象。
不過在PetShop 4.0的設計中,對於類似於ProductsControl類型的控件而言,採用的緩存機制是頁輸出緩存。我們可以從ProductsControl.ascx頁面的Source代碼中發現端倪:
<%@ OutputCache Duration="100000" VaryByParam="page;categoryId" %>

與ASP.NET 1.x的頁輸出緩存不同的是,在ASP.NET 2.0中,爲ASP.NET用戶控件新引入了CachePolicy屬性,該屬性的類型爲ControlCachePolicy類,它以編程方式實現了對ASP.NET用戶控件的輸出緩存設置。我們可以通過設置ControlCachePolicy類的Dependency屬性,來設置與該用戶控件相關的依賴項,例如在ProductsControl用戶控件中,進行如下的設置:
protected void Page_Load(object sender, EventArgs e)
{
    this.CachePolicy.Dependency = DependencyFacade.GetProductDependency();
}

採用頁輸出緩存,並且利用ControlCachePolicy設置輸出緩存,能夠將業務數據與整個頁面放入到緩存中。這種方式比起應用程序緩存而言,在性能上有很大的提高。同時,它又通過引入的SqlCacheDependency特性有效地避免了“數據過期”的缺點,因而在PetShop 4.0中被廣泛採用。相反,之前爲Product、Category、Item業務對象建立的代理對象則被“投閒散置”,僅僅作爲一種設計方法的展示而“倖存”與整個系統的源代碼中。 

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