簡介
我們在前一篇教程中看到 , 要緩存ObjectDataSource 的數據 , 只需要設置幾個屬性即可。遺憾的是 ,ObjectDataSource 在 表示層進行 數據緩存 , 這將緩存策略與 ASP.NET 頁面緊密地結合在一起。創建分層架構的原因之一就是為了打破這種結合。例如 ,業務邏輯層將 業務邏輯從ASP.NET 頁面中分離出來 ,而 數據訪問層將數據訪問細節分離出來。將業務邏輯細節與數據訪問細節分離出來是我們的首選,其部分原因是這樣使得系統更為易讀,易於維護,可以更為靈活地修改。這也考慮到了知識領域與勞動分工的情況— 表示層 的開發人員不需要熟悉數據庫的細節就 可以進行開發工作 。而將緩存策略從 表示層 分離出來也有類似的好處。
在本教程中 , 我們將擴展我們的架構 , 使其包括一個緩存層(Caching Layer , 簡稱 CL ), 用該層來實施我們的緩存策略。 緩存層 包括一個 ProductsCL 類 ,該類 通過諸如GetProducts() 、GetProductsByCategoryID(categoryID) 等方法 來訪問 產品信息。當調用這些方法時 ,這些方法 首先嘗試從緩存中取得數據。如果緩存為空 , 這些方法會調用 BLL 中 ProductsBLL 類的相應 方法 , 這進而會從 DAL 中獲取數據。 ProductsCL 類的方法將從 BLL 中獲取的數據緩存后再返回。
如圖 1 所示, CL 位於表示層與業務邏輯層之間。
圖1 : 在我們的架構中加入了另外一層— 緩存層(CL)
步驟1 : 創建緩存層的類
在本教程中 , 我們將創建一個非常簡單的 CL ,它 只有一個類 —ProductsCL ,該類 只有幾個方法。要為整個應用程序構建一個完整的 緩存層, 則需要創建 CategoriesCL 、EmployeesCL 以及 SuppliersCL 類 , 並在這些 緩存層的 類中 , 為BLL 中的每個數據訪問或修改方法提供一個相應的方法。與BLL 和 DAL 一樣 , 理想情況下 緩存層 應該實現為一個單獨的 Class Library 項目 ; 然而 , 我們要將它實現為 App_Code 文件夾中的一個類。
為了將 CL 類與 DAL 和 BLL 類更好地區分開,我們在 App_Code 文件夾中創建一個新的子文件夾。在 Solution Explorer 中右鍵單擊 App_Code 文件夾 , 選擇 New Folder , 將新文件夾命名為 CL 。創建這個文件夾之后 , 在其中添加一個名為 ProductsCL.cs 的新類。
圖2 : 添加名為 CL 的新文件夾和名為 ProductsCL.cs 的類
與對應的業務邏輯層的類 (ProductsBLL) 一樣 ,ProductsCL 應包含相同的一組數據訪問與修改方法。不過在這里我們不會創建所有這些方法 ,而 只是創建幾個來感受一下 CL 所 使用的模式。具體說來,我們將在步驟 3 中添加 GetProducts() 與 GetProductsByCategoryID(categoryID) 方法,在步驟 4 中添加 UpdateProduct 重載方法。您可以在空閑時添加上其它的 ProductsCL 方法以及 CategoriesCL 、 EmployeesCL 和 SuppliersCL 類。
步驟2 : 讀寫數據緩存
在前面的教程中探討過 ObjectDataSource 緩存功能 , 該功能在內部使用 ASP.NET 數據緩存來存儲從 BLL 中獲取的數據。我們還可以通過編碼從ASP.NET 頁面的code-behind 類或從 web 應用架構中的類來訪問該數據緩存。要從ASP.NET 頁面的code-behind 類讀寫該數據緩存 , 請使用下面的模式 :
// Read from the cache object value = Cache["key"]; // Add a new item to the cache Cache["key"] = value; Cache.Insert(key, value); Cache.Insert(key, value, CacheDependency); Cache.Insert(key, value, CacheDependency, DateTime, TimeSpan);
Cache 類 的 Insert 方法 有許多重載。Cache("key") = value 和 Cache.Insert(key, value) 是 相同的 , 都是用指定的鍵值向緩存添加一個條目 ,但 沒有指定有效期。典型地 , 我們想在向緩存添加條目時指定有效期 ,該 有效期或者是基於依賴項的 , 或者是基於時間的 ,又 或者兩者兼而有之。使用 Insert 方法的其它重載 , 就可以提供基於依賴項或基於時間的有效期信息。
緩存層 的方法首先要檢查請求的數據是否在緩存中,如果在,從那里 將其返回。如果請求的數據不在緩存中 , 則需要調用 BLL 中的 相應方法。然后應將該方法返回的值緩存后再返回,如下面的流程圖所示。
圖3 : 如果數據存在於緩存中 ,緩存層 的方法會將其返回
在 CL 的 類中可以使用下面的模式來完成圖 3 描述的流程 :
Type instance = Cache["key"] as Type; if (instance == null) { instance = BllMethodToGetInstance(); Cache.Insert(key, instance, ...); } return instance;
其中 ,Type 是在緩存中存儲的數據的類型 — 例如 ,Northwind.ProductsDataTable ,而 key 是唯一標識緩存條目的鍵值。如果指定 key 的條目不在緩存中,那么 instance 就為空值,於是通過相應的 BLL 方法獲取數據,然后緩存該數據。當執行到 Return instance 時, instance 已包含了對數據的一個引用,它要么是從緩存獲得,要么是從 BLL 獲得的。
當訪問緩存中的數據時 , 請務必使用上述模式。下面的模式,乍一看好象和上面的模式一樣,但實際上卻有一個細微的差別,這個差別會產生競爭狀態。競爭狀態很難調試,因為它們只是偶爾出現,很難重現出來。
if (Cache["key"] == null) { Cache.Insert(key, BllMethodToGetInstance(), ...); } return Cache["key"];
這第二個不正確代碼段的不同之處是 , 它並沒有將緩存條目的引用存儲在一個局部變量中 , 而是在條件語句以及 Return 語句中直接訪問數據緩存。設想這種情況,執行到這段代碼時, Cache["key"] 是非空的,但是當執行到 Return 語句之前時,系統從緩存中刪除了這個 key。在這種罕見的情況下,代碼會返回空值,而不是返回期待類型的對象。參見Scott Cate 的博客文章 , 里面舉例描述了使用這個不正確的緩存模式怎樣偶爾導致非預期的行為。
注意 : 該數據緩存是線程安全的,所以對於簡單的讀寫,您不需要對線程訪問進行同步。然而,如果您需要對緩存中的數據進行原子級的多重操作,那么您就要負責實現鎖定或其它機制以確保線程安全。詳情參見 對 ASP.NET 緩存訪問進行同步 。
可以通過編碼用如下的 Remove 方法 從數據緩存中刪除一個條目 :
Cache.Remove(key)
步驟3 : 從ProductsCL 類返回產品信息
對於本教程 , 我們來實現如下兩個方法 , 它們從ProductsCL 類返回產品信息 :GetProducts() 和 GetProductsByCategoryID(categoryID) 。與 業務邏輯層 中的 ProductsBL 類相似 ,CL 中的 GetProducts() 方法以一個Northwind.ProductsDataTable 對象返回所有產品的信息 ,而 GetProductsByCategoryID(categoryID) 返回 指定類別的所有產品。
下面的代碼是 ProductsCL 類中的一部分方法 :
[System.ComponentModel.DataObject] public class ProductsCL { private ProductsBLL _productsAPI = null; protected ProductsBLL API { get { if (_productsAPI == null) _productsAPI = new ProductsBLL(); return _productsAPI; } } [System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Select, true)] public Northwind.ProductsDataTable GetProducts() { const string rawKey = "Products"; // See if the item is in the cache Northwind.ProductsDataTable products = _ GetCacheItem(rawKey) as Northwind.ProductsDataTable; if (products == null) { // Item not found in cache - retrieve it and insert it into the cache products = API.GetProducts(); AddCacheItem(rawKey, products); } return products; } [System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Select, false)] public Northwind.ProductsDataTable GetProductsByCategoryID(int categoryID) { if (categoryID < 0) return GetProducts(); else { string rawKey = string.Concat("ProductsByCategory-", categoryID); // See if the item is in the cache Northwind.ProductsDataTable products = _ GetCacheItem(rawKey) as Northwind.ProductsDataTable; if (products == null) { // Item not found in cache - retrieve it and insert it into the cache products = API.GetProductsByCategoryID(categoryID); AddCacheItem(rawKey, products); } return products; } } }
首先 , 注意應用於類和方法的 DataObject 和 DataObjectMethodAttribute 屬性。這些屬性向 ObjectDataSource 的向導提供信息 , 指示哪些類和方法應出現在向導的步驟中。因為要從 表示層 中的ObjectDataSource 訪問 CL 的 類和方法 , 所以我添加了這些屬性來增強設計時體驗。有關這些屬性及其作用的更為詳盡的描述,請參閱創建業務邏輯層 教程。
在GetProducts() 和 GetProductsByCategoryID(categoryID) 方法中 ,GetCacheItem(key) 方法返回的數據賦值給了一個局部變量。我們稍后探討GetCacheItem(key) 方法 , 該方法會根據指定的 key , 從緩存中返回一個特定的條目。如果在緩存中沒有找到這樣的數據 , 則通過ProductsBLL 類的相應方法獲取該數據 , 然后用 AddCacheItem(key, value) 方法緩存該數據。
GetCacheItem(key) 和 AddCacheItem(key, value) 方法是對數據緩存的接口 , 分別負責讀與寫。GetCacheItem(key) 方法是兩者中相對簡單的。它只是根據傳入的key 值 從Cache 類返回數據 :
private object GetCacheItem(string rawKey) { return HttpRuntime.Cache[GetCacheKey(rawKey)]; } private readonly string[] MasterCacheKeyArray = {"ProductsCache"}; private string GetCacheKey(string cacheKey) { return string.Concat(MasterCacheKeyArray[0], "-", cacheKey); }
GetCacheItem(key) 並沒有直接使用我們提供的 key 值 , 而是調用了GetCacheKey(key) 方法 , 這個方法在 key 前面加上 "ProductsCache-" 然后返回之 。MasterCacheKeyArray 用於保存字符串 "ProductsCache" ,我們稍后會看到,AddCacheItem(key, value) 方法也使用這個變量。
從ASP.NET 頁面的 code-behind 類 , 我們可以使用 Page 類的 Cache 屬性 來訪問數據緩存 , 並允許類似Cache["key"] = value 的語法 , 如步驟 2 中所述。從架構內的類中,可以使用 HttpRuntime.Cache 或 HttpContext.Current.Cache 來訪問數據緩存。Peter Johnson 在其博客文章 HttpRuntime.Cache vs. HttpContext.Current.Cache 中提到了使用 HttpRuntime 比使用 HttpContext.Current 稍有性能優勢;因此, ProductsCL 類使用 HttpRuntime 。
注意 : 如果您的架構是使用 Class Library 項目實現的 ,則 需要添加一個對 System.Web 程序集的引用才能使用HttpRuntime 和HttpContext 類。
如果在緩存中沒有找到這個條目 ,ProductsCL 類的方法會從 BLL 中獲取數據 , 然后用 AddCacheItem(key, value) 方法緩存該數據。我們可以用下面的代碼將value 添加到緩存 ,其中 使用了 60 秒的有效期 :
const double CacheDuration = 60.0; private void AddCacheItem(string rawKey, object value) { HttpRuntime.Cache.Insert(GetCacheKey(rawKey), value, null, DateTime.Now.AddSeconds(CacheDuration), Caching.Cache.NoSlidingExpiration); }
DateTime.Now.AddSeconds(CacheDuration) 指定了基於時間的有效期 — 未來 60 秒 , 而 System.Web.Caching.Cache.NoSlidingExpiration 指示不存在滑動有效期 (sliding expiration) 。雖然這個 Insert 重載方法既有絕對有效期又有滑動有效期的輸入參數 , 但是您只能提供其中一種。如果您試圖同時指定絕對時間和時間范圍 ,Insert 方法會拋出一個 ArgumentException 異常。
注意 : 這個 AddCacheItem(key, value) 方法的實現目前有些缺點。我們將在步驟4 中討論並解決這些問題。
步驟4 : 通過架構修改數據時使緩存數據失效
除了檢索數據的方法之外 , 和BLL 一樣 , 緩存層還需要提供插入、更新、刪除數據的方法。 CL 的數據修改方法並不修改緩存數據,而是調用 BLL 的相應數據修改方法,然后使緩存數據失效。我們在前面的教程中看到 , 這與 ObjectDataSource 的行為是一樣的 , 當啟用了 ObjectDataSource 的緩存功能 ,並調用 它的 Insert 、Update 、Delete 方法時 ,ObjectDataSource 會產生這些行為。
下面的 UpdateProduct 重載說明了怎樣在 CL 中實現數據修改方法 :
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)] public bool UpdateProduct(string productName, decimal? unitPrice, int productID) { bool result = API.UpdateProduct(productName, unitPrice, productID); // TODO: Invalidate the cache return result; }
其中調用了 業務邏輯層的相應 數據修改方法 , 但在將該方法的響應返回之前 , 我們需要使緩存數據失效。不過,這並非易事 , 因為ProductsCL 類的 GetProducts() 和GetProductsByCategoryID(categoryID) 方法各自使用不同的鍵值向緩存添加條目 ,GetProductsByCategoryID(categoryID) 方法會為每個唯一的 categoryID 添加不同的緩存條目。
在使緩存數據失效時 , 我們需要刪除ProductsCL 類可能已添加的所有條目。為此 , 我們在 AddCacheItem(key, value) 方法中 , 將添加到緩存的每一項與一個緩存依賴項相關聯。通常 , 緩存依賴項可以是緩存中的另一條目、文件系統中的一個文件、或Microsoft SQL Server 數據庫中的數據。當依賴項發生改變或從緩存中刪除時,它所關聯的緩存條目會自動從緩存中刪除。對於本教程,我們要在緩存中創建一個額外條目,用它作為通過 ProductsCL 類添加的所有條目的緩存依賴項。由此,就可以通過簡單地刪除該緩存依賴項來從緩存中刪除所有這些條目了。
我們來更新 AddCacheItem(key, value) 方法,使得通過這個方法向緩存添加的每個條目都與唯一一個緩存依賴項相關聯:
private void AddCacheItem(string rawKey, object value) { System.Web.Caching.Cache DataCache = HttpRuntime.Cache; // Make sure MasterCacheKeyArray[0] is in the cache - if not, add it if (DataCache[MasterCacheKeyArray[0]] == null) DataCache[MasterCacheKeyArray[0]] = DateTime.Now; // Add a CacheDependency System.Web.Caching.CacheDependency dependency = new CacheDependency(null, MasterCacheKeyArray); DataCache.Insert(GetCacheKey(rawKey), value, dependency, DateTime.Now.AddSeconds(CacheDuration), System.Web.Caching.Cache.NoSlidingExpiration); }
MasterCacheKeyArray 是一個字符串數組 , 它只保存了一個值 ,“ProductsCache ” 。首先 , 在緩存中添加一個緩存條目 , 將其賦值為當前日期與時間。如果該緩存條目已經存在,就更新它。接下來,創建一個緩存依賴項。CacheDependency 類 的構造函數有多個重載,但這里使用的重載接受兩個字符串數組作為輸入參數。第一個參數指定用作依賴項的一組文件。因為我們不打算使用任何基於文件的依賴項,所以對第一個輸入參數使用空值。第二個輸入參數指定用作依賴項的一組緩存鍵值。在這里我們指定唯一的依賴項 ,MasterCacheKeyArray 。然后將 CacheDependency 傳入 Insert 方法。
對 AddCacheItem(key, value) 做了上述修改后 ,要使 緩存失效,只需刪除依賴項即可。
[System.ComponentModel.DataObjectMethodAttribute(DataObjectMethodType.Update, false)] public bool UpdateProduct(string productName, decimal? unitPrice, int productID) { bool result = API.UpdateProduct(productName, unitPrice, productID); // Invalidate the cache InvalidateCache(); return result; } public void InvalidateCache() { // Remove the cache dependency HttpRuntime.Cache.Remove(MasterCacheKeyArray[0]); }
步驟5 : 從 表示層 調用 緩存層
用這些教程中介紹的技巧 , 可以使用 緩存層 的類和方法來對數據進行操作。為了演示怎樣操作緩存數據 , 先保存對ProductsCL 類的更改 , 然后打開 Caching 文件夾中的 FromTheArchitecture.aspx 頁面 , 在其中添加一個 GridView 控件 。從該 GridView 控件的智能標記中,創建一個新的 ObjectDataSource 。在向導的第一步, ProductsCL 類作為一個選項出現於下拉列表中。
圖4 :ProductsCL 類包含在 Business Object 下拉列表中
選擇ProductsCL , 然后 單 擊Next 。SELECT 選項卡中的下拉列表具有兩項 — GetProducts() 和 GetProductsByCategoryID(categoryID) ,而 UPDATE 選項卡只有一個 UpdateProduct 重載方法。從 SELECT 選項卡中選擇 GetProducts() 方法,從 UPDATE 選項卡中選擇 UpdateProducts 方法,然后單擊 Finish 。
圖5 :下拉列表中列出了 ProductsCL 類的方法
完成向導之后 ,Visual Studio 會將 ObjectDataSource 的OldValuesParameterFormatString 屬性設置為 original_{0} 並向 GridView 添加相應的字段。將 OldValuesParameterFormatString 屬性改回默認值 {0} , 配置 GridView 使其支持分頁、排序和編輯。因為 CL 使用的 UploadProducts 重載只接受所編輯產品的名稱與價格 , 所以要限制 GridView 使其只有這兩個字段是可編輯的。
在前面的教程中 , 我們定義了一個包含有 ProductName 、CategoryName 和 UnitPrice 字段的 GridView 控件 。可放心地復制這一格式與結構 , 這樣,GridView 和 ObjectDataSource 的聲明標記看起來應類似如下 :
<asp:GridView ID="Products" runat="server" AutoGenerateColumns="False" DataKeyNames="ProductID" DataSourceID="ProductsDataSource" AllowPaging="True" AllowSorting="True"> <Columns> <asp:CommandField ShowEditButton="True" /> <asp:TemplateField HeaderText="Product" SortExpression="ProductName"> <EditItemTemplate> <asp:TextBox ID="ProductName" runat="server" Text='<%# Bind("ProductName") %>' /> <asp:RequiredFieldValidator ID="RequiredFieldValidator1" ControlToValidate="ProductName" Display="Dynamic" ErrorMessage="You must provide a name for the product." SetFocusOnError="True" runat="server">*</asp:RequiredFieldValidator> </EditItemTemplate> <ItemTemplate> <asp:Label ID="Label2" runat="server" Text='<%# Bind("ProductName") %>'></asp:Label> </ItemTemplate> </asp:TemplateField> <asp:BoundField DataField="CategoryName" HeaderText="Category" ReadOnly="True" SortExpression="CategoryName" /> <asp:TemplateField HeaderText="Price" SortExpression="UnitPrice"> <EditItemTemplate> $<asp:TextBox ID="UnitPrice" runat="server" Columns="8" Text='<%# Bind("UnitPrice", "{0:N2}") %>'></asp:TextBox> <asp:CompareValidator ID="CompareValidator1" runat="server" ControlToValidate="UnitPrice" Display="Dynamic" ErrorMessage="You must enter a valid currency value with no currency symbols. Also, the value must be greater than or equal to zero." Operator="GreaterThanEqual" SetFocusOnError="True" Type="Currency" ValueToCompare="0">*</asp:CompareValidator> </EditItemTemplate> <ItemStyle HorizontalAlign="Right" /> <ItemTemplate> <asp:Label ID="Label1" runat="server" Text='<%# Bind("UnitPrice", "{0:c}") %>' /> </ItemTemplate> </asp:TemplateField> </Columns> </asp:GridView> <asp:ObjectDataSource ID="ProductsDataSource" runat="server" OldValuesParameterFormatString="{0}" SelectMethod="GetProducts" TypeName="ProductsCL" UpdateMethod="UpdateProduct"> <UpdateParameters> <asp:Parameter Name="productName" Type="String" /> <asp:Parameter Name="unitPrice" Type="Decimal" /> <asp:Parameter Name="productID" Type="Int32" /> </UpdateParameters> </asp:ObjectDataSource>
此時 , 我們有了一個使用 緩存層 的頁面。為了看到緩存的運行情況 , 在ProductsCL 類的 GetProducts() 和UpdateProduct 方法中設置斷點。在瀏覽器中訪問該頁面 , 在排序與翻頁時 , 單步執行代碼 , 以便看到從緩存中獲取數據。然后更新一條記錄,注意由於緩存失效,當數據被重新綁定到 GridView 時,它是從 BLL 中獲得的。
注意 : 本文附帶的下載中提供的 緩存層 並不完整。它只包含了一個類 ,ProductsCL ,該類 只有少數幾個方法。此外 , 只有一個ASP.NET 頁面使用了 CL (~/Caching/FromTheArchitecture.aspx) , 所有其它頁面都還是直接調用 BLL 。如果打算在您的應用程序中使用 CL , 那么 表示層 的所有調用都應該是對 CL 的調用, 這就需要 CL 的類和方法要涵蓋 表示層 當前使用的 BLL 中的類和方法。
小結
雖然使用ASP.NET 2.0 的 SqlDataSource 和ObjectDataSource 控件 , 可以在 表示層進行 緩存 , 但理想的做法是由架構中的單獨一層來承擔緩存任務。在本教程中 , 我們創建了一個 緩存層,該層位 於 表示層 與 業務邏輯層 之間。 對於BLL 中已有的由 表示層 調用的類和方法 ,緩存層應該 提供與之相同的一組類與方法。
我們在本教程與前面教程中探討的 緩存層的 例子都展示了應激裝載。對於應激裝載 , 僅當請求了數據 , 並且緩存中沒有這個數據時 , 才會將數據裝載進緩存中。數據也可以預裝載進緩存,該技術會在實際需要數據之前就將數據裝載進緩存。在下一篇教程中,我們將看到預裝載的例子,在那時我們將看到怎樣在應用程序啟動時將靜態值存儲到緩存中。
快樂編程!
出處:http://www.cnblogs.com/codecrazy/archive/2010/10/14/1851934.html