缓存体系结构中的数据 (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)
方法, UpdateProduct
并在步骤 4 中添加重载。 你可以随意添加剩余 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 是存储在缓存 Northwind.ProductsDataTable
中的数据类型,例如, 键 是唯一标识缓存项的键。 如果具有指定 键 的项不在缓存中,则 实例 将为 null
,并且将从相应的 BLL 方法检索数据并将其添加到缓存中。 到达 时 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
类返回产品信息
在本教程中,让我们实现两个方法,用于从 ProductsCL
类返回产品信息: GetProducts()
和 GetProductsByCategoryID(categoryID)
。 与 ProductsBL
业务逻辑层中的 类一样, GetProducts()
CL 中的 方法将有关所有产品的信息作为 对象 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 -键。 MasterCacheKeyArray
保存字符串 ProductsCache 的 也由 AddCacheItem(key, value)
方法使用,我们将暂时看到。
从 ASP.NET 页代码隐藏类中,可以使用 类的 Cache
属性访问Page
数据缓存,并允许使用类似于 Cache["key"] = value
的语法,如步骤 2 中所述。 在体系结构中的类中,可以使用 或 HttpContext.Current.Cache
访问HttpRuntime.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 中解决这些问题。
步骤 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
然后打开 文件夹中的页面FromTheArchitecture.aspx
Caching
并添加 GridView。 在 GridView 智能标记中,创建新的 ObjectDataSource。 在向导的第一步中, ProductsCL
应会看到 类是下拉列表中的选项之一。
图 4:类 ProductsCL
包含在业务对象 Drop-Down 列表中 (单击以查看全尺寸图像)
选择 ProductsCL
后,单击“下一步”。 SELECT 选项卡中的下拉列表有两项 - GetProducts()
和 GetProductsByCategoryID(categoryID)
UPDATE 选项卡具有唯一 UpdateProduct
重载。 GetProducts()
从“SELECT”选项卡中选择方法,UpdateProducts
从“更新”选项卡中选择 方法,然后单击“完成”。
图 5:类ProductsCL
方法列在 Drop-Down Lists (单击以查看全尺寸图像)
完成向导后,Visual Studio 会将 ObjectDataSource 属性 OldValuesParameterFormatString
设置为 original_{0}
,并将相应的字段添加到 GridView。 将 OldValuesParameterFormatString
属性更改回其默认值 {0}
,并将 GridView 配置为支持分页、排序和编辑。 UploadProducts
由于 CL 使用的重载仅接受已编辑的产品名称和价格,因此请限制 GridView,以便只有这些字段可编辑。
在前面的教程中,我们定义了 GridView 以包含 、 CategoryName
和 UnitPrice
字段的ProductName
字段。 请随意复制此格式和结构,在这种情况下,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>
此时,我们有一个使用缓存层的页面。 若要查看运行中的缓存,请在 类 GetProducts()
和 UpdateProduct
方法中ProductsCL
设置断点。 在浏览器中访问页面,在排序和分页时逐步浏览代码,以查看从缓存中提取的数据。 然后更新记录并注意缓存已失效,因此,当数据重新绑定到 GridView 时,将从 BLL 中检索它。
注意
本文随附的下载中提供的缓存层不完整。 它仅包含一个类 , ProductsCL
该类仅包含少量方法。 此外,只有单个 ASP.NET 页使用 CL (~/Caching/FromTheArchitecture.aspx
) 所有其他页面仍直接引用 BLL。 如果计划在应用程序中使用 CL,则来自表示层的所有调用都应转到 CL,这将要求 CL 的类和方法涵盖呈现层当前使用的 BLL 中的那些类和方法。
总结
虽然缓存可以在具有 ASP.NET 2.0 s SqlDataSource 和 ObjectDataSource 控件的表示层上应用,但理想情况下,缓存责任将委托给体系结构中的单独层。 在本教程中,我们创建了一个缓存层,该层驻留在表示层和业务逻辑层之间。 缓存层需要提供 BLL 中存在并从表示层调用的相同类和方法集。
我们在本文和前面的教程中探讨的缓存层示例展示了 反应式加载。 使用反应式加载时,仅当对数据发出请求并且缓存中缺少该数据时,数据才会加载到缓存中。 还可以 将数据主动加载 到缓存中,这是一种在实际需要之前将数据加载到缓存中的技术。 在下一教程中,我们将看到一个主动加载示例,其中介绍了如何在应用程序启动时将静态值存储到缓存中。
编程愉快!
关于作者
Scott Mitchell 是七本 ASP/ASP.NET 书籍的作者, 4GuysFromRolla.com 的创始人,自 1998 年以来一直从事 Microsoft Web 技术工作。 Scott 担任独立顾问、培训师和作家。 他的最新书是 山姆斯在24小时内 ASP.NET 2.0自学。 可以在 上联系 mitchell@4GuysFromRolla.com他, 也可以通过他的博客联系到他,该博客可在 http://ScottOnWriting.NET中找到。
特别感谢
本教程系列由许多有用的审阅者查看。 本教程的首席审阅者是 Teresa Murph。 有兴趣查看我即将发布的 MSDN 文章? 如果是,请在 处放置一行 mitchell@4GuysFromRolla.com。