缓存(输出缓存)
缓存是内存中保存创建代价高的信息副本的一种技术。服务器内存是有限的资源,如果在其中保存了太多的信息,某些信息就会保存到硬盘的页面文件上,这样可能会减慢整个系统。最佳的缓存策略(如内置在 ASP.NET 中的)是自我约束的。
信息的生命周期由服务器自行管理,如果缓存满了或者其他应用程序消耗了大量的内存,信息将会选择性的从缓存移除以保持性能。正是这种自我管理,使得缓存如此强大(也正因为如此,实现你自己的缓存是非常复杂的)。
理解 ASP.NET 缓存
ASP.NET 有 2 种类型的缓存。你的应用程序能够也应该同时使用这 2 种类型,因为它们是互补的。
- 输出缓存:这是最简单的缓存类型。它保存最终生成的发送到客户端的 HTML 页面的一个副本。下一个客户再次请求这个页面时,页面没有真正运行。运行页面及其代码的时间完全被省下来了。
- 数据缓存:它由你的代码手工引入。使用数据缓存时,你将重建起来比较耗时(如从数据库获得DataSet)的重要对象保存在缓存内。其他页面可用检查这一信息是否已经存在,然后重用它,这样可以省略获取它们所需的步骤。
此外,还有建立在这两个模型上的两类特殊缓存:
- 部分页缓存:它是输出缓存的特殊类型,仅缓存部分 HTML 。部分缓存保存页面上用户控件的 HTML 输出,下一次执行页面时,同样的页面事件还会发生(这样你的页面代码仍会执行),但是相应的用户控件的代码不会被执行。
- 数据源缓存:这种缓存建立在数据源控件(包括 SqlDataSource、ObjectDataSource 和 XmlDataSource)内。就技术而言,数据源缓存使用数据缓存。差别在于你不必显式处理这一过程。你只需要配置适当的属性,数据源控件就会自行管理缓存的保存和读取。
输出缓存
使用输出缓存时,页面最终产生的 HTML 被缓存,当再次请求相同的页面时,不会创建控件对象,不会开始新的页面生命周期,你的代码页不会被执行。理论上,输出缓存可以达到最大的性能提升。
protected void Page_Load(object sender, EventArgs e)
{
lblDate.Text = "The time is now:<br />" + DateTime.Now.ToString();
}
有两个办法把页面加入到输出缓存中。最常见的办法是在 .aspx 文件顶端 Page 指令下面加入:
<%@ OutputCache Duration="20" VaryByParam="None" %>
Duration 特性告诉 ASP.NET 将页面缓存 20 秒。20秒看似很短,不过对于一个高访问量的网站来说,已经带来显著的差别。数据库每分钟最多只会被访问 3 次,而不使用缓存,客户端每次请求时都要访问数据库,这些请求很容易就达到每分钟数十次或更高!
当然,并不会因为你设置了20秒,它就一定会保存那么久,系统可能会因为内存紧张而提前把页面从缓存中移除。(这个机制也保证了你可以自由使用缓存,无须过多担心因为大量使用内存而影响你的应用程序。)
缓存和查询字符串
把 VaryByParam 特性设置为“*”,它表示页面要使用查询字符串,同时告诉 ASP.NET 按不同的查询字符串参数缓存页面的独立副本:
<%@ OutputCache Duration="20" VaryByParam="*" %>
现在当你请求带有查询字符串信息的页面时,ASP.NET 会首先检查查询字符串,如果字符串和以前的请求匹配且该页面的缓存副本存在,那么它将被重用。否则 ASP.NET 会创建一个新的页面并单独缓存它。
下一过程能帮助你更好的理解输出缓存:
- 不使用查询字符串请求页面,获得页面的副本 A
- 使用参数 ProductID=1 请求页面,获得页面的副本 B
- 另一个用户使用 ProductID=2 请求页面,获得页面的副本 C
- 另一个用户使用 ProductID=1 请求页面,如果缓存中的 B 还没有过期,他获得页面的副本 B
- 这个用户不使用查询字符串请求页面,如果 A 还没有过期,A 也会从缓存中送出
使用特定查询字符串参数的缓存
这项技术也有潜在的问题。可以接收很大范围的查询字符串参数的页面就不适用于输出缓存,可能的参数值数量巨大,潜在的重用性就非常低,尽管这些页面会在需要内存时自动从缓存中移除,但它们还是可能使其他更为重要的信息提前从缓存移除或者减慢其他操作。
大多数情况下,把 VaryByParam 设为“*”不合适。通常,通过名称明确指定重要的查询字符串变量会更好一些:
<%@ OutputCache Duration="20" VaryByParam="ProductID" %>
这样,ASP.NET 会在查询字符串中查找 ProductID 参数,使用不同的 ProductID 参数的请求被分别缓存,而其他参数都被忽略。
通过使用分号分隔,还可以指定多个参数:
<%@ OutputCache Duration="20" VaryByParam="ProductID;CurrencyType" %>
此时,会按照 ProductID 或者 CurrencyType 分别缓存独立版本的页面。
自定义缓存控制
ASP.NET 还允许你创建自己的过程来确定是保存一个新的页面版本还是使用现有的缓存。这样的代码检查任意合适的信息并返回一个字符串,ASP.NET 使用这个字符串实现缓存。
自定义缓存的一个应用是基于浏览器的类型缓存不同版本的页面。这样,使用了 FireFox 浏览器的用户将获得为 FireFox 优化的页面,IE 的用户也会获得为 IE 优化的 HTML。
为了建立这样的逻辑,首先要添加 OutputCache 指令,然后为 VaryByCustom 特性指定你创建的自定义缓存的类型的名称(你可以选择任何你喜欢的名字)。
<%@ OutputCache Duration="10" VaryByParam="none" VaryByCustom="browser" %>
接下来要创建用于产生自定义缓存字符串的过程。这个过程必须写在 global.asax 应用程序文件中:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "browser")
{
string browserName = context.Request.Browser.Browser;
browserName += context.Request.Browser.MajorVersion.ToString();
return browserName;
}
else
{
return base.GetVaryByCustomString(context, arg);
}
}
使用 HttpCachePolicy 类进行缓存
你的代码还可以使用内置的特殊属性 Response.Cache,它提供 System.Web.HttpCachePolicy 类的一个实例,这个对象提供的属性允许你打开当前页面的缓存,这允许你通过编程启用输出缓存。
protected void Page_Load(object sender, EventArgs e)
{
// Cache this page on the server
Response.Cache.SetCacheability(HttpCacheability.Public);
// Use the cached copy of this page for the next 60 seconds.
Response.Cache.SetExpires(DateTime.Now.AddSeconds(10));
// 某些浏览器在刷新页视图时会将 HTTP 缓存无效标头发送到 Web 服务器并从缓存中收回该页。
// 当 validUntilExpires 参数为 true 时,ASP.NET 会忽略缓存无效标头
// 而该页将保留在缓存中直到过期为止。
Response.Cache.SetValidUntilExpires(true);
lblDate.Text = "The time is now:<br />" + DateTime.Now.ToString();
}
从设计的角度来看,可编程的缓存不够清晰。把缓存代码直接加到页面中通常不太灵活,如果你还要包含其他初始化代码,它甚至会把事情搞得一团糟。因为 Page.Load 事件处理程序内的代码仅在页面不在缓存中时执行。
缓存后替换和部分页缓存
有时候你会发现不能缓存整个页面,但你还是很乐意缓存某些创建昂贵且不怎么变化的内容。有两个方法应对这一挑战:
- 部分页缓存:你需要找出缓存的内容,把它们封装到一个专用的用户控件内,然后缓存该控件的输出。
- 缓存后替换:你需要找出不想缓存的动态内容,然后使用某些 Substitution 控件的东西替代这部分内容。
这两个方式中,部分页缓存最容易实现。但究竟适用于哪种方法往往基于你要缓存的内容的大小。如果要缓存的内容小且单一,部分页缓存是最有效的办法。反之,缓存后替换是更简单的办法。这两个办法的性能相近。
1. 部分页缓存
要实现部分页缓存,需要为缓存的部分创建一个用户控件,然后在用户控件上加入 OutputCache 指令。从概念上讲,部分页缓存和页面缓存相同。但有一个缺点,如果页面获得的是用户控件的一个缓存版本,它就不能通过代码和控件交互。使用缓存版本的用户控件时,只是把一段 HTML 代码插入到页面上,相应的用户控件对象不可用。
2. 缓存后替换
缓存后替换功能涉及到 HttpResponse 类新增的一个方法:WriteSubstitution(),它接受一个参数,一个指向页面类中回调方法的委托。这个回调方法返回页面要替换的部分。
就本质而言,总体思想是你创建一个生成动态内容的方法,确保这个方法每次都会被调用且它的内容从来不会被缓存。
用于生产动态内容的方法必须是静态的。因为即使页面类的实例不可用(很显然,当页面内容从缓存提供时,页面对象不会被创建),ASP.NET 也要调用这个方法。这个方法的签名很简单,它接受一个代表当前请求的 HttpContext 对象,返回一个带有新 HTML 内容的字符串。
private static string GetDate(HttpContext context)
{
return "<b>" + DateTime.Now.ToString() + "</b>";
}
// 为了在页面中获得日期,需要在某个地方使用 Response.WriteSubstitution()方法
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("This date is cached with the page: ");
Response.Write(DateTime.Now.ToString() + "<br />");
Response.Write("This date is not: ");
Response.WriteSubstitution(new HttpResponseSubstitutionCallback(GetDate));
}
现在,即使页面被缓存了,第二个日期也会在每次请求后更新,因为回调绕过了缓存过程。
通常,设计 ASP.NET 页面时,你根本不会使用 Response 对象,而是可以使用 Web 控件,这些 Web 控件使用 Response 控件对象生成它们的内容。如果像前面的示例那样使用 Response 对象,它带来的一个问题是:你会失去在页面其他部分定位内容的能力。唯一可行的办法是把动态内容封装在某个控件内,这样,控件自身呈现时可以使用 Response.WriteSubstitution()方法,这个我们以后会详细阐述。
不过,如果不愿意只为了获得缓存后替换的功能而自定义控件的话,ASP.NET 还有一个快捷方式:一个通用的 Substitution 控件,它使用这项技术来使自己的全部内容是动态的。
Substitution 控件可以和其他 ASP.NET 控件放到一起,这样就可以精确控制动态内容出现的位置了。
重写上面这个示例:
This date is cached with the page:
<asp:Label ID="lblDate" runat="server" Text="Label"></asp:Label><br />
This date is not:
<asp:Substitution ID="Substitution" runat="server" MethodName="GetDate" />
protected void Page_Load(object sender, EventArgs e)
{
lblDate.Text = DateTime.Now.ToString();
}
private static string GetDate(HttpContext context)
{
return "<b>" + DateTime.Now.ToString() + "</b>";
}
缓存后替换只允许你执行静态方法!ASP.NET 还是跳过了页面的生命周期,它不会创建任何控件对象或产生任何控件事件。因为在回调中这些控件对象不可用,所有如果动态内容依赖于其他控件的值,你就需要使用其他技术(如 数据缓存)。
缓存用户配置
输出缓存的一个问题是你必须把缓存命令嵌入到页面内:OutputCache 指令或者代码中调用 Response.Cache 对象的部分方法。如果有数十个缓存页面,它们会存在严重的管理问题(例如把时间从 10秒 设置到 20秒)。
ASP.NET 允许你对一组页面应用相同的缓存设置,这个功能叫做缓存用户配置。它允许你在 web.config 中定于缓存配置,这些设置和一个名字关联,然后可以用这个名字对多个页面应用这些设置。
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ProductItemCacheProfile" duration="5"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
<%@ OutputCache CacheProfile="ProductItemCacheProfile" VaryByParam="None" %>
输出缓存扩展
ASP.NET 的缓存模型在各种 Web 应用程序里工作的都非常好。它使用简单、运行飞快,因为缓存服务运行在 ASP.NET 进程内并且把数据保存在物理内存中。
但是,如果想长时间缓存大量数据,ASP.NET 的缓存系统就不那么适合了。例如大型电子商务网站的产品目录,假设产品目录不会频繁变化,你可能希望缓存数以千计的产品页面从而节省创建它们的开销。但对于这样海量的数据,使用 Web 服务器的内存是很有风险的。
相反,你可能会选用其他类型的存储,它比内存慢但还是比重建页面要快(并且不太可能造成资源瓶颈)。这个存储可能基于磁盘、数据库、或者像 Windows Server AppFabric 这样的分布式存储系统。
1. 构建自定义的缓存提供程序
使用基于磁盘的文件系统缓存要比基于内存的缓存要慢,但使用它基于很重要的两点:
- 可持久化的缓存:由于输出缓存保存在磁盘上,即使Web程序域重新启动了,它还继续保持在那里。
- 低内存使用:当缓存的页面被重用时,它直接从硬盘提供,因此不需要把数据块读取到内存中。1)对于大型缓存文件非常有用。2)如果按查询字符串策略进行缓存输出且查询字符串的变化非常多时,它特别有用。这 2 种情况下,使用内存很难构建成功的缓存策略。
创建自定义缓存提供程序很简单。只要继承 System.Web.Caching 空间的 OutputCacheProvider 类,然后重写下列方法:
Initialize() | 当提供程序第一次加载时完成一些初始化的任务。(这是这个列表里唯一不一定需要重写的方法) |
Add() | 如果项不存在,则添加到缓存里。 |
Set() | 把项添加到缓存里,可以覆盖原始项。 |
Get() | 如果存在,从缓存里获取项。这个方法应该强制使用基于时间的过期策略,检查过期时间移除项。 |
Remove() | 把项从缓存中移除。 |
为了实现序列化,创建了一个叫做 CacheItem 的类,把初始要缓存的项和过期时间封装到了一起:
[Serializable]
public class CacheItem
{
public DateTime ExpiryDate; // 有效期
public object Item;
public CacheItem(object item, DateTime expiryDate)
{
this.ExpiryDate = expiryDate;
this.Item = item;
}
}
现在只要重写 Add()、Set()、Get()、Remove()方法即可。这些方法接收一个唯一标识缓存内容的键值。这个键基于缓存页面的文件名。
例如,对网站 CustomCacheProvider 的 OutputCaching.aspx 页面使用输出缓存时,代码接收到的键可能是:
a2/customcacheprovider/outputcaching.aspx
为了把它转换为有效的文件名,代码只要把其中的 "/" 转换为 "-" 即可。同时还增加了扩展名 .txt ,以区分真正的 ASP.NET 页面和缓存内容并方便在调试时打开和查看缓存文件的内容。转换后文件名的示例:
a2-customcacheprovider-outputcaching.aspx.txt
public class FileCacheProvider : System.Web.Caching.OutputCacheProvider
{
public FileCacheProvider()
{
}
public string CachePath { get; set; }
private string ConvertKeyToPath(string key)
{
string file = key.Replace('/', '-');
file += ".txt";
// 将两个字符串组合成一个路径。
return Path.Combine(CachePath, file);
}
// 检查内容是否已存在
// 同时还要返回缓存对象
public override object Add(string key, object entry, DateTime utcExpiry)
{
// Transform the key to a unique filename.
string path = ConvertKeyToPath(key);
if (!File.Exists(path))
{
Set(key, entry, utcExpiry);
}
return entry;
}
// 总是保存内容
// 勿忘序列化要引入下列命名空间
// System.Runtime.Serialization.Formatters.Binary;
public override void Set(string key, object entry, DateTime utcExpiry)
{
CacheItem item = new CacheItem(entry, utcExpiry);
string path = ConvertKeyToPath(key);
using (FileStream fs = File.OpenWrite(path))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(fs, entry);
}
}
// 检查项是否存在
// 检查项是否过期,如过期需要移除
public override object Get(string key)
{
string path = ConvertKeyToPath(key);
if (!File.Exists(path))
{
return null;
}
CacheItem item = null;
using (FileStream fs = File.OpenRead(path))
{
BinaryFormatter bf = new BinaryFormatter();
item = (CacheItem)bf.Deserialize(fs);
}
// 将当前 System.DateTime 对象的值转换为协调世界时 (UTC)。
if (item.ExpiryDate <= DateTime.Now.ToUniversalTime())
{
Remove(key);
return null;
}
return item;
}
// 总是删除
public override void Remove(string key)
{
string path = ConvertKeyToPath(key);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
2. 使用自定义缓存提供程序
在使用自定义缓存提供程序之前,需要把它添加到 <caching> 配置节。
<system.web>
<caching>
<outputCache defaultProvider="FileCache">
<providers>
<add name="FileCache" type="FileCacheProvider" cachePath="~/Cache"/>
</providers>
</outputCache>
......
</system.web>
如果该类是一个单独程序集的一部分(上例假设在 APP_CODE 目录里),则需要同时指定程序集的名称。例如,名为 CacheExtensibility 程序集中命名空间 CustomCaching 中的 FileCacheProvider 应该这样配置:
<add name="FileCache" type="CustomCaching.FileCacheProvider,CacheExtensibility" cachePath="~/Cache"/>
还有一个细节,这个节包括一个自定义特性 cachePath 。ASP.NET 忽略额外添加的这一部分,但是你的代码可以自由读取并使用它。例如,FileCacheProvider 可以使用 Initialize()方法读取这个信息并设置路径(对于这个例子,它应当是当前 Web 应用程序目录的 Cache 子目录)。
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// 初始化提供程序。
// name:
// 该提供程序的友好名称。
// config:
// 名称/值对的集合,表示在配置中为该提供程序指定的、提供程序特定的属性。
base.Initialize(name, config);
// Retrieve the web.config settings.
CachePath = HttpContext.Current.Server.MapPath(config["CachePath"]);
}
如果不使用 defaultProvider 特性,就会由 ASP.NET 来决定何时使用标准的内存内的缓存服务,以及何时使用自定义缓存提供程序。【你可能会希望在页面指令里处理这一问题,但不行,缓存是在页面读取之前进行的(如果成功的话,页面会完全忽略页面标记)】。
相反,需要重写 global.asax 文件的 GetOutputCacheProviderName()方法。这个方法检查当前请求并返回处理当前请求的缓存提供程序的名称。这个示例告诉 ASP.NET 对页面 OutputCaching.aspx 使用 FileCacheProvider (其他的不使用):
public override string GetOutputCacheProviderName(HttpContext context)
{
// 获取请求的虚拟路径
// 通过 Path.GetFileName() 获取文件名和扩展名
string pageAndQuery = System.IO.Path.GetFileName(context.Request.Path);
if (pageAndQuery.StartsWith("OutputCaching.aspx"))
{
return "FileCache";
}
else
{
return base.GetOutputCacheProviderName(context);
}
}