开发人员都喜欢 ASP.NET 应用程序缓存。 一个原因是 ASP.NET 能够在放入缓存中的项与文件系统中的文件之间创建相关性。 如果相关性所针对的文件更改,ASP.NET 会自动将相关项从缓存中删除。 通过与缓存删除回叫(当缓存项删除时向所有关注方广播通知)结合,缓存相关性为开发人员提供了方便,使他们得以通过尽量减少耗时的文件访问来最大限度地提高性能,因为这使他们可以放心地允许文件数据缓存,而不必担心数据变得陈旧。
尽管缓存相关性非常实用,但是在 ASP.NET 1.0 中还缺乏一项至关重要的功能,这项功能一旦存在,将会使缓存相关性随着开发人员的美梦成真而得到证明。 这项功能就是对数据库实体的支持。 在现实情况中,大多数 Web 应用程序都是从数据库中提取数据,而不是从文件中提取数据。 尽管 ASP.NET 能够非常出色地将缓存项链接到文件,但无法将缓存项链接到数据库实体。 也就是说,可以将文件内容读入 DataSet 中,然后缓存 DataSet,并使 DataSet 在初始化时所在的文件更改时自动从缓存中删除。 但是,无法使用数据库查询来初始化 DataSet,因此也就无法缓存 DataSet 并使 DataSet 自动在数据库更改时废弃。 这确实太糟糕了,因为正是由于数据库访问过多(例如,文件 I/O 过多)而导致应用程序性能下降。
ASP.NET 不支持数据库相关性并不意味着数据库相关性不可能实现。 这一部分 Wicked Code 展示了扩展 ASP.NET 应用程序缓存以支持数据库相关性的技术。 它包含数据库触发器及扩展存储过程。 尽管这里展示的实现仅适用于 Microsoft? SQL Server?,但是大体技术也适用于支持与文件系统交互的触发器和用户定义过程的任何数据库。
活动的数据库相关性
先看一个演示。
图 1 包含 ASP.NET 页的源代码,该 ASP.NET 页显示从名为 Quotes 的 SQL Server 数据库中随机选择的语录。 要创建数据库,请运行安装脚本。
图 2 显示的是缩略形式的安装脚本。 完整的脚本包含在本专栏附带的可下载 zip 文件中。 (可以在 SQL Server 查询分析器内部执行该脚本,或者使用 OSQL 命令从命令行执行该脚本。) 每次提取该页时,Page_Load 都使用数据库的 Quotations 表中的所有记录初始化一个 DataSet,然后从该 DataSet 中随机选择一个记录,并将其写入到该页中。 按 F5 若干次,您将看到出自某些著名(以及不那么有名的)人士的各种随机语录,如图 3 所示。
图 3 随机语录
由于某一原因,该页被命名为 DumbDBQuotes.aspx。 每次请求该页时都查询数据库并不是很明智的做法。 在每一次访问该页时都访问数据库(尤其是当数据库是由远程服务器承载时)是生成不具可伸缩性的数据库的可靠方法。
ASP.NET 应用程序缓存可解决数据库访问过多的问题。 如果 DataSet 已缓存,便可以直接从内存(也就是缓存)中提取它,从而避免了多余的数据库访问。 DataSet 的缓存非常容易。应用程序缓存接受从 System.Object 派生的任何类型的实例。在 Microsoft .NET Framework 中,这意味着任何托管类型(包括 DataSet)的实例。 问题在于,如果在 DataSet 缓存之后,它底层的数据库更改,您为用户提供的将是陈旧的数据。 可以实现一个定期重新查询数据库的解决方案,但是理想的解决方案应该满足以下条件:不需要轮询,并且在数据源中的已更新数据一旦变得可用时便立即将其交付。
请看图 4 和 图 5,这两个图中包含更智能的语录应用程序的源代码。 SmartDBQuotes.aspx 不从数据库中检索语录,而是从应用程序缓存中获取语录。 Global.asax 填充缓存,并在数据库更改时刷新缓存。 下面是这两段源代码的试用说明:
在 Web 服务器的 C: 驱动器的根目录中创建一个名为 AspNetSql 的子目录。 在 AspNetSql 内部,创建一个名为 Quotes.Quotations 的零字节文件。 确保 Everyone,或者至少 SYSTEM 和 ASPNET(安装 ASP.NET 时创建的特殊帐户)有权访问 Quotes.Quotations。
将包含在本专栏的可下载代码示例中的 XSP.dll 复制到 SQL Server 的 binn 目录(例如,C:\Program Files\Microsoft SQL Server\MSSQL\Binn)中,或者复制到 Web 服务器上 Windows 将在其中自动搜索 DLL 的任意位置(例如,C:\Windows\System32)。
从外部来看,DumbDBQuotes.aspx 和 SmartDBQuotes.aspx 很相似,产生的输出也完全相同。 从内部来看,则两者截然不同。 前者在每次被请求时都执行数据库访问,后者从应用程序缓存中提取数据。 此外,SmartDBQuotes.aspx 使用数据库相关性来确保缓存数据随着数据库的更改而更改。 如果数据库不更改,在应用程序的生命期中,将仅仅查询一次数据库。 如果数据库更改,将再查询一次数据库以更新缓存。
使用图 6 中的已修改脚本重新生成数据库。
将 Global.asax 和 SmartDBQuotes.aspx 部署到 Web 服务器上的虚拟目录(例如,wwwroot)中。
在浏览器中请求 SmartDBQuotes.aspx。 将页面刷新若干次,直到“The use of COBOL cripples the mind; its teaching should therefore be regarded as a criminal offense”语录出现。
使用 SQL Server 企业管理器或您选择的工具在 Quotes 数据库的 Quotations 表中修改该语录。 将它更改为“The use of Visual Basic? enriches the mind; its teaching should therefore not be regarded as a criminal offense”。 然后刷新该页,直到修改后的语录出现。 请注意,出现的是新语录而不是旧语录,尽管查询结果现在存储在应用程序缓存中。
刚才展示了 ASP.NET 应用程序缓存现在可以与数据库相关性结合起来,从而产生高效率的数据驱动应用程序。 问题是怎样才能将两者结合起来呢? 怎样在缓存的 DataSet 与数据库之间形成链接,以及解决方案的可伸缩性如何呢?
数据库相关性的工作原理
从外部来看,DumbDBQuotes.aspx 和 SmartDBQuotes.aspx 很相似,产生的输出也完全相同。 从内部来看,则两者截然不同。 前者在每次被请求时都执行数据库访问,后者从应用程序缓存中提取数据。 此外,SmartDBQuotes.aspx 使用数据库相关性来确保缓存数据随着数据库的更改而更改。 如果数据库不更改,在应用程序的生命期中,将仅仅查询一次数据库。 如果数据库更改,将再查询一次数据库以更新缓存。
图 7 数据库相关性
图 7 说明了数据库相关性的工作原理。 当 Global.asax 将 DataSet 放入缓存中时,在该 DataSet 与 C:\AspNetSql 目录中名为 Quotes.Quotations 的文件之间创建文件系统相关性。Quotes.Quotations 是一个零字节的信号文件,它唯一的作用是触发 ASP.NET 应用程序的缓存删除逻辑。 下面是 Global.asax 中的语句,它创建将 DataSet 链接到 Quotes.Quotations 的 CacheDependency 对象:
new CacheDependency ("C:\\AspNetSql\\Quotes.Quotations")
Global.asax 还注册自己的 RefreshCache 方法。当 DataSet 从缓存中删除(也就是当信号文件更改时)将调用该方法。该方法的代码如下:
new CacheItemRemovedCallback (RefreshCache)
RefreshCache 的任务是查询数据库并将所产生的 DataSet 放入应用程序缓存中。 它在应用程序启动时调用一次,并在 DataSet 从缓存中删除时再调用一次。
这只是整个体系的一半。 另一半涉及到数据库。 图 6 中是修改后的数据库安装脚本,该脚本在数据库的 Quotations 表中添加一个 insert/update/delete 触发器。
CREATE TRIGGER DataChanged ON Quotations
FOR INSERT, UPDATE, DELETE
AS EXEC master..xsp_UpdateSignalFile 'Quotes.Quotations'
GO
当在表中添加或删除记录以及当记录更改时此触发器触发。 触发器的工作原理是怎样的呢? 它调用一个名为 xsp_UpdateSignalFile 的扩展存储过程(扩展存储过程是 SQL Server 中用来指代 Win32? DLL 中的代码的委婉用语)。 然后该扩展存储过程使用 Win32 CreateFile 函数来更新 Quotes.Quotations 的时间戳。
缓存 DataSet 的生命期通过普通的文件系统缓存相关性与 Quotes.Quotations 关联;更新 Quotations 表会导致数据库触发器触发;而该触发器调用“更新”Quotes.Quotations 的扩展存储过程,从而促使 ASP.NET 将 DataSet 从应用程序缓存中删除,并调用 Global.asax 的 RefreshCache 方法,之后该方法将执行一个全新的数据库查询,并重新启动整个过程。
这个难题的最后一个关键点是扩展存储存储。 它存放在以前安装的 XSP.dll 中。 此 XSP.dll 是用 Visual C++? 6.0 在非托管的 C++ 中编写的。 它的源代码显示在图 8 中。 信号文件的路径 C:\AspNetSql 硬编码到了该 DLL 中,但是您可以根据需要对它进行更改,并使它像文件名一样成为一个输入参数。
在使用扩展存储过程之前,必须先安装。 所执行的 SQL 安装脚本中的下列语句将 xsp_UpdateSignalFile 安装