首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> 關於.NET

《解剖 PetShop》系列之四:PetShop ASP.NET 快取機制深度解析

     編輯:關於.NET

若對微型計算機硬體系統有充分了解,對 Cache(高速緩衝存儲器)一詞必不陌生。CPU 與主板晶片中引入此技術,因 Cache 存取速度比記憶體快,有效解決 CPU 與記憶體速度不匹配問題。硬體系統利用 Cache 儲存 CPU 訪問機率高的數據,CPU 需訪問時直接從 Cache 讀取,無需訪問較慢的記憶體,提高工作效率。軟體設計借鑒硬體引入快取機制改善系統性能,對資料庫驅動的 Web 應用程式而言,快取利用不可或缺,畢竟資料庫查詢可能是整個 Web 站點調用最頻繁但執行最緩慢的操作之一,不能被其拖慢腳步。快取機制正是解決此缺陷的加速器。


4.1 ASP.NET 快取概述

作為 .NET 框架下開發 Web 應用主打產品,ASP.NET 充分考慮快取機制。透過某種方法將系統所需數據對象、Web 頁面存儲在記憶體中,使 Web 站點需要獲取數據時,無需經過繁瑣資料庫連接、查詢與複雜邏輯運算,即可「探囊取物」般容易快速,提高整個 Web 系統性能。

ASP.NET 提供兩種基本快取機制:

  1. 應用程式快取:允許開發者將程式生成的數據或報表業務對象放入快取。
  2. 頁輸出快取:直接獲取存放在快取中的頁面,無需再次處理該頁面。

應用程式快取實現原理平淡無奇,僅透過 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 指令完成,主要兩參數:Duration 與 VaryByParam。Duration 設定頁面或控件快取時間(秒)。如 Duration="60" VaryByParam="none" 表示快取 60 秒有效。未超過期限時,訪問相同頁面或控件可直接從快取獲取。

VaryByParam 可依設定參數值建立不同快取。如輸出天氣預報頁面,為 ID 為 txtCity 的 TextBox 建立快取: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 即充分利用此特性。


4.2 SqlCacheDependency 特性

SqlCacheDependency 特性透過 System.Web.Caching.SqlCacheDependency 類體現。可在所有支援的 SQL Server 版本(7.0、2000、2005)監視特定資料庫表,創建依賴該表及表中數據行的快取項。當資料表或特定行數據發生更改,具有依賴項的數據項失效並自動從 Cache 刪除,保證快取不再保留過期數據。

因版本原因,SQL Server 2005 完全支援 SqlCacheDependency,但 SQL Server 7.0/2000 較不幸。這些產品出現於 .NET Framework 2.0 之前,未實現自動監視資料表數據變化通知 ASP.NET 功能。解決辦法:利用輪詢機制,透過 ASP.NET 進程內一線程以指定時間間隔輪詢 SQL Server 資料庫,追蹤數據變化情況。

使 7.0/2000 版 SQL Server 支援 SqlCacheDependency 需對資料庫伺服器執行配置步驟。兩種配置方法:使用 aspnet_regsql 命令列工具,或使用 SqlCacheDependencyAdmin 類。

4.2.1 利用 aspnet_regsql 工具

aspnet_regsql 位於 Windows\Microsoft.NET\Framework\[版本] 資料夾。直接雙擊執行檔會彈出嚮導對話框:

aspnet_regsql 工具嚮導

嚮導主要用於配置 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 -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

執行上述四條命令後,工具會在 MSPetShop4 建立 AspNet_SqlCacheTablesForChangeNotification 新資料表,包含三欄位:tableName 記錄追蹤資料表名稱(PetShop 4.0 包含 Category、Item、Product),notificationCreated 記錄開始追蹤時間,changeId(int)記錄資料表數據發生變化次數:

AspNet_SqlCacheTablesForChangeNotification 資料表

此外,執行命令還會為資料庫添加一組預存程序供 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_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 對象更改通知的所有表列表

假設定義連接字串:const string connectionStr = "Server=localhost;Database=MSPetShop4";

啟用資料庫通知:

protected void Page_Load(object sender, EventArgs e)
{
    if (!IsPostBack) SqlCacheDependencyAdmin.EnableNotifications(connectionStr);
}

啟用 Product 表通知:

SqlCacheDependencyAdmin.EnableTableForNotifications(connectionStr, "Product");

調用相關方法需注意:訪問 SQL Server 資料庫帳戶必須具建立表與預存程序權限;調用 EnableTableForNotifications 還需具在該表建立觸發器權限。編程方式賦予靈活性,但 aspnet_regsql 提供更簡單配置管理方法。PetShop 4.0 採用 aspnet_regsql 工具,編寫 InstallDatabases.cmd 批次檔包含工具執行,透過安裝程式調用實現 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.ICacheDependency 定義 IPetShopCacheDependency 介面,僅含一介面方法:

public interface IPetShopCacheDependency
{
    AggregateCacheDependency GetDependency();
}

AggregateCacheDependency 為 .NET Framework 2.0 新增類,負責監視依賴項對象集合。集合中任一依賴項對象發生改變時,對應快取對象將自動移除。它起組合 CacheDependency 對象作用,可將多個甚至不同類型 CacheDependency 對象與快取項建立關聯。PetShop 需為 Category、Product、Item 建立依賴項,故 GetDependency() 目的為返回建立這些依賴項的 AggregateCacheDependency 對象。

4.3.2 CacheDependency 實現

實現為 Category、Product、Item 建立對應 SqlCacheDependency 類型依賴項:

public abstract class TableDependency : IPetShopCacheDependency
{
    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 依賴:

public class Product : TableDependency
{
    public Product() : base("ProductTableDependency") { }
}

若需自定義 CacheDependency,創建依賴項方式不同。但無論創建 SqlCacheDependency 或自定義對象,皆添加至 AggregateCacheDependency,故可為自定義 CacheDependency 建立專門類,只需實作 IPetShopCacheDependency 介面。

4.3.3 CacheDependency 工廠

繼承 TableDependency 的 Product、Category、Item 類調用時需創建各自對象。因父類實作 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);
    }
}

工廠模式實現:

CacheDependency 工廠圖

DependencyAccess 創建實作 IPetShopCacheDependency 的類 Category、Product、Item,但引入介面目的在於獲得創建依賴項的 AggregateCacheDependency 類型對象。可調用介面方法 GetDependency():

AggregateCacheDependency dependency = DependencyAccess.CreateCategoryDependency().GetDependency();

為方便調用者,似可改進 DependencyAccess 將 CreateCategoryDependency() 修改為創建 AggregateCacheDependency 類型對象方法。但這擾亂工廠類職責,且創建介面對象行為仍可能被調用者調用,故保留原類仍必要。

PetShop 4.0 設計中透過引入 Facade 模式 方便調用者更簡單獲得 AggregateCacheDependency 類型對象。

4.3.4 引入 Facade 模式

Facade 模式可將複雜邏輯包裝,方便調用者調用。如提供統一門面,將內部子系統封裝,統一為高層次介面。典型 Facade 模式示意圖:

Facade 模式圖

Facade 目的非引入新功能,而是在現有功能基礎上提供更高層次抽象,使調用者可直接調用無需關心內部實現。以 CacheDependency 工廠為例,創建 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 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)
    {
        int cacheDuration = int.Parse(ConfigurationManager.AppSettings["CategoryCacheDuration"]);
        data = category.GetCategory(categoryId).Name;
        AggregateCacheDependency cd = DependencyFacade.GetCategoryDependency();
        HttpRuntime.Cache.Add(cacheKey, data, cd, DateTime.Now.AddHours(cacheDuration), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
    }
    return data;
}

GetCategoryName() 先檢查快取中是否存在 CategoryName 數據項,存在則直接獲取;否則透過 BLL 調用 DAL 訪問資料庫獲取,獲得後將新數據連同 DependencyFacade 創建的 AggregateCacheDependency 添加到快取。

WebUtility 被表示層許多頁面調用,如 Products 頁面:

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,在 GetProductsByCategory() 等方法引入快取機制:

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<ProductInfo> GetProductsByCategory(string category)
    {
        Product product = new Product();
        if (!enableCaching) return product.GetProductsByCategory(category);

        string key = "product_by_category_" + category;
        IList<ProductInfo> data = (IList<ProductInfo>)HttpRuntime.Cache[key];
        if (data == null)
        {
            data = product.GetProductsByCategory(category);
            AggregateCacheDependency cd = DependencyFacade.GetProductDependency();
            HttpRuntime.Cache.Add(key, data, cd, DateTime.Now.AddHours(productTimeout), Cache.NoSlidingExpiration, CacheItemPriority.High, null);
        }
        return data;
    }
}

與 BLL Product.GetProductsByCategory() 比較,增加快取機制。快取內不存在相關數據項時,直接調用 BLL Product 方法獲取數據,並與對應 AggregateCacheDependency 存儲在快取中。

引入 Proxy 模式實現快取層級上對業務對象封裝,增強對業務對象控制。因暴露方法一致,對調用方而言調用代理對象與真實對象無實質區別。

從職責分離與分層設計角度,筆者更希望 Proxy 對象定義在 BLL 中,而非像 PetShop 設計劃分到表示層 UI。若考慮程式可擴展性與可替換性,可為真實對象與代理對象建立統一介面或抽象類。但單以 PetShop 表示層調用看,採用靜態類與靜態方法或許更合理。需謹記「過度設計」是軟體設計警戒線。

若需對 UI 層採用快取機制將應用程式數據存放快取,可調用代理對象。以 ProductsControl 用戶控件為例:

productsList.DataSource = ProductDataProxy.GetProductsByCategory(categoryKey);

productsList 為自定義 CustomList 類型(繼承 DataList),DataSource 接受 IList 集合。

但 PetShop 4.0 設計中,對類似 ProductsControl 控件採用頁輸出快取。從 ProductsControl.ascx Source 代碼可見端倪:

<%@ OutputCache Duration="100000" VaryByParam="page;categoryId" %>

ASP.NET 2.0 為用戶控件新引入 CachePolicy 屬性(型別 ControlCachePolicy),以編程方式實現輸出快取設置。可透過設置 ControlCachePolicy.Dependency 設置相關依賴項,如 ProductsControl:

protected void Page_Load(object sender, EventArgs e)
{
    this.CachePolicy.Dependency = DependencyFacade.GetProductDependency();
}

採用頁輸出快取並利用 ControlCachePolicy 設置,能將業務數據與整個頁面放入快取。此方式比應用程式快取在性能上有大幅提高,又透過引入 SqlCacheDependency 有效避免「數據過期」缺點,故在 PetShop 4.0 廣泛採用。相對地,之前為 Product、Category、Item 建立的代理對象被「投閒置散」,僅作為設計方法展示而「倖存」於系統源代碼中。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved