如果您是一位 Web 开发人员,您过去可能使用过 ASP.NET 提供的输出缓存功能。 ASP.NET 输出缓存功能是随着 Microsoft .NET Framework 的第一个版本推出的,该功能通过从缓存中检索向站点访问者提供的内容以及避免重新执行页面或控制器,提高了向站点访问者提供内容方面的性能。 当返回您不经常更新的数据或者返回一段时间后将过期的数据时,该功能不需要您的应用程序执行耗费大量资源的数据库调用操作。 ASP.NET 输出缓存使用的是内存存储机制,并且在 NET Framework 4 出现之前,您无法使用您自己的实现覆盖或替代默认缓存。 现在,借助新的 OutputCacheProvider 类型,您可以在 ASP.NET 中实现您自己的缓存页面输出机制。 在本文中,我将为您介绍两种自定义机制。 首先,我将使用 MongoDB(一种常用的面向文档数据库)在一个简单的 ASP.NET MVC 应用程序中创建我自己的提供程序,以方便输出缓存。 然后,我会使用同一个应用程序快速交换我的自定义提供程序,以便利用 Windows Azure AppFabric 的功能 — 具体地说,就是在云中利用 Windows Azure 基础结构提供分布式内存缓存的新 DistributedCache 提供程序。 ASP.NET 中的输出缓存在 ASP.NET Web 窗体应用程序中,可以通过向任意 ASP.NET 页面或用户控件添加 OutputCache Page 指令来配置输出缓存:
对于 ASP.NET MVC 应用程序,输出缓存是使用 ASP.NET MVC 附带的操作筛选器来提供的,该操作筛选器可用作任何控制器操作的一个属性:
“Duration”和“VaryByParam”在 ASP.NET MVC 1 和 2 应用程序中是必需的(VaryByParam 在 ASP.NET MVC 3 中是可选的),这两种机制都提供其他一些属性和参数,这些属性和参数使开发人员能够控制缓存内容的方式(一些 VaryByX 参数)、缓存内容的位置 (Location) 和用于设置缓存无效依赖项的功能 (SqlDependency)。 对于传统的输出缓存,在您的应用程序中实现该功能时不需要任何其他东西。 OutputCache 类型是一个在您的应用程序启动时运行,并在遇到页面指令或操作筛选器时开始发挥作用的 HttpModule。 收到第一个相关的页面或控制器请求后,ASP.NET 将接收生成的内容(HTML、CSS、JavaScript 文件等)并将各个项目以及过期日期和用于标识相应项目的关键字放入内存缓存中。 过期日期由 Duration 属性确定,关键字则由到页面的路径和必要的 VaryBy 值的组合确定 — 例如,如果提供了 VaryByParam 属性,则会查询字符串或参数值。 现在,请考虑一下以这种方式定义的控制器操作:
在这种情况下,对于 vendorState 的各个实例(例如,一个针对德克萨斯州,一个针对华盛顿州等等),ASP.NET 将在请求该州时分别缓存生成的 HTML 视图的一个实例。 在这种情况下,存储各个实例所使用的关键字将是相关路径和 vendorState 的组合。 另一方面,如果将 VaryByParam 属性设置为“none”,则 ASP.NET 将缓存第一次执行 GetVendorList 的结果,并且会向所有后续请求传递相同的缓存版本,而不考虑 vendorState 参数的值是否传入了相应操作。 当没有提供 VaryByParam 值时存储此实例所使用的关键字就是路径。 图 1 简单描述了此过程。 图 1 ASP.NET 输出缓存过程 除了用于控制缓存中的项目的生存期的 Duration 参数以外,还有一些 VaryBy 参数(VaryByParam、VaryByHeader、VaryByCustom、VaryByControl 和 VaryByContentEncoding)用于控制缓存项目的精度,可以配置输出缓存,以控制缓存内容的位置(客户端、服务器或下游代理服务器)。 此外,ASP.NET 2.0 引入了一个 SqlDependency 属性,该属性允许开发人员指定页面或控件所依赖的数据库表,因此,除了在时间上过期以外,您的基础源数据的更新也可能会导致缓存项目过期。 尽管 .NET Framework 2.0 和 3.0 向默认缓存提供程序中引入了一些增强功能,但是提供程序本身仍然没有改变:它仍然是一种内存存储,并没有用于为您提供您自己的实现的扩展点或方法。 在大多数情况下,内存缓存是一种完全可以接受的方法,但有时候,当服务器资源透支且内存不足时,它也会削弱站点性能。 此外,当内存确实不足时,会导致开发人员无法有效地控制管理缓存资源的方式,从而导致默认缓存提供程序机制自动弃用缓存的资源(不考虑指定的持续时间)。 ASP.NET 中可扩展的输出缓存.NET Framework 4 引入了一种新功能,该功能使开发人员能够创建他们自己的输出缓存提供程序,并且只需对新的或现有的应用程序及其配置作少量更改,便可轻松地将这些提供程序插入其中。 无论他们选择的缓存信息使用的是哪种存储机制(例如,本地磁盘、相关和非相关数据库、云,甚至是分布式缓存引擎(如 Windows Server AppFabric 中所提供的)),都可以随意使用这些提供程序。 甚至还可以针对相同应用程序中的不同页面使用多个提供程序。 与创建派生自新的 System.Web.Caching.OutputCacheProvider 抽象类的新类以及覆盖 ASP.NET 使用缓存项目所需的四种方法一样,创建您自己的输出缓存提供程序也很简单。 下面列出了 OutputCacheProvider 类的框架定义(请参见 bit.ly/fozTLc 以了解更多信息):
实现了这四种方法后,接下来要做的就是向您的 web.config 添加新提供程序,将其指定为默认提供程序,并向您的应用程序添加一个 OutputCache 指令或属性。 我会在介绍如何创建我们自己的输出缓存提供程序(使用一个叫做 MongoDB 的文档数据库)时详细说明这些步骤。 首先,我要介绍一些关于在构建自定义提供程序时使用的工具的背景信息,这可能对大家有所帮助。 NoSQL、文档数据库和 MongoDB在过去的数十年中,首选应用程序存储机制一直都是关系数据库管理系统 (RDBMS),该系统在表中存储数据和关系。 SQL Server 和 Oracle 都属于 RDBMS,它们也是目前最受欢迎的商业和开源数据库。 然而,并非所有需要存储的问题都适合相同的事务建模。 二十世纪九十年代末,随着 Internet 的扩展,许多站点都扩大了规模以便托管大量数据,很明显,关系模型在某些类型的数据密集型应用程序上的性能差强人意。 例如,索引大量文档、在高流量站点上向消费者传递网页或向其传递流媒体。 许多公司通过转而使用 NoSQL 数据库解决了他们不断增加的存储需求,NoSQL 数据库是一种不公开 SQL 接口、固定架构或预定义关系的轻型数据库。 NoSQL 数据库得到了企业(例如 Google Inc. (BigTable)、Amazon.com Inc. (Dynamo) 和 Facebook(其收件箱搜索的存储量超过了 50TB))的广泛使用,并且在普及性和使用方面实现了稳定增长。 值得注意的是,虽然有些人将 NoSQL 这个词用作了呼吁停用所有 RDBMS 的口号,但另外一些人则强调同时利用这两类存储的价值。 人们构建 NoSQL 数据库的目的是解决 RDBMS 无法解决的一类问题,而不是要用它彻底代替这些系统。 有辨别能力的开发人员应该会清楚地了解这两种系统,并根据具体情况来利用相应的系统,有时甚至会在一个应用程序中混合使用这两种类型的存储。 非常适合使用 NoSQL 数据库的一种情况就是输出缓存。 NoSQL 数据库对于使用瞬态数据或临时数据来说是理想之选,而从 ASP.NET 应用程序缓存的页面当然符合要求。 MongoDB (mongodb.org) 是一个常用的 NoSQL 选项,Shutterfly、Foursquare、《纽约时报》等许多公司都在使用这种面向文档的 NoSQL 数据库。MongoDB 是一个以 C++ 编写的完全开放的源数据库,该数据库具有面向几乎所有主要编程语言(包括 C#)的驱动程序。 我们将会把 MongoDB 用作我们的自定义输出缓存提供程序的存储机制。 使用 MongoDB 构建自定义的 OutputCacheProvider要开始使用该工具,您需要登录 mongodb.org 下载和安装该工具。mongodb.org/display/DOCS/Quickstart 上提供的文档中包含了在 Windows、Mac OS X 和 Unix 上安装 MongoDB 时所需了解的所有信息。 下载 MongoDB 并使用外壳程序完成测试后,我建议您使用安装目录中的以下命令来将该数据库作为一项服务进行安装(请务必以管理员身份运行 cmd.exe):
MongoDB 将作为一项服务自行安装到您的计算机上,并将使用 C:Datadb 作为其所有数据库的默认目录。通过 diretoryperdb 选项可让 MongoDB 为您创建的每个数据库创建根目录。 运行前一个命令之后,键入以下内容以启动服务:
启动并运行 MongoDB 后,您需要在 .NET 中安装一个驱动程序库以使用 MongoDB。 有几个选项可供使用;我将使用 Sam Corder 创建的 mongodb-csharp 驱动程序 (github.com/samus/mongodb-csharp)。 我们安装了 MongoDB,并且在 .NET 应用程序中有一个可以使用的驱动程序,因此现在是时候创建自定义的输出缓存提供程序了。 为了执行这项操作,我创建了一个叫做 DocumentCache 的新类库,并添加了两个类:DocumentDatabaseOutputCacheProvider 和 CacheItem。 第一个是我的提供程序,它是一个可对抽象 OutputCacheProvider 进行子类化的公共类。 开始的实现过程如图 2 所示。 图 2 起始 OutputCacheProvider 类
请注意,图 2 中的第二个私有变量是指 CacheItem,即我需要在我的项目中创建的另一个类。 CacheItem 的作用是包含我的输出缓存提供程序与 ASP.NET 和我的数据库协同工作所需的相关详细信息,不过它不是我的提供程序所需的外部对象。 因此,我将 CacheItem 定义为内部类,如下所示:
Id 映射到 ASP.NET 向我提供的关键字。 您会想到这个关键字是路径和在您的页面指令或操作属性中定义的任何 VaryBy 条件的组合。 Expiration 字段对应于 Duration 参数,而 Item 属性是要缓存的项目。 我们将通过在 DocumentDatabaseOutputCacheProvider 类的构造函数中进行一些设置来开始实现我们的提供程序。 由于我们知道 ASP.NET 在应用程序的整个生存期内仅保留提供程序的一个实例,因此,我们可以在构造函数中进行一些设置,如下所示:
构造函数创建一个 Mongo 类型的新实例,并使用默认位置 (localhost) 连接到服务器。 之后,它向 MongoDB 请求 OutputCacheDB 数据库以及一个 CacheItem 类型的 IMongoCollection。 由于 MongoDB 是一个架构灵活的数据库,因此支持动态创建数据库。 第一次调用 _mongo.GetDatabase(“OutputCacheDB”) 将返回新数据库的一个实例,当第一次插入发生时,便会在磁盘上创建该数据库。 现在,让我们实现 Add 方法,如图 3 所示。 图 3 实现 Add 方法
我在各个方法中执行的第一个操作就是对传入的关键字调用 MD5 方法。 此方法(为了保持简洁,此处未作详细说明,但您可以从在线源代码下载部分中进行查看)生成基于 ASP.NET 向我提供的关键字的、数据库可以识别的 MD5 哈希。 然后,我调用我的 IMongoCollection<CacheItem> 类型 _cacheItems,以便在底层数据库中查询那个关键字。 请注意传入 FindOne 方法中的匿名类型 (new { _id = key})。 查询 MongoDB 主要是通过选择器对象或在文档中指定一个或多个字段以便在数据库中进行匹配的模板文档来完成的。 _id 是 MongoDB 用于存储文档的关键字,而按照我所使用的驱动程序的约定,该属性会自动映射到我的 CacheItem 类的 Id 属性。 因此,当我保存新的缓存项时,正如您在图 3 中所示的 _cacheItems.Insert 方法中看到的,关键字是使用 Id 属性进行分配的,MongoDB 使用该属性填充记录的内部 _id 字段。 MongoDB 是一种关键字值存储,因此各个 CacheItem 对象都是使用二进制序列化的 JSON 进行存储的,如下所示:
如果我发现一个 CacheItem 具有与传入的关键字相同的关键字,则我会根据当前的 UTC 时间来检查该项目是否过期。 如果该项目尚未过期,则我会利用私有方法(可在在线源代码部分中找到该方法)对它进行二进制反序列化,并返回现有项目。 此外,我向我的存储中插入一个新项目,对其进行二进制序列化,并返回传入的条目。 向缓存中添加了项目后,我便可以添加 Get 方法,该方法将根据关键字查找并返回一个缓存项(如果找不到结果,则返回 null),如图 4 所示。 图 4 实现 Get 方法
与 Add 方法一样,Get 方法也会检查数据库中存在的项目是否过期,如果项目已经过期,则会将其删除并返回 null。 如果存在的项目尚未过期,则会返回该项目。 现在,让我们来实现 Remove 方法,该方法可接受一个关键字并从数据库中删除与该关键字相匹配的项目,如下所示:
正如我们的驱动程序为了获得尚不存在的数据库而使用的代码一样,如果我们尝试删除数据库中不存在的项目,MongoDB 不会报错。 它不会执行任何操作。 根据我们的抽象基类,我们仍需要实现最后一个方法 — Set 方法来获得一个功能性自定义输出缓存提供程序。 我已在图 5 中使用了该方法。 图 5 实现 Set 方法 Public Override Void Set(string key, object entry, DateTime utcExpiry)
总体来看,Add 方法似乎与 Set 方法是相同的,但它们的预期实现之间却存在重要区别。 根据关于 OutputCacheProvider 类的 MSDN 库文档 (bit.ly/fozTLc),自定义提供程序的 Add 方法应在缓存中查找与指定关键字匹配的值,如果存在这样的值,则不对缓存执行任何操作并返回保存的项目。 如果不存在该项目,Add 应插入该项目。 另一方面,Set 方法应始终将其值保存在缓存中,如果不存在该项目,则插入该项目,如果存在,则覆盖该项目。 在关于 Add 的图 3 和关于 Set 的图 5 中,您会注意到,这两种方法都是按指定方式执行的。 实现这四种方法后,现在我们可以运行我们的提供程序了。 在 ASP.NET MVC 中使用 MongoDB OutputCacheProvider编译完我们的自定义提供程序后,我们可以通过几行配置将此提供程序添加到任何 ASP.NET 应用程序中。添加对包含此提供程序的程序集的引用后,将以下文本添加到您的 web.config 文件的 <system.web> 部分:
<providers> 元素定义您想添加到应用程序中的所有自定义提供程序,并为每个提供程序定义名称和类型。由于您可以在一个应用程序中包含多个自定义提供程序,因此您还希望指定 defaultProvider 属性,如我在之前的代码段中的操作一样。 我的示例应用程序是一个带有 CustomersController 的简单 ASP.NET MVC 站点。 该控制器中有一个称为 TopCustomers 的操作,它返回我的业务的顶级客户列表。 此信息经过复杂计算并在我的 SQL Server 数据库中进行多次数据库查询得出,且每小时仅更新一次。 因此,它是缓存的理想候选项。 所以,我在我的操作中添加了一个 OutputCache 属性,如下所示:
现在,如果我运行站点并导航到我的 TopCustomers 页面,那么我的自定义提供程序便会执行。 首先,将调用我的 Get 方法,但是由于此页面还未缓存,因此不会返回任何内容。 接下来,控制器操作将执行并返回 TopCustomers 视图,如图 6 所示。 图 6 缓存的 TopCustomers 视图 之后,ASP.NET 将调用我的自定义缓存提供程序,执行 Set 方法,项目随即被缓存。 我已将持续时间设置为 3,600 秒(即 60 分钟),该时间段内的每个后续请求都将使用我的 Get 方法返回的缓存项,避免重新执行我的控制器操作。 如果任何基础数据发生了更改,那么到期后第一次执行时将显示更新内容,之后,新信息将被缓存一小时。 如果您想查看运行中的 MongoDB,您有两种选择。 您可以打开您的浏览器,导航到 http://localhost:28107/,它显示了日志和有关您数据库的最近查询和统计信息。 或者,您也可以运行 MongoDB 安装路径的 bin 目录下的 mongo.exe,然后通过 Mongo Shell 查询您的数据库。 有关使用这些工具的更多信息,请查看 mongodb.org。 使用 DistributedCache 提供程序如果到目前为止我介绍的所有内容已经超出了您想了解的内容的深度和广度,怎么办呢? 也许您想换用一种缓存机制,但是您是不是既没有时间创建自己的缓存机制,也不希望用自己的缓存机制呢? 您将很高兴地看到,自从推出可扩展的输出缓存以来,目前已有多种不同的缓存机制(由 Microsoft 提供的、商业化的、开源的)可供使用或正处于开发阶段。 其中一个例子就是基于云的分布式内存缓存:当前,DistributedCache 提供程序是作为 Windows Azure AppFabric 的一部分提供的。 如果您已开始构建基于云的应用程序,Windows Azure AppFabric Caching 可以提高对这些应用程序数据的访问速度,因为缓存是作为云服务提供的,设置非常简单且维护方面不需要任何开销。 目前,AppFabric Caching 是 Windows Azure AppFabric 社区技术预览 10 月发行版本的一部分,因此没有活动的 Windows Azure 帐户,您也可以访问缓存功能。 但是,如果您是 MSDN 订户,我强烈建议您在windows.azure.com 上激活您的 Windows Azure 权益。 访问 portal.appfabriclabs.com 并创建一个帐户,以使用开发人员预览功能。 创建 Labs 帐户后,单击“添加服务命名空间”链接,以启用 AppFabric 服务(请参见图 7)。 图 7 Windows Azure AppFabric Labs 摘要页面 设置服务命名空间后,单击“缓存”链接,记下缓存部分列出的服务 URL 和身份验证令牌(请参见图 8)。 您需要使用这些信息配置您的应用程序来使用 DistributedCache 提供程序。 图 8 AppFabric Labs 缓存设置页面 接下来,您需要下载并安装 Windows Azure AppFabric SDK(单击门户中“缓存”页面上的下载链接)。 安装完成后,您就可以为应用程序配置 Windows Azure AppFabric Caching 了。 您需要添加对安装 SDK 时放置在您计算机中的多个程序集的引用。 使用您为您的自定义文档数据库缓存使用的同一 ASP.NET MVC 应用程序,导航到 SDK 安装位置(默认情况下是 C:Program Files*Windows Azure AppFabric SDKV2.0AssembliesCache),然后添加对其中包含的每个程序集的引用。 完成后,打开您的 web.config 并添加以下 <configSections> 条目,同时保留任何现有的配置部分:
接下来,创建 <dataCacheClient> 部分,使用您的门户帐户中的详细信息替换 <host> 名称、cachePort 和 <messageSecurity> authorizationInfo 属性,如下所示:
然后,在 <system.web> 下查找 <caching> 部分,并在您为自定义提供程序创建的条目后添加以下提供程序条目:
最后,将 <outputCache> 元素中的 defaultProvider 属性更改为“DistributedCache”。DistributedCacheOutputCacheProvider 是抽象 OutputCacheProvider 的子类类型,正如我们的 MongoDB 实现一样。 现在,构建并运行您的应用程序,然后导航到“顶级客户”页面。 尝试在列表仍处于缓存状态时添加客户,请注意,正如我们的 MongoDB 实现一样,列表将根据您指定的时间保持缓存状态。 总结在本文中,我介绍了 ASP.NET 输出缓存(默认内存缓存的典型用法),以及使用 .NET Framework 4 中的 OutputCacheProvider 抽象类提供的新的可扩展缓存功能。 我谈到了 NoSQL 和文档数据库,以及这些类型的系统对于使用瞬态数据(如缓存的输出)来说有多么理想。 我使用 MongoDB 构建了一个示例输出缓存,并在 ASP.NET MVC 应用程序中使用了该缓存。 最后,我将输出缓存移动到了云中,经过简单的设置和配置而不需要更改代码,便交换出了我们应用程序中的缓存机制。 可扩展的输出缓存只是 ASP.NET 4 中众多强大的新功能中的一项,我希望对此功能(以及可与此功能一起使用的技术)的这种探索能够给大家带来帮助。 要了解有关 MongoDB 的更多信息,请访问 mongodb.org。要了解有关 Windows Azure AppFabric 的更多信息,请访问portal.appfabriclabs.com/helpandresources.aspx。 Brandon Satrom 是 Microsoft 在德克萨斯州奥斯汀市的开发推广人员。他的博客网址是userinexperience.com,您还可以通过以下 Twitter 地址与他取得联系:@BrandonSatrom。 衷心感谢以下技术专家对本文的审阅:Brian H. Prince 和 Clark Sell |