摘要:了解如何通过与 ASP.NET 2.0 所使用的技术类似的技术来使用 ASP.NET 1.1 中的数据库缓存无效化机制。


不知道您看了标题以后有什么感觉? ASP.NET 缓存是到目前为止我最喜欢的 ASP.NET 功能之一。为什么呢? 这是因为,通过使用缓存,可以获得一些奇异的性能和可伸缩性结果,而这些结果可以容易地进行度量并转换为应用程序真正节省的资金。 这会使您成为 CTO 最喜欢的代码猴子,因为您影响了应用程序的至关重要的投资回报率 (ROI) 。 换句话说,缓存确实节省了资金!

从较高级别来说,在 ASP.NET 1.1 中,缓存是作为支持最近最少使用 (LRU) 算法概念的哈希表实现的,以便确保如果需要内存,则可以从缓存中删除项(一组用于在缓存中插入和移除项的编程接口),并且最终实现依赖项、支持时间、文件和关键依赖项等概念。

依赖项模型是缓存的更为重要的功能之一,因为它允许进行如下陈述: 

• 此时此刻,缓存中的该项将不再有效(基于时间的依赖项)。 
 
• 如果该缓存条目更改,则这另外一个缓存条目也无效(基于键的依赖项)。 
 
• 如果该文件更改,则缓存中的该项不再有效(基于文件的依赖项)。 
 

当用缓存编程时,应该总是在使用项之前检查该项是否存在。 因而,在使用缓存时,遵循下面的模式是一个很好的主意:

' #1 – Get a reference to the object
Dim myCachedArrayList As ArrayList = Cache("MyArrayList")

' #2 – Check if the reference is null
If (myCachedArrayList) Is Nothing Then

   ' Add items to array list
   myCachedArrayList = PopulateMyArrayList()

   ' Cache
   Cache.Insert("MyArrayList", myCachedArrayList)

End If

' #3 – Return the data
Return myCachedArrayList

看起来不错,是不是? 我们具有一个灵活的用于在可在某些条件下无效化的内存中存储数据的模型。 该模型存在 ASP.NET 开发人员经常要求解决的几个缺点。 一个缺点是数据库缓存无效化,它意味着当数据库中的某数据更改时,从缓存中移除某项。 第二个缺点是创建自己的缓存依赖项的能力。 

好消息是在 ASP.NET 2.0(先前的代号为“Whidbey”)中,这两个缺点都被解决了。 现在有两个数据库缓存依赖项模型 — 一个用于 Microsoft SQL Server 7 和 2000,另一个用于 SQL Server 2005(先前的代号为“Yukon”)。 第二项大的更改是 CacheDependency 类没有密封,并且被重新管道化,以便您可以为缓存编写自己的依赖项规则。 

坏消息是 ASP.NET 2.0 还不能用于生产用途。 但是,我们目前可以通过 ASP.NET 1.1 生成类似的数据库缓存无效化系统。 实际上,在 2002 年早些时候,当我还在 ASP.NET 团队中时,我就是使用该技术来对用于 Microsoft SQL Server 7 和 SQL Server 2000 的数据库缓存无效化机制进行原型设计的。 

ASP.NET 2.0 中提供的两个数据库缓存无效化选项极为不同。 用于 SQL Server 2000 或 SQL Server 7.0 的选项被限制到一种名为“表级别通知”的功能。 只引发到在 SQL 表上执行的操作的通知。 例如,表上的 UPDATE、INSERT 和 DELETE 操作。 然而在 SQL Server 2005 中,可以从对动态 SQL、存储过程、视图和简单表级别无效化的结果进行的更改接收通知。

遗憾的是,无法通过 ASP.NET 1.1 复制 SQL Server 2005 所使用的数据库缓存依赖项模型,因为数据库中直接内置了新的功能,以便支持在以前版本的 SQL Server 中不可用的通知。

数据库缓存无效化: 您需要了解的内容

目前,有多种用于实现数据库缓存无效化的技术。 其中第一种技术是我在将近四年以前提供的,它使用扩展存储过程技术在数据库中的数据更改时通知 ASP.NET。 该原型是 ASP.NET 团队对如何实现数据库缓存无效化的第一次探索。 如果您希望阅读有关该技术的更多详细信息,则请阅读位于 http://www.dotnetjunkies.com/Tutorial/A4ED ... 57-27CF43A78205.dcik 的文章。

很多人已经使用过的另一种技术,是让 SQL Server 在数据库中的数据更改并且缓存需要无效化时接触外部文件(文件依赖项)。

这些技术的表现非常好,催生了许多美妙的文章,并且非常适合于小型应用程序。 但是,如果您要生成大型的复杂应用程序,则我强烈建议您避免使用这些技术。 举个例子说,我们不会使用这些技术来开发我们自己的应用程序,例如,在 www.asp.net/forums 运行的论坛,或我们正在生成的名为 Community Server 的下一个版本 (www.communityserver.org)。

让我们简要讨论一下为什么这些现有技术存在缺陷 — 首先从扩展存储过程模型开始。

扩展存储过程 HTTP 推模型

扩展存储过程推模型使用以下体系结构: 

• 一个用来向缓存中添加项并且使缓存依赖于某个数据库通知的自定义类。 
 
• 一个可以接收通知并且使缓存中的项无效的 HttpHandler。 
 
• 被监视是否发生更改的数据库中的表上的触发器。 
 
• 数据库中的一个表,用于对被监视是否发生更改的表以及在数据更改时要求通知的应用程序进行跟踪。 
 
• 一个扩展存储过程,用来调用应用程序的 HttpHandler 以通知其发生的更改。 
 

当检测到更改时,无论使用存储过程或触发器中需要的哪个逻辑,都会调用一个更改通知存储过程。 该更改通知过程检索要向其通知更改的 Web 应用程序列表,然后对于每个应用程序,调用一个扩展存储过程,以便对该 Web 应用程序进行 HTTP 调用,以指示它移除特定的缓存条目,从而将更改推送到该应用程序。 Web 应用程序只是接收一个包含需要移除的缓存键的 HTTP 请求。 在内部,该应用程序只是用发送的缓存键调用 Cache.Remove()。

听起来很好,是不是? 是的,而且它实际上能够非常好地工作。 但以下是不利方面: 

• 扩展存储过程向缓存需要无效化的服务器进行 HTTP 回调。 存储过程作为更改数据库内部数据的“原子”操作的一部分执行。 它在表被修改时从触发器中调用。 与其他协议不同,HTTP 不是发后不理;相反,任何给定请求都期待响应。 因而,扩展存储过程无法在 HTTP 调用完成之前完成。 如果 Web 服务器位于慢速网络中或者需要花费较长的时间进行响应,则该延迟会妨碍数据库操作完成。 这一妨碍会进一步导致 SQL Server 可能序列化其他 UPDATES/DELETE/INSERT,或者更糟糕的是,妨碍线程完成。 现在,请将这一因素乘以服务器场中服务器的数量和数据库操作的总数量。 
 
• 如果 Web 服务器在网络园模式(此时会创建多个进程以模拟虚拟 Web 服务器)下运行,则没有办法指示将给定的请求分配给特定的进程。 换句话说,在网络园模式下运行时,服务器会为应用程序运行很多个虚拟 Web 服务器。 当扩展存储过程回调到服务器以通知它更改已经发生时,它没有办法确保所有虚拟 Web 服务器都获得通知。 您最后有可能更新了一个应用程序,但其他几个应用程序却仍然未能同步。 
 

如果您运行的是小型服务器环境,则请不要在网络园模式下运行 Web 服务器,并且 SQL Server 不应该是系统中的争用资源。 扩展存储过程 HTTP 推模型能够很好地工作,但是如果出现应用程序突然增长的情况,则您可能阻塞了数据库或者陷入缓存并不总是同步的情况。

文件更新模型

第二种技术(它无疑比另一种技术更简单)在数据更改时更新文件,而不是试图使用 HTTP 回调到应用程序。 ASP.NET 应用程序开发人员使用标准缓存文件更改依赖项来监视文件更改,当文件更改时,缓存的项被移除。

该技术不存在与扩展存储过程推模型相关联的网络园问题,但是它的确存在很多与扩展存储过程技术相关联的相同阻塞问题。 另外,它引入了它自己的特性: 

• 在使用该技术时,文件争用成为一个问题,因为当“更改”文件没有被另外的操作锁定时,SQL Server 可能会更新该文件。 因而,需要通知更改的数据库中的所有更改必须相互协调以锁定该文件,更改一些值,然后取消锁定该文件。 换句话说,数据库序列化针对该文件进行的工作。 这里面临的问题仍然是 SQL Server 上可能发生的序列化和阻塞问题。 
 
• 在使用该技术时,文件更改通知也会成为问题,因为要在 Web 场中使用更改文件,必须将其放到共享中,并且将正确的安全权限授予各种 Web 服务器,以便查看该共享以及监视文件更改。 
 

文件更新模型对于小型服务器环境能够很好地工作,支持网络园,并且适合于 SQL Server 不是系统中的争用资源的情况。 实际上,它可能是一种更好的选择,因为它比扩展存储过程解决方案简单得多。 但是,在较大的服务器环境或数据库已经是系统中的选通资源的环境中,该模型会崩溃。 

正如您可以看到的那样,这两种技术都具有适用性,但是您需要就是否可以基于服务器大小和负载使用这些技术做出良好决策。 每当您将已知的阻塞操作引入到应用程序中的时候,都会添加潜在的可伸缩性和性能瓶颈。

数据库缓存无效化:ASP.NET 2.0 样式

扩展存储过程缓存无效化模型的原始目标,是开始对数据库缓存无效化问题进行一些早期的原型化工作。 ASP.NET 团队知道该功能是他们希望在版本 2.0 中解决的一个问题,但是他们需要更好地了解如何以可伸缩的方式生成该功能。

实际上,作为一家公司,Microsoft 知道这有多么重要,并且在 ASP.NET、IIS、SQL Server、ADO.NET 和 ISA 服务器等团队的基础上成立了一个新的团队,称为 The Caching Taskforce。

注我的有关 Bill Gates 会议的博客张贴专门用于使他了解我们已经在 ASP.NET/Yukon 实现中完成的工作。 您可以在 http://weblogs.asp.net/rhoward/archive/2003/04/28/6128.aspx 阅读该文章。

该缓存工作组的成绩是两个新的数据库缓存无效化体系结构。 第一个被设计到系统中,并且只是 ASP.NET、ADO.NET 和 SQL Server 2005 的一部分。 它在超粒度级别生成了推通知的可伸缩模型。 例如,当特定存储过程的特定结果更改时,请告诉我。 缓存无效化的 SQL Server 2005 实现无法在 .NET Framework 的版本 1.1 中镜像或实现。 我们将在以后的文章中讨论该系统如何工作。 创建第二种技术的目的是支持 SQL Server 7.0 和 SQL Server 2000 的数据库缓存无效化。 显然,无法向数据库中添加任何东西,因此我们必须在当今技术的约束范围内工作。 好消息是目前可以通过 ASP.NET 1.1 实现与用于 SQL Server 7.0 和 SQL Server 2000 支持的技术完全相同的技术。

我们还没有到吗?

我们全都在电视上见过,或者如果您有孩子的话,您恐怕已经亲身体验过,当孩子坐在汽车中前往某个梦寐已久的地方时,会不停地询问是否已经到达目的地。 此时,他们是在不停地轮询,直到他们收到希望的响应为止。

类似地,用于 SQL Server 7.0 和 SQL Server 2000 的数据库缓存无效化技术轮询数据库以检查是否存在更改。 这里,无意识的反应通常是消极的 — 轮询难道不是一件糟糕的事情吗? 但是,一旦您了解了有关正在发生什么事情的更多信息,该设计的简单性和可伸缩性就会迅速地呈现在您的面前。

对于任何数据库缓存无效化系统而言,都必须克服两个较大的问题: 

• 防止数据库发生阻塞:数据库尽可能地快速和高效是极为重要的,因而在数据更改期间必须避免发生阻塞和序列化。 从基本上说来,数据库的目的是有效地管理对数据的访问并允许进行这种访问。 
 
• 确保缓存一致性:如果一个解决方案只能保证将通知发送到在网络园模式下运行的 Web 服务器中的单个虚拟服务器,则该解决方案是没有用的。 所有应用程序都必须能够在数据更改时收到通知。 
 

用来生成轮询模型的根据是:轮询数据库的成本大大低于重新执行原始查询的成本。 此外,轮询不应当在请求线程上发生,而是应该作为后台操作发生。

这里,一个良好的方案是 Community Server。 Community Server 是一个复杂的应用程序,并且使用很多标准化的表将相关数据结合在一起来满足请求。 一种常见的任务是通过存储过程检索分页的数据集,以便显示特定论坛中的线程的分页视图。 满足分页线程视图请求的存储过程执行一系列从 3 到 5 个表中联接的选择语句,创建一个临时表,从该临时表中进行选择,然后执行另外一次联接。 换句话说,必须发生以满足请求的数据转换完全是一个操作。

通过 ASP.NET 2.0 中使用的数据库缓存无效化模型(我们稍后将实现该模型),我们在数据库中创建了一个更改通知表。 最初请求的数据被在应用程序级别缓存,数据在这里可以快速地进行检索,并且应用程序层每隔几秒钟就会轮询数据库以确定数据是否已经更改。 与原始请求不同,轮询将访问其行深度不大于数据库中表的数量的单个表。 轮询操作从该更改通知表中检索所有记录。 很有可能,该表是如此之小,以至于可以将全部结果分页到 SQL 中的内存中并快速地进行访问。

随后,将在应用程序层内部分析该更改通知表的结果。 结果集是数据库中每个表的更改 ID 的列表。 如果更改 ID 值不同于当前缓存的更改 ID,则会无效化相关的缓存条目,对下一个请求使用完全查找操作(因为数据不在缓存中),并且重新填充缓存,而该进程将重新启动。 图 1 显示了该体系结构的工作方式。


按此在新窗口打开图片
图 1. 体系结构关系图


该轮询机制利用的一个极为重要的方面,是轮询发生在与执行请求的线程不同的后台线程中。 因而,如果可以从缓存中提供结果,则在进行请求时,绝不会访问数据库。 但是,一旦检测到更改,条目将被从缓存中移除,下一个请求将正常执行并重新填充缓存,并且重新启动系统,如图 2 所示。

按此在新窗口打开图片
图 2. 更改之后的体系结构关系图


为了在后台线程中完成轮询,我们将利用 .NET Framework 中的一个我最喜欢并且不太为人所知的类— Timer。 Timer 类位于 System.Threading 命名空间中。 通过 Timer 可以完成的工作是创建一个按照以编程方式确定的时间间隔定期引发的事件。 当 Timer 被唤醒时,它会从当前应用程序域的线程池中抓取一个纤程,并且引发一个事件。 正是在该事件内部,我们的代码可以定期地运行以验证数据库中的更改或缺少更改的情况。

后台服务类

我们使用 Community Server 中的这一相同技术,按照预先设置的时间间隔(而不是针对每个请求)来发送电子邮件或者编制张贴索引。 这已经为我们节省了在添加新张贴的请求中花费的很多时间,因为我们原来是在每次添加新张贴时执行上述操作。

在 Community Server 中,我们使用 Timer 作为 HttpModule 内部的静态实例。 当 ASP.NET 应用程序初始化时,静态计时器被实例化并且轮询内部周期被确定。 当轮询事件引发时,我们执行下列操作: 

• 执行必要的 SQL 以便从数据库中接收一系列更改 ID。 
 
• 将数据库中的更改 ID 与 ASP.NET 缓存中存储的相应值进行比较。 缓存中不匹配的值被更新,这会将依赖项强行从缓存中移除。 
 

从我的 Microsoft Tech Ed 2004 演示文稿(它位于 http://www.rob-howard.net)中的 Blackbelt Slides and Demos 中,您可以下载数据库缓存无效化的完全有效示例。

该特定示例代码片段适用于 SQL Server 随附的 Northwind 示例数据库。 在使用它之前,您还将需要对该数据库进行一些更改。

首先,您需要添加 ChangeNotification 表:

CREATE TABLE [dbo].[ChangeNotification] (
   [Table] [nvarchar] (256) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL ,
   [ChangeID] [int] NOT NULL 
)

其次,您需要向 Products 表中添加一个触发器:

CREATE TRIGGER [ChangeNotificationTrigger] ON [dbo].[Products] 
FOR INSERT, UPDATE, DELETE 
AS
UPDATE
   ChangeNotification
SET
   ChangeID = ChangeID + 1
WHERE
   [Table] = 'Products' 

每当 Products 表被修改时,都会应用该触发器,从而更新 ChangeNotification 表中的行。

尽管这是一个极度简单的示例,但如果您希望为您自己的应用程序实现上述功能,则该示例可以提供一个良好的起点。 该示例中未解决的一些缺点包括: 

• 仅限于表级别更改:为视图甚至行级别更改修改这一点并不十分困难。 
 
• 行锁定:需要添加更新逻辑,以顾及为 Products 表进行的大型修改。 例如,更新 100 种产品会导致对 ChangeNotification 表进行 100 个更改。 与 ASP.NET 2.0 随附的版本类似,您将可能希望添加一些逻辑,以便更好地处理大型更新。 
 

小结

ASP.NET 的缓存系统是所有 ASP.NET 开发人员都应当努力使用的一项功能。 请趁早计划使用缓存,并且了解应用程序的哪些方面可以大大帮助您最佳地使用缓存。 几个 ASP.NET 1.1 限制(例如,数据库缓存无效化)可以使用一些与 ASP.NET 2.0 中使用的技术相同的技术予以克服。 本文中对 Timer 类的使用说明了实现该目标的一种可能的方式,而且,尽管还有其他多个选择,仍然建议您使用该轮询技术。 我认为您还将发现,Timer 类对于其他问题的解决也很有用。

原文地址:http://www.microsoft.com/china/msdn/library ... net/aspnetasp11022004.mspx