作者 :斯科特·米切尔
缓存可能意味着慢速 Web 应用程序与快速 Web 应用程序之间的差异。 本教程是四个教程中的第一个,其中详细介绍了 ASP.NET 中的缓存。 了解缓存的关键概念,以及如何通过 ObjectDataSource 控件将缓存应用到呈现层。
介绍
在计算机科学中,缓存是一种从获取成本高昂的数据或信息中获取数据并将其副本存储在访问速度较快位置的过程。 对于数据驱动型应用程序,大型和复杂的查询通常占用大部分应用程序执行时间。 然后,通过将昂贵的数据库查询结果存储在应用程序内存中,通常可以提高此类应用程序的性能。
ASP.NET 2.0 提供了各种缓存选项。 可以通过 输出缓存来缓存整个网页或用户控件呈现的标记。 ObjectDataSource 和 SqlDataSource 控件也提供缓存功能,从而允许在控件级别缓存数据。 ASP.NET 数据缓存 提供了丰富的缓存 API,使页面开发人员能够以编程方式缓存对象。 在本教程和接下来的三个教程中,我们将使用 ObjectDataSource 的缓存功能和数据缓存进行检查。 我们还将探讨如何在启动时缓存应用程序范围内的数据,以及如何使用 SQL 缓存依赖项使缓存的数据保持新鲜。 这些教程不探索输出缓存。 有关输出缓存的详细信息,请参阅 ASP.NET 2.0 中的输出缓存。
缓存可以应用于体系结构中的任意位置,从数据访问层到呈现层。 本教程介绍如何通过 ObjectDataSource 控件将缓存应用到呈现层。 在下一教程中,我们将检查业务逻辑层的缓存数据。
关键缓存概念
缓存可以通过获取成本高昂的数据并将其存储在可更高效访问的位置来极大地提高应用程序的整体性能和可伸缩性。 由于缓存仅保留实际基础数据的副本,因此如果基础数据发生更改,则它可能会过时或 过时。 若要解决此问题,页面开发人员可以使用以下任一方法指示缓存项从缓存 中逐出 的条件:
- 基于时间的条件,条目可以在绝对或滑动的持续时间内添加到缓存中。 例如,页面开发人员可能表示持续时间为 60 秒。 在绝对持续时间内,缓存项在添加到缓存后 60 秒被逐出,而不管它访问的频率如何。 在滑动持续时间后,缓存的项在上次访问后 60 秒被逐出。
- 基于依赖关系的标准可以在将依赖项添加到缓存时与项目关联。 当项的依赖项更改时,它会从缓存中逐出。 依赖项可以是文件、另一个缓存项或两者的组合。 ASP.NET 2.0 还允许 SQL 缓存依赖项,使开发人员能够将项添加到缓存,并在基础数据库数据发生更改时将其逐出。 我们将在即将推出的 “使用 SQL 缓存依赖项”教程中检查 SQL 缓存依赖项 。
无论指定的逐出条件如何,缓存中的项都可以在满足基于时间或基于依赖项的条件之前 进行清理 。 如果缓存已达到其容量,必须先删除现有项,然后才能添加新项。 因此,当以编程方式处理缓存的数据时,始终假定缓存的数据可能不存在至关重要。 我们将在下一教程“ 在体系结构中缓存数据”中以编程方式查看从缓存访问数据时要使用的模式。
缓存提供了一种经济手段,用于从应用程序中挤压更多性能。 正如 史蒂文·史密斯 在 文章 ASP.NET 缓存:技术和最佳做法中阐述的那样:
缓存是一种获得足够好性能的好方法,而无需花费大量时间和分析。 内存很便宜,因此,如果可以通过缓存输出 30 秒(而不是花费一天或一周尝试优化代码或数据库)来获取所需的性能,请执行缓存解决方案(假设 30 秒旧数据正常)并继续作。 最终,设计不佳可能会赶上你,因此你当然应该尝试正确设计应用程序。 但是,如果你现在只需要获得满足要求的性能,缓存可能是一种出色的方法,可以为你争取时间,以便在以后有时间时重构应用程序。
虽然缓存可以提供明显的性能增强功能,但在所有情况下都不适用,例如使用实时、频繁更新数据的应用程序,或者即使生存期较短的过时数据也是不能接受的。 但对于大多数应用程序,应使用缓存。 有关 ASP.NET 2.0 中缓存的更多背景信息,请参阅 ASP.NET 2.0 快速入门教程的“缓存性能”部分。
步骤 1:创建缓存网页
在开始探索 ObjectDataSource 缓存功能之前,让我们先花点时间在网站项目中创建 ASP.NET 页面,本教程和接下来的三个页面需要。 首先添加名为 <Site.master
母版页相关联:
Default.aspx
ObjectDataSource.aspx
FromTheArchitecture.aspx
AtApplicationStartup.aspx
SqlCacheDependencies.aspx
图 1:为 Caching-Related 教程添加 ASP.NET 页
与其他文件夹中一样, Default.aspx
该 Caching
文件夹中将列出其部分中的教程。 回想一下, SectionLevelTutorialListing.ascx
用户控件提供了此功能。 因此,通过将此用户控件从解决方案资源管理器拖动到页面的设计视图中,将它添加到Default.aspx
。
图 2:图 2:将用户控件Default.aspx
添加到 SectionLevelTutorialListing.ascx
(单击以查看全尺寸图像)
最后,将这些页面作为条目添加到Web.sitemap
文件中。 具体而言,在处理二进制数据 <siteMapNode>
后添加以下标记:
<siteMapNode title="Caching" url="~/Caching/Default.aspx"
description="Learn how to use the caching features of ASP.NET 2.0.">
<siteMapNode url="~/Caching/ObjectDataSource.aspx"
title="ObjectDataSource Caching"
description="Explore how to cache data directly from the
ObjectDataSource control." />
<siteMapNode url="~/Caching/FromTheArchitecture.aspx"
title="Caching in the Architecture"
description="See how to cache data from within the
architecture." />
<siteMapNode url="~/Caching/AtApplicationStartup.aspx"
title="Caching Data at Application Startup"
description="Learn how to cache expensive or infrequently-changing
queries at the start of the application." />
<siteMapNode url="~/Caching/SqlCacheDependencies.aspx"
title="Using SQL Cache Dependencies"
description="Examine how to have data automatically expire from the
cache when its underlying database data is modified." />
</siteMapNode>
更新 Web.sitemap
后,请花点时间通过浏览器查看教程网站。 左侧菜单现在包括缓存教程相关的项目。
图 3:站点地图现在包括缓存教程的条目
步骤 2:在网页中显示产品列表
本教程介绍如何使用 ObjectDataSource 控件的内置缓存功能。 不过,在查看这些功能之前,我们首先需要一个页面才能使用。 让我们创建一个网页,该网页使用 GridView 列出 ObjectDataSource 从 ProductsBLL
类中检索的产品信息。
首先打开 ObjectDataSource.aspx
文件夹中的页面 Caching
。 将 GridView 从工具箱拖到设计器上,将其 ID
属性 Products
设置为,并从其智能标记中选择将其绑定到名为 ProductsDataSource
的新 ObjectDataSource 控件。 将 ObjectDataSource 配置为使用 ProductsBLL
类。
图 4:将 ObjectDataSource 配置为使用 ProductsBLL
类(单击以查看全尺寸图像)
对于此页面,让我们创建一个可编辑的 GridView,以便我们可以检查通过 GridView 接口修改 ObjectDataSource 中缓存的数据时会发生什么情况。 将 SELECT 选项卡中的下拉列表保留为默认值GetProducts()
,但将 UPDATE 选项卡中的选定项更改为UpdateProduct
的重载,该重载接受productName
、unitPrice
和productID
作为输入参数。
图 5:将 UPDATE 选项卡 Drop-Down 列表设置为适当的 UpdateProduct
重载(单击以查看全尺寸图像)
最后,将 INSERT 和 DELETE 选项卡中的下拉列表设置为“无”,然后单击“完成”。 完成“配置数据源”向导后,Visual Studio 会将 ObjectDataSource s OldValuesParameterFormatString
属性设置为 original_{0}
。 如 “插入、更新和删除数据概述 ”教程中所述,需要从声明性语法中删除此属性或重新设置为其默认值, {0}
以便更新工作流继续不出错。
此外,在向导完成时,Visual Studio 会将一个字段添加到 GridView 中,用于每个产品数据字段。 删除除 ProductName
和 CategoryName
BoundFields 外UnitPrice
的所有内容。 接下来,将 HeaderText
其中每个 BoundField 的属性分别更新为 Product、Category 和 Price。 由于该 ProductName
字段是必需的,因此请将 BoundField 转换为 TemplateField,并将 RequiredFieldValidator 添加到该 EditItemTemplate
字段。 同样,将 UnitPrice
BoundField 转换为 TemplateField 并添加 CompareValidator,以确保用户输入的值是大于或等于零的有效货币值。 除了这些修改之外,还可以随意进行任何美化更改,例如将 UnitPrice
值右对齐,或在只读和编辑界面中指定 UnitPrice
文本的格式。
通过选中 GridView 智能标记中的“启用编辑”复选框,使 GridView 可编辑。 另请查看“启用分页”和“启用排序”复选框。
注释
需要查看如何自定义 GridView 编辑界面? 如果是,请参阅自定义 数据修改接口 教程。
图 6:启用 GridView 对编辑、排序和分页的支持(单击以查看全尺寸图像)
进行这些 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:TextBox>
<asp:RequiredFieldValidator
ID="RequiredFieldValidator1" Display="Dynamic"
ControlToValidate="ProductName" SetFocusOnError="True"
ErrorMessage="You must provide a name for the product."
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"
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" runat="server"
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="ProductsBLL" UpdateMethod="UpdateProduct">
<UpdateParameters>
<asp:Parameter Name="productName" Type="String" />
<asp:Parameter Name="unitPrice" Type="Decimal" />
<asp:Parameter Name="productID" Type="Int32" />
</UpdateParameters>
</asp:ObjectDataSource>
如图 7 所示,可编辑的 GridView 列出了数据库中每个产品的名称、类别和价格。 花点时间测试页面功能,对结果进行排序、分页浏览和编辑记录。
图 7:每个产品名称、类别和价格都列在可排序、可分页、可编辑的 GridView 中(单击以查看全尺寸图像)
步骤 3:检查 ObjectDataSource 请求数据的时机
Products
GridView 通过调用 ProductsDataSource
ObjectDataSource 的Select
方法来检索要显示的数据。 此 ObjectDataSource 创建业务逻辑层的ProductsBLL
类实例,并调用其GetProducts()
方法,该方法又调用数据访问层的ProductsTableAdapter
类的GetProducts()
方法。 DAL 方法连接到 Northwind 数据库并发出配置的 SELECT
查询。 然后,此数据将返回到 DAL,该 DAL 将其打包在一个 NorthwindDataTable
中。 DataTable 对象将返回到 BLL,该对象将返回给 ObjectDataSource,后者将其返回到 GridView。 然后,GridView 将为 DataTable 中的每个 DataRow
创建一个 GridViewRow
对象,每个 GridViewRow
最终都会被渲染成返回给客户端并在访问者浏览器中显示的 HTML。
每次 GridView 都需要绑定到其基础数据时,都会发生此事件序列。 当首次访问页面、从一页数据移动到另一页、对 GridView 进行排序或通过内置编辑或删除接口修改 GridView 数据时,就会发生这种情况。 如果禁用 GridView 的视图状态,则 GridView 也会在每个回发上反弹。 GridView 还可以通过调用 DataBind()
方法显式地重新绑定到其数据。
为了充分了解从数据库检索数据的频率,让我们显示一条消息,指示何时重新检索数据。 在名为 ODSEvents
GridView 的上方添加标签 Web 控件。 清除其 Text
属性并将其属性设置为 EnableViewState
false
。 在标签下,添加按钮 Web 控件并将其 Text
属性设置为回发。
图 8:向 GridView 上方的页面添加标签和按钮(单击以查看全尺寸图像)
在数据访问工作流期间,ObjectDataSource 事件 Selecting
会在创建基础对象及其已配置的方法调用之前触发。 为此事件创建事件处理程序并添加以下代码:
protected void ProductsDataSource_Selecting(object sender,
ObjectDataSourceSelectingEventArgs e)
{
ODSEvents.Text = "-- Selecting event fired";
}
每次 ObjectDataSource 向体系结构发出数据请求时,标签将显示触发的文本“选择”事件。
在浏览器中访问此页面。 当首次访问页面时,将显示“选择事件已触发”的文本。 单击“回发”按钮并注意文本消失(假定 GridView 的属性 EnableViewState
设置为 true
默认值)。 这是因为,在回发时,GridView 会从其视图状态重新构造,因此不会转向 ObjectDataSource 以获取其数据。 但是,对数据进行排序、分页或编辑会导致 GridView 重新绑定到其数据源,因此选择触发的事件文本将重新出现。
图 9:每当 GridView 重新绑定到其数据源时,“选择事件触发”就会显示(单击以查看全尺寸图像)
图 10:单击回发按钮会导致从其视图状态重新构造 GridView(单击以查看全尺寸图像)
每次对数据进行分页或排序时,检索数据库数据似乎很浪费。 毕竟,由于我们使用的是默认分页,ObjectDataSource 在显示第一页时检索了所有记录。 即使 GridView 不提供排序和分页支持,每次用户首次访问页面时都必须从数据库中检索数据(如果禁用视图状态,则每次回发时)。 但是,如果 GridView 向所有用户显示相同的数据,则这些额外的数据库请求是多余的。 为什么不缓存从 GetProducts()
方法返回的结果并将 GridView 绑定到那些缓存的结果?
步骤 4:使用 ObjectDataSource 缓存数据
只需设置一些属性,即可将 ObjectDataSource 配置为自动缓存其检索到的数据 ASP.NET 数据缓存中。 以下列表汇总了 ObjectDataSource 的缓存相关属性:
-
EnableCaching 必须设置为
true
启用缓存。 默认值为false
。 -
CacheDuration 缓存数据的时间量(以秒为单位)。 默认值为 0。 仅在
EnableCaching
设置为true
且CacheDuration
设定值大于零的情况下,ObjectDataSource 才会缓存数据。 -
CacheExpirationPolicy 可以设置为
Absolute
或Sliding
。 如果Absolute
,ObjectDataSource 会缓存其检索到的数据CacheDuration
秒;如果Sliding
,则数据只有在未被访问CacheDuration
秒后才过期。 默认值为Absolute
。 -
CacheKeyDependency 使用此属性将 ObjectDataSource 的缓存条目与现有缓存依赖项相关联。 通过使其关联的
CacheKeyDependency
项过期,可以将 ObjectDataSource 的数据条目提前从缓存中移除。 此属性通常用于将 SQL 缓存依赖项与 ObjectDataSource 缓存相关联,我们将在将来 使用 SQL 缓存依赖项 教程中探讨一个主题。
让我们将 ProductsDataSource
ObjectDataSource 配置为在绝对刻度上缓存其数据 30 秒。 将 ObjectDataSource 的EnableCaching
属性设置为true
,并将其CacheDuration
属性设置为30。 将CacheExpirationPolicy
属性设置为其默认值。 Absolute
图 11:将 ObjectDataSource 配置为缓存其数据 30 秒(单击以查看全尺寸图像)
保存所做的更改,并在浏览器中重新访问此页面。 首次访问页面时,将显示“选择事件触发”文本,因为最初数据不在缓存中。 但是,通过单击“回发”按钮、排序、分页或单击“编辑”或“取消”按钮触发的后续回发不会重新显示由‘选择’事件触发的文本。 这是因为 Selecting
仅当 ObjectDataSource 从其基础对象获取其数据时才会触发该事件; Selecting
如果数据从数据缓存中拉取,则不会触发该事件。
30 秒后,数据将从缓存中逐出。 如果调用 ObjectDataSource 的Insert
、Update
或Delete
方法,数据也会从缓存中逐出。 因此,当经过 30 秒或者点击了“更新”按钮之后,再进行排序、分页或点击“编辑”或“取消”按钮时,ObjectDataSource 将会从其基础对象获取数据,并在 Selecting
事件触发时显示“选择”事件已触发的文本。 这些返回的结果将放回到数据缓存中。
注释
如果您频繁看到“Selecting 事件触发”消息,即使您期望 ObjectDataSource 处理缓存的数据,这可能是由于内存限制所致。 如果内存不足,ObjectDataSource 添加到缓存中的数据可能已被清理。 如果 ObjectDataSource 似乎未正确缓存数据,或者只是偶尔缓存数据,请关闭某些应用程序以释放内存,然后重试。
图 12 说明了 ObjectDataSource 的缓存工作流。 当“选择事件触发”文本出现在屏幕上时,这是因为数据不在缓存中,必须从基础对象中检索。 但是,当缺少此文本时,这是因为数据可从缓存获取。 从缓存返回数据时,不会调用基础对象,因此,不会执行数据库查询。
图 12:ObjectDataSource 存储数据并从数据缓存中检索其数据
每个 ASP.NET 应用程序都有自己的数据缓存实例,这些实例在所有页面和访问者之间共享。 这意味着,ObjectDataSource 存储在数据缓存中的数据同样在访问页面的所有用户之间共享。 若要验证这一点,请在 ObjectDataSource.aspx
浏览器中打开页面。 首次访问页面时,将显示“选择”事件触发的文本(假设先前测试添加到缓存的数据现已被逐出)。 打开第二个浏览器实例,并将 URL 从第一个浏览器实例复制并粘贴到第二个浏览器实例。 在第二个浏览器实例中,不显示“选择”事件触发的文本,因为它使用与第一个相同的缓存数据。
在将检索到的数据插入缓存时,ObjectDataSource 使用包含以下内容的缓存键值:CacheDuration
和 CacheExpirationPolicy
属性值;ObjectDataSource 使用的基础业务对象的类型,该对象通过 TypeName
属性 指定(在此示例中为 ProductsBLL
);SelectMethod
属性的值及 SelectParameters
集合中参数的名称和值;以及其 StartRowIndex
和 MaximumRows
属性的值,这些属性在实现 自定义分页 时使用。
将这些属性组合成缓存键值,可确保随着这些值的变化,缓存项是唯一的。 例如,在以往的教程中,我们使用了 ProductsBLL
类, GetProductsByCategoryID(categoryID)
该类返回指定类别的所有产品。 一位用户可能会来到页面并查看饮料,其数量为 CategoryID
1。 如果 ObjectDataSource 缓存了其结果而不考虑 SelectParameters
这些值,则当其他用户来到页面查看饮料产品时,它们会看到缓存的饮料产品,而不是调味品。 通过根据这些属性(包括SelectParameters
的值)来变化缓存键,ObjectDataSource 可以为饮料和调味品分别维护不同的缓存条目。
数据过时问题
当调用任一对象或方法时,ObjectDataSource 会自动从缓存中逐出其Insert
Update
Delete
项。 这有助于通过在页面修改数据时清除缓存条目来防止过时数据。 但是,使用缓存的 ObjectDataSource 仍可能显示过时的数据。 最简单的情况可能是由于数据直接在数据库中发生更改。 也许数据库管理员刚刚运行了一个脚本来修改数据库中的某些记录。
这种情况也可以以更微妙的方式展开。 当调用其中一个数据修改方法时,ObjectDataSource 会从缓存中逐出其项,但删除的缓存项用于 ObjectDataSource 的特定属性值组合(CacheDuration
、 TypeName
等等 SelectMethod
)。 如果有两个 ObjectDataSource 使用不同的SelectMethods
或SelectParameters
,但仍然可以更新相同的数据,则一个 ObjectDataSource 可能会更新一行并使其自己的缓存条目失效,但第二个 ObjectDataSource 的相应行仍将从缓存中获取。 我鼓励你创建页面来展示此功能。 创建一个页面,该页面显示一个可编辑的 GridView,该 GridView 从使用缓存的 ObjectDataSource 中提取数据,并配置为从 ProductsBLL
类的 GetProducts()
方法获取数据。 将另一个可编辑的 GridView 和 ObjectDataSource 添加到此页面(或其他页面),但对于第二个 ObjectDataSource,请使用GetProductsByCategoryID(categoryID)
方法。 由于两个 ObjectDataSources SelectMethod
属性不同,因此它们各自具有自己的缓存值。 如果在一个网格中编辑产品,下次将数据绑定到另一个网格(通过分页、排序等)时,它仍将提供旧的缓存数据,而不是反映从另一个网格所做的更改。
简言之,仅当愿意拥有过时数据的可能性时,才使用基于时间的到期时间,并在数据新鲜度非常重要的情况下使用较短的到期时间。 如果过时数据不可接受,请放弃缓存或使用 SQL 缓存依赖项(假设它是要缓存的数据库数据)。 我们将在将来的教程中探索 SQL 缓存依赖项。
概要
在本教程中,我们研究了 ObjectDataSource 的内置缓存功能。 只需设置几个属性,就可以指示 ObjectDataSource 将指定 SelectMethod
的结果缓存到 ASP.NET 数据缓存中。 属性CacheDuration
CacheExpirationPolicy
指示缓存项的持续时间,以及该项是绝对过期还是滑动过期。 该 CacheKeyDependency
属性将所有 ObjectDataSource 缓存条目与现有缓存依赖项相关联。 这可用于在达到基于时间的过期之前从缓存中逐出 ObjectDataSource 的条目,并且通常用于 SQL 缓存依赖项。
由于 ObjectDataSource 只将其值缓存到数据缓存,因此我们可以以编程方式复制 ObjectDataSource 的内置功能。 在呈现层执行此作并不有意义,因为 ObjectDataSource 现成提供此功能,但我们可以在体系结构的单独层中实现缓存功能。 为此,我们需要重复 ObjectDataSource 使用的相同逻辑。 我们将在下一教程中了解如何从体系结构中以编程方式处理数据缓存。
快乐编程!
深入阅读
有关本教程中讨论的主题的详细信息,请参阅以下资源:
关于作者
斯科特·米切尔,七本 ASP/ASP.NET 书籍的作者和 4GuysFromRolla.com 的创始人,自1998年以来一直在与Microsoft Web 技术合作。 斯科特担任独立顾问、教练和作家。 他的最新书是 《Sams Teach Yourself ASP.NET 2.0 in 24 Hours》。 可以通过 mitchell@4GuysFromRolla.com 联系到他。
特别致谢
本教程系列由许多有用的审阅者审阅。 本教程的主要审阅者是 Teresa Murphy。 有兴趣查看即将发布的 MSDN 文章? 如果是这样,请给我写信。mitchell@4GuysFromRolla.com