在架構中快取資料 (C#)
在先前的教學中,我們學習如何在表示層應用快取。 在本教學課程中,我們學習如何利用分層架構在業務邏輯層快取資料。 我們透過擴展架構以包含快取層來實現這一點。
簡介
正如我們在前面的教學課程中看到的,快取 ObjectDataSource 的資料就像設定幾個屬性一樣簡單。 不幸的是,ObjectDataSource 在表示層應用快取,這將快取策略與 ASP.NET 頁面緊密耦合。 建立分層架構的原因之一是允許打破這種耦合。 例如,業務邏輯層將業務邏輯與 ASP.NET 頁面分離,而資料存取層將資料存取細節分離。 這種業務邏輯和資料存取細節的解耦是首選,部分原因是它使系統更具可讀性,更易於維護,並且更改起來更靈活。 它還允許領域知識和分工,在表示層上工作的開發人員不需要熟悉資料庫的詳細資訊即可完成工作。 將快取策略與表示層解耦也能帶來類似的好處。
在本教學課程中,我們將增強我們的架構,以包含採用我們的快取策略的快取層 (或簡稱 CL)。 快取層將包含一個類別,ProductsCL
類別透過 GetProducts()
、GetProductsByCategoryID(categoryID)
等方法提供對產品資訊的訪問,在呼叫該類別時,將首先嘗試從快取中擷取資料。 如果快取為空,這些方法將呼叫 BLL 中的對應 ProductsBLL
方法,進而從 DAL 取得資料。 這些 ProductsCL
方法在傳回之前會快取從 BLL 擷取的資料。
如圖 1 所示,CL 位於表示層與業務邏輯層之間。
圖 1:快取層 (CL) 是我們架構中的另一層
第 1 步:建立快取層類別
在本教學課程中,我們將建立一個非常簡單的 CL,其中包含一個只有少數方法的類別 ProductsCL
。 為整個應用程式建立完整的快取層需要建立 CategoriesCL
、EmployeesCL
和 SuppliersCL
類別,並在這些快取層類別中為 BLL 中的每個資料存取或修改方法提供一個方法。 與 BLL 和 DAL 一樣,快取層理想情況下應作為單獨的類別庫項目實現;但是,我們將把它作為 App_Code
資料夾中的類別來實現。
為了更清晰地將 CL 類別與 DAL 和 BLL 類別分開,讓我們在 App_Code
資料夾中建立一個新的子資料夾。 右鍵點擊解決方案資源管理器中的 App_Code
資料夾,選擇“新資料夾”,並將新資料夾命名為 CL
。 建立此資料夾後,向其中新增一個名為 ProductsCL.cs
。
圖 2:新增名為 CL
的新資料夾和名為 ProductsCL.cs
的類別
ProductsCL
類別應包含與其對應的業務邏輯層類別 (ProductsBLL
) 中相同的資料存取和修改方法集。 我們不是建立所有這些方法,而是在這裡建立幾個方法來感受 CL 使用的模式。 特別是,我們將在步驟 3 中新增 GetProducts()
和 GetProductsByCategoryID(categoryID)
方法,並在步驟 4 中新增 UpdateProduct
重載。 您可以隨意加入剩餘的 ProductsCL
方法和 CategoriesCL
、EmployeesCL
、SuppliersCL
以及類別。
步驟 2:讀取和寫入資料快取
前面教學中探討的 ObjectDataSource 快取功能在內部使用 ASP.NET 資料快取來儲存從 BLL 擷取的資料。 也可以透過 ASP.NET 頁面程式碼隱藏類別或 Web 應用程式體系結構中的類別以程式設計方式存取資料快取。 若要從 ASP.NET 頁面的程式碼隱藏類別讀取和寫入資料快取,請使用下列模式:
// 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:快取層的方法從快取返回資料(如果可用)
圖 3 中描述的序列是使用以下模式在 CL 類別中完成的:
Type instance = Cache["key"] as Type;
if (instance == null)
{
instance = BllMethodToGetInstance();
Cache.Insert(key, instance, ...);
}
return instance;
這裡,Type 是儲存在快取中的資料類別型,例如 key 是唯一標識快取 Northwind.ProductsDataTable
項目的鍵。 如果具有指定鍵的項目不在快取中,則實例將在快取中,並且將從適當的 BLL 方法擷取資料並將 null
新增至快取。 當到達 return instance
時,實例包含對資料的引用,無論是來自快取還是從 BLL 中提取。
從快取存取資料時請務必使用上述模式。 下面的模式乍看之下似乎是等效的,但其中包含引入競爭條件的細微差別。 競爭條件很難調試,因為它們偶爾會顯現出來並且很難重現。
if (Cache["key"] == null)
{
Cache.Insert(key, BllMethodToGetInstance(), ...);
}
return Cache["key"];
第二個不正確的程式碼片段的差異在於,資料快取不是在局部變數中儲存對快取項目的引用,而是直接在 return
條件陳述式 和 。 想像一下,當到達此程式碼時,Cache["key"]
是非 null
,但在到達 return
陳述式之前,系統會從快取中逐出鍵。 在這種罕見的情況下,程式碼將傳回一個 null
值而不是預期類別型的物件。
注意
資料快取是線程安全的,因此您不需要為簡單的讀取或寫入同步線程存取。 但是,如果您需要對快取中的資料執行多個需要原子操作的操作,則您需要負責實作鎖定或其他一些機制來確保執行緒安全。 有關詳細資訊,請參閱同步對 ASP.NET 快取的存取。
可以使用以下 Remove
方法以程式設計方式從資料快取中逐出項目:
Cache.Remove(key);
步驟 3:從 ProductsCL
Class 返回產品訊息
在本教學課程中,我們將實作兩種從 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)
方法,它根據指定的鍵從快取中返回特定的項目。 如果在快取中沒有找到這樣的資料,則從相應的 ProductsBLL
類別方法中擷取,然後使用 AddCacheItem(key, value)
方法新增到快取中。
GetCacheItem(key)
和 AddCacheItem(key, value)
方法分別與資料快取、讀取和寫入值介面。 GetCacheItem(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)
鍵值,而是呼叫 方法,GetCacheKey(key)
方法傳回前綴為 ProductsCache- 的鍵。 保存字串 ProductsCache 的 MasterCacheKeyArray
也被 AddCacheItem(key, value)
方法使用,我們稍後會看到。
從 ASP.NET 頁的程式碼隱藏 Page
類別中,可以使用類別的 Cache
屬性存取資料快取,並 允許使用類似 的語法,Cache["key"] = value
如步驟 2 所述。 從架構內的類別中,可以使用 HttpRuntime.Cache
或 HttpContext.Current.Cache
來存取資料快取。 Peter Johnson 的部落格文章 HttpRuntime.Cache vs. HttpContext.Current.Cache 指出使用而不是 HttpRuntime
、HttpContext.Current
具有輕微的效能優勢。因此,使用 ProductsCL
HttpRuntime
。
注意
如果您的體系結構是使用類別庫專案實現的 System.Web
,那麼您將需要新增對組件的參考才能使用 HttpRuntime 和 HttpContext 類別。
如果在快取中沒有找到該項目,則 ProductsCL
類別的方法會從 BLL 取得資料並使用 AddCacheItem(key, 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
表示沒有滑動到期日。 雖然 Insert
方法重載具有絕對到期日和滑動到期日的輸入參數,但您只能提供兩者之一。 如果您嘗試同時指定絕對時間和時間跨度,Insert
方法將引發 ArgumentException
異常。
注意
目前 AddCacheItem(key, value)
方法的實作存在一些缺陷。 我們將在步驟 4 中解決並克服這些問題。
第四步:透過架構修改資料時使快取失效
除了資料擷取方法之外,快取層還需要提供與 BLL 相同的方法來插入、更新和刪除資料。 CL的資料修改方法不會修改快取的資料,而是呼叫BLL對應的資料修改方法,然後使快取失效。 正如我們在前面的教學課程中所看到的,這與 ObjectDataSource 在啟用其快取功能並呼叫其 Insert
、Update
或 Delete
方法時所應用的行為相同。
以下 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
類別的 建構函式有許多重載,但此處使用的建構函式需要兩個 string
陣列輸入。 第一個指定要用作依賴項的檔案集。 由於我們不想使用任何基於檔案的依賴項,因此第一個輸入參數使用 null
值。 第二個輸入參數指定用作依賴項的快取鍵集。 在這裡我們指定我們的單一依賴項 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
類別包含在業務物件下拉清單中 (按一下查看大圖)
選擇 ProductsCL
後,按下一步。 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 以便只有這些欄位是可編輯的。
在前面的教學中,我們定義了 GridView 以包含 ProductName
、CategoryName
和 UnitPrice
欄位。 請隨意複製此格式和結構,在這種情況下,您的 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 中存在的相同的類別和方法集,並從表示層呼叫。
我們在本教學和前面的教學中探索的快取層範例展示了反應式載入。 透過反應式加載,僅當發出資料請求並且快取中缺少該資料時,資料才會加載到快取中。 資料還可以主動載入到快取中,這是一種在實際需要資料之前將資料載入到快取中的技術。 在下一個教學中,當我們了解如何在應用程式啟動時將靜態值儲存到快取中時,我們將看到主動載入的範例。
快樂程式!
關於作者
Scott Mitchell 是七本 ASP/ASP.NET 書籍的作者和 4GuysFromRolla.com 的創始人,自 1998 年以來一直致力於 Microsoft Web 技術。 史考特是一名獨立顧問、培訓師和作家。 他的最新著作是 Sams Teach Yourself ASP.NET 2.0 in 24 Hours。 您可以撥打 mitchell@4GuysFromRolla.com 聯絡他。或者透過他的部落格, http://ScottOnWriting.NET部落格可以在以下位置找到。
特別感謝
本教學系列得到了許多有用的審閱者的審閱。 本教學課程的首席審閱者是 Teresa Murph。 有興趣查看我即將發表的 MSDN 文章嗎? 如果是這樣,請留言給我 mitchell@4GuysFromRolla.com。