【原文地址】 Tip/Trick: Implement "Donut Caching" with the ASP.NET 2.0 Output Cache Substitution Feature
【原文发表日期】 Tuesday, November 28, 2006 12:17 AM
一些背景:
ASP.NET中一个强大无比,但往往未被充分利用的功能是它丰富的缓存设施。ASP.NET的缓存功能允许你在服务端避免为来自客户端的每一个新请求做重复的工作,而是,你可以一次生成HTML内容或数据结构,然后在服务器端ASP.NET中缓存或存储其结果,在以后的web请求中重用这些结果。这可以极大地提高你应用的性能,降低对象数据库这样的关键后台资源的负载。
Steve Smith 几年前曾在MSDN上写过一篇很好的关于ASP.NET 1.1 中缓存的文章,讨论了ASP.NET 1.1 缓存功能的一些基本知识,并且对如何使用它们提供了一个很好的总结。如果你以前从没有用过ASP.NET 缓存的话,我建议你先读一下这篇文章,并对其中的每个特性都尝试一下。我也非常建议你观看一下ASP.NET 2.0 免费录像系列中的这个15分钟的关于ASP.NET缓存的“How Do I”录像,这个录像实战演示了ASP.NET 缓存。
ASP.NET 2.0添加了2个非常重要的改进,使得缓存功能更加完善:
1) 对SQL缓存失效的支持- 这允许你在缓存的页面或数据结构所依赖的数据表或记录行被更新时,使缓存内容自动失效然后重新生成缓存内容。例如,你可以在一个电子商务网站上输出缓存你所有的产品列表网页,然后确信在数据库中的产品价格一旦有所变动,这些网页就会在下一个请求时重新生成,这样就不会向用户显示过期的价格数据了。
2) 输出缓存的替换 - 这个奇妙的特性允许你实现我有时称之为“甜圈缓存(donut caching)” 的功能,在这里,你输出缓存页面上的所有东西,但除了几个包含在缓存区域内的动态区域外。这允许你更积极地实现整页输出缓存,不用为了实现局部页面缓存而把你的页面分成多个.ascx用户文件。下面这个技巧/诀窍指南更好地解释了这个特性的促动因素以及其实现。
实战中的场景:
你要在你的网站上实现一个产品列单网页,在上面列出在某个指定产品分类下的所有产品。你也想要输出缓存这个网页,这样,你就不用在每次请求时都访问数据库。你在Products.aspx 网页的顶部用声明的方式添加一个 <%@ OutputCache %> 指令就可以很轻松地达成这个目的,该网页上包含一个绑定到从你的中间层返回的产品数据的 <asp:datalist> 控件。
注意下面该网页是如何设置输出缓存它的内容 100,000 秒或者直到northwind数据库中的 products数据表为新的价格数据所更新为止,在后面这个情形下,下一个请求时,它就会重新生成页面。OutputCache 指令还有一个VaryByParam 属性,它告诉 ASP.NET 为每个独特的categoryID 单独储存一份缓存页面。譬如,Products.aspx?categoryId=1,Products.aspx?categoryId=2等等各有一个单独的缓存页面。
Products.aspx:
Products.aspx.vb:
浏览器访问时,从服务器端返回下面这个页面:
注意,页面底部的时间戳每隔100,000秒,或者当products数据表里的价格数据被更新时,才会被更新。它将会被缓存起来以应付所有其他的 HTTP 请求,允许我们在生产服务器上每秒处理1000个请求,避免了不必要的数据库访问,从而使得访问的速度极快。
问题:
我们在上面的例子中将会遇到的一个问题,跟我们在页面右上方输出的欢迎消息和用户名字(在上面红圈中)有关。目前这是在我们的Site.Master母板页文件中通过使用一个新的ASP.NET 2.0 <asp:loginname> 控件来生成的,象这样:
我们将撞上的问题是,因为我们给我们的页面加了整页输出缓存,第一个访问网站的用户的名字将被保存到页面的缓存输出中,这意味着,在默认情形下,在初始请求之后的 100,000 秒之内访问网站的用户将收到一个错误的欢迎消息,更糟糕的是,显示的是错误的用户名字!
解决方案:
有2个方法可以解决这个问题。
第一个方案是将整个页面做成动态的,即去除顶层的 <%@ OutputCache %> 指令,对页面的内容重构,把所有可缓存的内容都封装在ASP.NET用户控件中,这些用户控件是通过 .ascx 文件来实现的。然后你在这些用户控件的每一个文件的顶部添加 <%@ OutputCache %> 指令,使得它们可以单独缓存。这避免了每次请求都需要访问数据库,确保了用户名字总是正确地输出的,因为用户名字不在缓存的用户控件区域里。这个方法目前在ASP.NET 1.1 里就可行,当然,在ASP.NET 2.0依然行之有效。
但这个方案的缺点是,为使缓存可行,它要求我们重构我们页面的编码和布局。但假如我们在页面上只有几个地方我们想要保持动态,这个重构会非常不方便。好消息是,ASP.NET 2.0 增加了对输出缓存替换块(Output Cache Substitution block )的支持,它提供了一个极其干净的方法来处理这个场景。
使用 <asp:substitution>控件的输出缓存替换块:
输出缓存替换块允许你OutputCache整个页面的输出,同时在HTML输出中留下几个动态区域标记来指明在以后的请求中你需要动态填充内容的地方(譬如,上面例子中的用户名消息)。我有时把这称为“甜圈缓存(donut caching)功能”,因为外部的页面内容都是缓存的,只有页面内容流中间的几个孔(hole)是动态的。这与使用用户控件实现的局部页面缓存正好相反,因为在局部页面缓存的情形下,整个页面是动态的,中间的区域是缓存的。
你通过使用整页输出缓存的方式来实现输出缓存替换,使用与上面 Products.aspx 例子中完全一样的句法。然后,你可以通过在页面上添加 <asp:substitution> 控件来指明页面的哪些区域你需要动态地使用替换块来填充,象这样:
<asp:Substitution> 控件与ASP.NET中的其他控件不同,它与 ASP.NET 的输出缓存注册了一个回调事件,当页面内容在后来的请求中从 ASP.NET输出缓存发出时,该事件会导致你的页面或母板页的一个静态方法的调用。这个静态方法在运行时会传进一个HttpContext 对象,它包含了ASP.NET Request, Response,User, Server,Session, Application等内在对象,然后你就可以使用它们来返回一个字符串,ASP.NET 会在内容发回客户端前自动把这个字符串注入到页面的相关区域里去。
譬如,在输出缓存的products.aspx 页面中,为处理上面这个我们需要动态输出欢迎消息的场景,我们只要简单地添加这个方法到我们的Site.Master后台代码文件中,然后让上面这个 <asp:substitution> 控件来调用:
这样,整个页面将被输出缓存,除了我们页面右上方的 <asp:substitution> 控件代表的欢迎消息的内容外。
很明显地,我们可以把这个进一步延伸,假如我们想要包括另外象用户他们的购物篮有多少样东西这样个人化的信息等。非常酷的是,页面上所有其他的内容仍然是保持完全缓存的,我们不用在后继请求里访问数据库来生成其内容,这意味着我们在单独一个服务器上每秒钟就可以处理成千个产品页。实际上,在请求中,不用生成页面上的任何控件,在以后的请求里,除了上面那个静态方法外,也没有编码会执行,这使得一切都快速无比。
使用Response.WriteSubstitution 方法的输出缓存替换块:
除了使用 <asp:substitution> 控件在页面上指定可替换的内容块外,你也可以使用 Response.WriteSubstitution 方法。这个方法接受一个HttpResponseSubstitutionCallback 方法的delegate对象为参数,你可以在你的应用的任何类里实现这个方法 (而不限于你后台类里的静态方法)。
<asp:substitution> 控件在内部使用这个方法来接连页面的后台类里的delegate方法。同样地,你也可以在你自己的控件或页面使用这个方法,以取得最大的控制和灵活性。
结论:
我还没有找到一个无法从ASP.NET缓存功能上受益的ASP.NET应用。因为ASP.NET支持整页输出缓存,局部页面输出缓存,以及现在的甜圈(donut)层次的缓存,这允许你根据任何参数或你需要的自定义逻辑来变换缓存内容,而现在又允许你在数据库改变时自动使得缓存内容失效并重新生成缓存内容,你不应该发现你自己再会建造一个用不上任何缓存的应用了。
我绝对建议你花点时间熟悉一下ASP.NET所有的缓存功能。想找到我完成的有关缓存的另外的例子的话,请下载我最近在ASP.NET Connections大会上做的技巧/诀窍讲座。下载内包括讲义和演示代码,说明如何使用整页缓存,局部页面缓存,替换块缓存(substitution block caching),以及SQL缓存失效(SQL Cache Invalidation)。
想阅读我写的其他ASP.NET技巧/诀窍博客帖子的话,请浏览我的ASP.NET技巧,诀窍和资源网页。
希望本文对你有所帮助,
Scott