Telligent Systems, Inc.
标题比较口语化,但是引起了您的兴趣,对不对?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/A4ED5FD6-D385-4475-A357-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 类对于其他问题的解决也很有用。