既然缓存中的数据其实是来自数据库的,那么缓存中的数据如何和数 据库进行同步呢?一般来说,缓存中应该存放改动不大或者对数据的实时性没有太多要求的数据。这样,我们只需要定期更新缓存就可以了。相反,如果缓存的更新 频率过快的话,使用缓存的意义就不是很大了,因此更新缓存的时候需要一次性从数据库中读取大量的数据,过于频繁地更新缓存反而加重了数据库的负担。
那么ASP.NET中的Cache又提供了哪些缓存的过期策略呢?
· 永不过期。和Application一样,缓存永不过期。
· 绝对时间过期。缓存在某一时间过期,比如5分钟后。
· 变化时间过期(平滑过期)。缓存在某一时间内未访问则超时过期,这个和Session有点类似,比如我们可以设定缓存5分钟没有人访问则过期。
· 依赖过期。缓存依赖于数据库中的数据或者文件中的内容。一旦数据库中某些表的数据发生变动或者文件内容发生变动,则缓存自动过期。
缓存过期后我们就要更新缓存了,ASP.NET提供了两种更新策略。
· 被动更新。缓存过期以后手动进行更新。
· 主动更新。缓存过期以后在回调方法中更新。
二、 Cache性能与过期策略
首先,在页面上添加两个按钮,并双击按钮实现Click事件处理方法。
Text="从缓存中读取数据" />
<asp:Button ID="btn_GetDataFromDb" runat="server" OnClick="btn_GetDataFromDb_Click"
Text="从数据库中读取数据" />
第一个按钮实现从缓存读取数据。
注意:本例需要using以下命名空间。
using System.Web.Caching; // 用于缓存的策略
using System.IO; // 用于文件操作
protected void btn_GetData_Click(object sender, EventArgs e)
{
InsertRecord();
Stopwatch sw=new Stopwatch();
sw.Start();
if (Cache["Data"]==null)
{
Response.Write("缓存无效<br/>");
}
else
{
DataSet ds = Cache["Data"] as DataSet;
Response.Write(string.Format("查询结果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗费时间:{0}<br/>", sw.ElapsedTicks));
}
}
· 一开始的InsertRecord()方法是我们自己创建的,用来向数据库插入一条记录。这样,我们就能看出来数据是否是从缓存中读取的了。
InsertRecord()方法如下:
{
using (SqlConnection conn = new SqlConnection(@"server=(local)\SQLEXPRESS;
database=Forum;Trusted_Connection=True"))
{
conn.Open();
using (SqlCommand cmd = new SqlCommand("Insert into CacheTest (Test) values
('Test')", conn))
{
cmd.ExecuteNonQuery();
}
}
}
· 如果缓存存在则输出查询结果和查询耗费的时间,如果缓存不存在则输出“缓存无效”。
· Stopwatch类用于精确测定逝去的时间,ElapsedTicks属性返回了间隔的计数器刻度,所谓计数器刻度就是系统的计数器走过了多少次。当 然,Stopwatch还有ElapsedMilliseconds能返回间隔的总毫秒数。之所以使用ElapsedTicks,因为它是一个更小的时间 单位。
第二个按钮直接从数据库读取数据。
{
InsertRecord();
Stopwatch sw = new Stopwatch();
sw.Start();
DataSet ds = GetData();
Response.Write(string.Format("查询结果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗费时间:{0}<br/>", sw.ElapsedTicks));
}
{
DataSet ds = new DataSet();
using (SqlConnection conn = new SqlConnection(@"server=(local)\SQLEXPRESS;
database=Forum;Trusted_Connection=True"))
{
SqlDataAdapter da = new SqlDataAdapter("select count(*) from CacheTest", conn);
da.Fill(ds);
}
return ds;
}
为了能体现出缓存的效率,我们在Forum数据库中又新建立了一个CacheTest数据表,表结构很简单,如图4-1所示。
图4-1 CacheTest表结构
我们在表中插入了10万条以上的记录,使得表的大小达到了100MB左右。
运行程序,单击“从数据库中读取数据”按钮,如图4-2所示,
图4-2 从数据库读取数据需要花费大量的时间
我们可以看到,这个操作耗费了相当多的时间。
因为我们直接从数据库读取count(*),所以每次单击按钮查询结果显示的数字都会+1。现在你单击“从缓存中读取数据”肯定是显示“缓存无效”,因为我们还没有添加任何缓存。
然后,我们在页面上添加三个按钮并双击按钮创建事件处理方法,三个按钮使用不同的过期策略添加缓存。
OnClick="btn_InsertNoExpirationCache_Click" />
<asp:Button ID="btn_InsertAbsoluteExpirationCache" runat="server" Text="插入绝对时间
过期缓存" OnClick="btn_InsertAbsoluteExpirationCache_Click" />
<asp:Button ID="btn_InsertSlidingExpirationCache" runat="server" Text="插入变化时间
过期缓存" OnClick="btn_InsertSlidingExpirationCache_Click" />
三个按钮的Click事件处理方法如下:
{
DataSet ds = GetData();
Cache.Insert("Data", ds);
}
protected void btn_InsertAbsoluteExpirationCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds,null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);
}
protected void btn_InsertSlidingExpirationCache_Click(object sender, EventArgs e)
{
DataSet ds = GetData();
Cache.Insert("Data", ds, null, DateTime.MaxValue, TimeSpan.FromSeconds(10));
}
· 永不过期。直接赋值缓存的Key和Value即可
· 绝对时间过期。DateTime.Now.AddSeconds(10)表示缓存在10秒后过期,TimeSpan.Zero表示不使用平滑过期策略。
· 变化时间过期(平滑过期)。DateTime.MaxValue表示不使用绝对时间过期策略,TimeSpan.FromSeconds(10)表示缓存连续10秒没有访问就过期。
在这里,我们都使用了Insert()方法来添加缓存。其实,Cache还有一个Add()方法也能向缓存中添加项。不同之处在于Add()方法只能添加缓存中没有的项,如果添加缓存中已有的项将失败(但不会抛出异常),而Insert()方法能覆盖原来的项。
注意:和Application不同,这里不需要使用在插入缓存的时候进行锁操作,Cache会自己处理 并发。
现在,我们就可以打开页面对这三种过期策略进行测试了。
1.单击“从缓存中读取数据”按钮,提示“缓存无效”。
2.单击“从数据库中读取数据”按钮,查询结果显示现在记录总数为100646。
3. 单击“插入永不过期缓存”按钮,然后连续单击“从缓存中读取数据”按钮,可以发现,无论过去多久,缓存始终没有过期,而且观察记录查询结果可以发现值始终 没有发生变化。不同的是,从缓存中读取数据的效率比从数据库中读取数据提高了几个数量级,如图4-3所示,你可以和图4-2进行比较。
图4-3 从缓存中读取数据所花费的时间
4.单击“插入绝对时间过期缓存”,然后连续单击“从缓存中读取数据”按钮,大约10秒过期后,页面提示“缓存无效”,说明缓存过期了。
5.单击“插入变化时间过期缓存”,然后连续单击“从缓存中读取数据”按钮,缓存始终不过期,如果我们等待10秒后再去单击按钮,页面提示“缓存无效”,说明缓存过期了。
我们再来看一下依赖过期策略。所谓依赖过期就是缓存的依赖项(比如一个文件)的内容改变之后缓存也就失效了。由于篇幅关系,这里只介绍文件依赖。我们在页面上再加两个按钮并双击按钮添加Click事件处理方法。
Click" />
<asp:Button ID="btn_AddFileDependencyCache" runat="server" Text="插入文件依赖缓存"
OnClick="btn_AddFileDependencyCache_Click" />
在本例中,我们将使缓存依赖一个txt文本文件。因此,首先在项目中添加一个test.txt文本文件。单击“修改文件”按钮实现文件的修改。
{
FileStream fs = new FileStream(Server.MapPath("test.txt"), FileMode.Append,
FileAccess.Write);
StreamWriter sw = new StreamWriter(fs);
sw.WriteLine(DateTime.Now.ToString());
sw.Close();
fs.Close();
}
我们通过在文件的最后写入当前的时间来修改文件。插入文件依赖缓存按钮的事件处理方法如下:
{
CacheDependency cd = new CacheDependency(Server.MapPath("test.txt"));
DataSet ds = GetData();
Cache.Insert("Data", ds, cd);
}
现在就可以打开页面进行测试了。
1.单击“从缓存中读取数据”按钮,提示“缓存无效”。
2.单击“从数据库中读取数据”按钮,查询结果显示现在记录总数为100710。
3.单击“插入文件依赖缓存”按钮,然后连续单击“从缓存中读取数据”按钮,可以发现,无论过去多久,缓存始终没有过期,而且观察记录查询结果可以发现值始终没有发生变化。
4.单击“修改文件”按钮,然后单击“从缓存中读取数据”按钮,提示“缓存无效”。由于文件已经修改了,依赖这个文件的缓存立刻失效了。
三、 Cache的更新策略
最 后,我们来讨论缓存的更新策略。在Web程序中我们通常会使用被动更新。所谓被动更新,就是在调用数据的时候判断缓存是否为空,如果为空则先更新缓存然后 再从缓存中读取数据,如果不为空则直接从缓存中读取数据。可以把“从缓存中读取数据”按钮的Click事件处理方法修改成如下,实现被动更新。
{
InsertRecord();
DataSet ds = new DataSet();
Stopwatch sw = new Stopwatch();
sw.Start();
if (Cache["Data"] == null)
{
ds = GetData();
Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero);
}
else
{
ds = Cache["Data"] as DataSet;
}
Response.Write(string.Format("查询结果:{0}<br/>", ds.Tables[0].Rows[0][0]));
Response.Write(string.Format("耗费时间:{0}<br/>", sw.ElapsedTicks));
}
我 们可以看出,如果没有人访问数据缓存是不会更新的,只有缓存被访问的时候发现缓存无效才会去更新。这样很明显的一个缺点就是,如果缓存过期了更新操作将花 费很长时间,这个时候的查询也需要花费很多时间。我们可以利用缓存的回调功能让缓存过期后自动续建实现自动更新的目的。
{
DataSet ds = GetData();
Cache.Insert("Data", ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero,
CacheItemPriority.Default, CacheRemovedCallback);
}
最后一个参数表明缓存被移除以后自动调用CacheRemovedCallback()方法,方法实现如下。
removedReason)
{
DataSet ds = GetData();
Cache.Insert(key, ds, null, DateTime.Now.AddSeconds(10), TimeSpan.Zero, CacheItemPriority.
Default, CacheRemovedCallback);
}
在回调方法中,我们再次插入一个支持回调的缓存。这样,缓存被移除以后又能自动更新了。说了这么多创建缓存的方法,读者可能会问怎么手动移除缓存呢?比如我们要移除Key="Data"的缓存只需要:
Cache.Remove("Data");
你可能会马上想到用Cache.RemoveAll()方法移除所有缓存,可是Cache没有提供这样的方法,我们只能通过遍历来实现移除所有缓存。
while (CacheEnum.MoveNext())
{
Cache.Remove(CacheEnum.Key.ToString());
}
四、 Cache总结
同样,我们以第一节中的几个问题结束对Cache的讨论。
· 存储的物理位置。服务器内存。
· 存储的类型限制。任意类型。
· 状态使用的范围。当前请求上下文,所有用户共用一份。
· 存储的大小限制。任意大小。
· 生命周期。有多种过期策略控制缓存的销毁。
· 安全与性能。数据总是存储在服务端,安全性比较高,但不易存储过多数据。
· 优缺点与注意事项。检索数据速度快,过期策略丰富。注意别把对实时性要求很高的数据放到Cache中,不断更新Cache会对数据库造成压力。
-------------------------------------------------------------------------------------------------------------------------------------------------
要实现页面输出缓存,只要将一条 OutputCache 指令添加到页面即可。
<%@ OutputCache Duration="60" VaryByParam="*" %>
如同其他页面指令一样,该指令应该出现在 ASPX 页面的顶部,即在任何输出之前。它支持五个属性(或参数),其中两个是必需的。
Duration
必需属性。页面应该被缓存的时间,以秒为单位。必须是正整数。
Location
指定应该对输出进行缓存的位置。如果要指定该参数,则必须是下列选项之一:Any、Client、Downstream、None、Server 或 ServerAndClient。
VaryByParam
必需属性。Request 中变量的名称,这些变量名应该产生单独的缓存条目。"none" 表示没有变动。"*" 可用于为每个不同的变量数组创建新的缓存条目。变量之间用 ";" 进行分隔。
VaryByHeader
基于指定的标头中的变动改变缓存条目。
VaryByCustom
允许在 global.asax 中指定自定义变动(例如,"Browser")。
利用必需的 Duration 和 VaryByParam 选项的组合可以处理大多数情况。例如,如果产品目录允许用户基于 categoryID 和页变量查看目录页,可以用参数值为 "categoryID;page" 的 VaryByParam 将产品目录缓存一段时间(如果产品不是随时都在改变,一小时还是可以接受的,因此,持续时间是 3600 秒)。这将为每个种类的每个目录页创建单独的缓存条目。每个条目从其第一个请求算起将维持一个小时。
VaryByHeader 和 VaryByCustom 主要用于根据访问页面的客户端对页面的外观或内容进行自定义。同一个 URL 可能需要同时为浏览器和移动电话客户端呈现输出,因此,需要针对不同的客户端缓存不同的内容版本。或者,页面有可能已经针对 IE 进行了优化,但需要能针对 Netscape 或 Opera 完全降低优化(而不仅仅是破坏页面)。后一个例子非常普遍,将提供一个说明如何实现此目标的示例:
示例:VaryByCustom 用于支持浏览器自定义
为了使每个浏览器都具有单独的缓存条目,VaryByCustom 的值可以设置为 "browser"。此功能已经内置在缓存模块中,并且将针对每个浏览器名称和主要版本插入单独的页面缓存版本。
<%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="browser" %>
片段缓存,用户控件输出缓存
缓存整个页面通常并不可行,因为页面的某些部分是针对用户定制的。不过,页面的其他部分是整个应用程序共有的。这些部分最适合使用片段缓存和用 户控件进行缓存。菜单和其他布局元素,尤其是那些从数据源动态生成的元素,也应该用这种方法进行缓存。如果需要,可以将缓存的控件配置为基于对其控件(或 其他属性)的更改或由页面级输出缓存支持的任何其他变动进行改变。使用同一组控件的几百个页面还可以共享那些控件的缓存条目,而不是为每个页面保留单独的 缓存版本。
实现
片段缓存使用的语法与页面级输出缓存一样,但其应用于用户控件(.ascx 文件)而不是 Web 窗体(.aspx 文件)。除了 Location 属性,对于 OutputCache 在 Web 窗体上支持的所有属性,用户控件也同样支持。用户控件还支持名为 VaryByControl 的 OutputCache 属性,该属性将根据用户控件(通常是页面上的控件,例如,DropDownList)的成员的值改变该控件的缓存。如果指定了 VaryByControl,可以省略 VaryByParam。最后,在默认情况下,对每个页面上的每个用户控件都单独进行缓存。不过,如果一个用户控件不随应用程序中的页面改变,并且在所有 页面都使用相同的名称,则可以应用 Shared="true" 参数,该参数将使用户控件的缓存版本供所有引用该控件的页面使用。
示例
<%@ OutputCache Duration="60" VaryByParam="*" %>
该示例将缓存用户控件 60 秒,并且将针对查询字符串的每个变动、针对此控件所在的每个页面创建单独的缓存条目。
<%@ OutputCache Duration="60" VaryByParam="none" VaryByControl="CategoryDropDownList" %>
该示例将缓存用户控件 60 秒,并且将针对 CategoryDropDownList 控件的每个不同的值、针对此控件所在的每个页面创建单独的缓存条目。
<%@ OutputCache Duration="60" VaryByParam="none" VaryByCustom="browser" Shared="true %>
最后,该示例将缓存用户控件 60 秒,并且将针对每个浏览器名称和主要版本创建一个缓存条目。然后,每个浏览器的缓存条目将由引用此用户控件的所有页面共享(只要所有页面都用相同的 ID 引用该控件即可)。
页面级和用户控件级输出缓存的确是一种可以迅速而简便地提高站点性能的方法,但是在 ASP.NET 中,缓存的真正灵活性和强大功能是通过 Cache 对象提供的。使用 Cache 对象,您可以存储任何可序列化的数据对象,基于一个或多个依赖项的组合来控制缓存条目到期的方式。这些依赖项可以包括自从项被缓存后经过的时间、自从项上 次被访问后经过的时间、对文件和/或文件夹的更改以及对其他缓存项的更改,在略作处理后还可以包括对数据库中特定表的更改。
在 Cache 中存储数据
在 Cache 中存储数据的最简单的方法就是使用一个键为其赋值,就像 HashTable 或 Dictionary 对象一样:
Cache["key"] = "value";
这种做法将在缓存中存储项,同时不带任何依赖项,因此它不会到期,除非缓存引擎为了给其他缓存数据提供空间而将其删除。要包括特定的缓存依赖 项,可使用 Add() 或 Insert() 方法。其中每个方法都有几个重载。Add() 和 Insert() 之间的唯一区别是,Add() 返回对已缓存对象的引用,而 Insert() 没有返回值(在 C# 中为空,在 VB 中为 Sub)。
示例
Cache.Insert("key", myXMLFileData, new System.Web.Caching.CacheDependency(Server.MapPath("users.xml")));
该示例可将文件中的 xml 数据插入缓存,无需在以后请求时从文件读取。 CacheDependency 的作用是确保缓存在文件更改后立即到期,以便可以从文件中提取最新数据,重新进行缓存。如果缓存的数据来自若干个文件,还可以指定一个文件名的数组。
Cache.Insert("dependentkey", myDependentData, new System.Web.Caching.CacheDependency(new string[] {}, new string[]
{"key"}));
该示例可插入键值为 "key" 的第二个数据块(取决于是否存在第一个数据块)。如果缓存中不存在名为 "key" 的键,或者如果与该键相关联的项已到期或被更新,则 "dependentkey" 的缓存条目将到期。
Cache.Insert("key", myTimeSensitiveData, null, DateTime.Now.AddMinutes(1), TimeSpan.Zero);
绝对到期:此示例将对受时间影响的数据缓存一分钟,一分钟过后,缓存将到期。注意,绝对到期和滑动到期(见下文)不能一起使用。
Cache.Insert("key", myFrequentlyAccessedData, null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.FromMinutes(1));
滑动到期:此示例将缓存一些频繁使用的数据。数据将在缓存中一直保留下去,除非数据未被引用的时间达到了一分钟。注意,滑动到期和绝对到期不能一起使用。
更多选项
除了上面提到的依赖项,我们还可以指定项的优先级(依次为 low、high、NotRemovable,它们是在 System.Web.Caching.CacheItemPriority 枚举中定义的)以及当缓存中的项到期时调用的 CacheItemRemovedCallback 函数。大多数时候,默认的优先级已经足够了 — 缓存引擎可以正常完成任务并处理缓存的内存管理。CacheItemRemovedCallback 选项考虑到一些很有趣的可能性,但实际上它很少使用。不过,为了说明该方法,我将提供它的一个使用示例:
CacheItemRemovedCallback 示例
System.Web.Caching.CacheItemRemovedCallback callback = new System.Web.Caching.CacheItemRemovedCallback (OnRemove);
Cache.Insert("key",myFile,null,
System.Web.Caching.Cache.NoAbsoluteExpiration,
TimeSpan.Zero,
System.Web.Caching.CacheItemPriority.Default, callback);
. . .
public static void OnRemove(string key,
object cacheItem,
System.Web.Caching.CacheItemRemovedReason reason)
{
AppendLog("The cached value with key " + key +
" was removed from the cache. Reason: " +
reason.ToString());
}
该示例将使用 AppendLog() 方法(这里不讨论该方法,请参阅 Writing Entries to Event Logs)中定义的任何逻辑来记录缓存中的数据到期的原因。通过在从缓存中删除项时记录这些项并记录删除的原因,您可以确定是否在有效地使用缓存或者您是 否可能需要增加服务器上的内存。注意,callback 是一个静态(在 VB 中为 Shared)方法,建议使用该方法的原因是,如果不使用它,保存回调函数的类的实例将保留在内存中,以支持回调(对 static/Shared 方法则没有必要)。
该特性有一个潜在的用处 — 在后台刷新缓存的数据,这样用户永远都不必等待数据被填充,但数据始终保持相对较新的状态。但实际上,此特性并不适用于当前版本的缓存 API,因为在从缓存中删除缓存的项之前,不触发或不完成回调。因此,用户将频繁地发出尝试访问缓存值的请求,然后发现缓存值为空,不得不等待缓存值的重 新填充。我希望在未来的 ASP.NET 版本中看到一个附加的回调,可以称为 CachedItemExpiredButNotRemovedCallback,如果定义了该回调,则必须在删除缓存项之前完成执行。
缓存数据引用模式
每当我们尝试访问缓存中的数据时,都应该考虑到一种情况,那就是数据可能已经不在缓存中了。因此,下面的模式应该普遍适用于您对缓存的数据的访问。在这种情况下,我们假定已缓存的数据是一个数据表。
public DataTable GetCustomers(bool BypassCache)
{
string cacheKey = "CustomersDataTable";
object cacheItem = Cache[cacheKey] as DataTable;
if((BypassCache) (cacheItem == null))
{
cacheItem = GetCustomersFromDataSource();
Cache.Insert(cacheKey, cacheItem, null,
DateTime.Now.AddSeconds(GetCacheSecondsFromConfig(cacheKey),
TimeSpan.Zero);
}
return (DataTable)cacheItem;
}
关于此模式,有以下几点需要注意:
某些值(例如,cacheKey、cacheItem 和缓存持续时间)是一次定义的,并且只定义一次。
可以根据需要跳过缓存 — 例如,当注册一个新客户并重定向到客户列表后,最好的做法可能就是跳过缓存,用最新数据重新填充缓存,该数据包括新插入的客户。
缓存只能访问一次。这种做法可以提高性能,并确保不会发生 NullReferenceExceptions,因为该项在第一次被检查时是存在的,但第二次检查之前就已经到期了。
该模式使用强类型检查。C# 中的 "as" 运算符尝试将对象转换为类型,如果失败或该对象为空,则只返回 null(空)。
持续时间存储在配置文件中。在理想的情况下,所有的缓存依赖项(无论是基于文件的,或是基于时间的,还是其他类型的依赖项)都应该存储在配置文件中,这样 就可以进行更改并轻松地测量性能。我还建议您指定默认缓存持续时间,而且,如果没有为所使用的 cacheKey 指定持续时间,就让 GetCacheSecondsFromConfig() 方法使用该默认持续时间。
相关的代码示例是一个 helper 类,它将处理上述所有情况,但允许通过一行或两行代码访问缓存的数据。请下载 CacheDemos.msi。
小结
缓存可以使应用程序的性能得到很大的提高,因此在设计应用程序以及对应用程序进行性能测试时应该 予以考虑。应用程序总会或多或少地受益于缓存,当然有些应用程序比其他应用程序更适合使用缓存。对 ASP.NET 提供的缓存选项的深刻理解是任何 ASP.NET 开发人员应该掌握的重要技巧。
尽早缓存;经常缓存
您应该在应用程序的每一层都实现缓存。向数据层、业务逻辑层、UI 或输出层添加缓存支持。内存现在非常便宜 — 因此,通过以智能的方式在整个应用程序中实现缓存,可以获得很大的性能提高。
缓存可以掩盖许多过失
缓存是一种无需大量时间和分析就可以获得"足够良好的"性能的方法。这里再次强调,内存现在非常便宜,因此,如果您能通过将输出缓存 30 秒,而不是花上一整天甚至一周的时间尝试优化代码或数据库就 可以获得所需的性能,您肯定会选择缓存解决方案(假设可以接受 30 秒的旧数据)。缓存正是那些利用 20% 付出获得 80% 回报的特性之一,因此,要提高性能,应该首先想到缓存。不过,如果设计很糟糕,最终却有可能带来不良的后果,因此,您当然也应该尽量正确地设计应用程序。 但如果您只是需要立即获得足够高的性能,缓存就是您的最佳选择,您可以在以后有时间的时候再尽快重新设计应用程序。
页面级输出缓存
作为最简单的缓存形式,输出缓存只是在内存中保留为响应请求而发送的 HTML 的副本。其后再有请求时将提供缓存的输出,直到缓存到期,这样,性能有可能得到很大的提高(取决于需要多少开销来创建原始页面输出 - 发送缓存的输出总是很快,并且比较稳定)。
http://hi.baidu.com/tiancaixgd/blog/item/261ec9fddd82544fd7887d38.html