ASP.NET 缓存:技术和最佳做法

 

史蒂文·史密斯
ASPAlliance.com

2003 年 8 月

适用于:
    Microsoft® ASP.NET

摘要: ASP.NET 提供了三种主要形式的缓存:页面级输出缓存、用户控制级别输出缓存 (或片段缓存) ,以及缓存 API。 输出缓存和片段缓存的优点是难以置信地易于实现,并且在许多情况下已足够。 缓存 API (提供了额外的灵活性,实际上) ,并且可用于在整个应用程序的每一层中利用缓存。

下载 CacheDemos.msi

目录

Steve 的缓存技巧
页面级别输出缓存
片段缓存,用户控制输出缓存
缓存 API,使用缓存对象
总结

在 ASP.NET 中提供的众多功能中,缓存支持远非我的最爱,这是有充分理由的。 与 ASP.NET 中的所有其他功能相比,它对应用程序的性能具有最大的潜在影响,并且它允许 ASP.NET 开发人员接受使用相当繁重的控件(如 DataGrids)构建站点的额外开销,而不必担心性能会受到太大影响。 为了从应用程序中的缓存中获得最大好处,应考虑在程序的所有级别实现缓存的方法。

Steve 的缓存技巧

提前缓存;经常缓存

在应用程序的每一层实现缓存。 向数据层、业务逻辑层和 UI 或输出层添加缓存支持。 内存成本较低, 通过在整个应用程序中以智能方式实现缓存,可以大幅提高性能。

缓存隐藏许多罪恶

缓存是获得“足够好”性能的好方法,无需花费大量时间和分析。 同样,内存成本较低,因此,如果可以通过缓存输出 30 秒(而不是花费一天或一周时间尝试优化代码或数据库)来获得所需的性能,请执行缓存解决方案 (假设 30 秒旧数据可以) 继续操作。 缓存是其中 20% 的工作提供 80% 的好处的其中一项,因此它应该是你尝试提高性能的首要任务之一。 最终,糟糕的设计可能会赶上你,所以你当然应该尝试正确设计应用程序。 但是,如果你现在只需要获得足够好的性能,缓存可能是一个绝佳的选择,这可以让你有时间在以后有时间重构应用程序。

页面级别输出缓存

最简单的缓存形式,输出缓存只是保留为响应内存中的请求而发送的 HTML 副本。 随后会向缓存输出发送后续请求,直到缓存过期, (可能会产生非常大的性能提升,具体取决于创建原始页面输出所需的工作量 - 发送缓存输出始终非常快速且相当稳定) 。

实现

若要实现页面输出缓存,只需将 OutputCache 指令添加到页面即可。

  
    <%@ OutputCache Duration="60" VaryByParam="*" %>

  

与其他页指令一样,此指令应出现在 ASPX 页面顶部,并显示在任何输出之前。 它支持五个属性 (或参数) ,其中两个是必需的。

持续时间 必需。 应缓存页面的时间(以秒为单位)。 必须是正整数。
位置 指定输出应缓存的位置。 如果指定,必须为以下项之一:Any、Client、Downstream、None、Server 或 ServerAndClient。
VaryByParam 必需。 请求中变量的名称,这应该会导致单独的缓存条目。 “none”可用于指定无变体。 “*”可用于为每个不同的变量集创建新的缓存条目。 使用“;”分隔变量。
VaryByHeader 根据指定标头中的变体改变缓存条目。
VaryByCustom 允许在 global.asax (中指定自定义变体,例如,“Browser”) 。

大多数情况都可以使用所需的 Duration 和 VaryByParam 选项的组合来处理。 例如,如果你有一个允许用户根据 categoryID 和页面变量查看目录页面的产品目录,则可以将其缓存一段时间, (一小时可能是可以接受的,除非产品一直更改,因此持续时间为 3600 秒,) VaryByParam 为“categoryID;page”。 这将为每个类别的目录的每一页创建单独的缓存条目。 每个条目将从其第一个请求开始保留一小时。

VaryByHeader 和 VaryByCustom 主要用于允许基于访问页面的客户端自定义页面的外观或内容。 也许同一 URL 会同时为浏览器和移动电话客户端呈现输出,并且需要基于此缓存单独的版本。 或者,页面已针对 IE 进行优化,但需要能够针对 Netscape 或 Opera (正常降级,而不只是中断) 。 最后一个示例很常见,我们将展示如何执行此操作的示例:

示例:用于支持浏览器自定义的 VaryByCustom

若要为每个浏览器启用单独的缓存条目,可以将 VaryByCustom 设置为值“browser”。 此功能内置于缓存模块中,并将为每个浏览器名称和主版本插入页面的单独缓存版本。

  
    <%@ OutputCache Duration="60" VaryByParam="None" VaryByCustom="browser"  %>

  

片段缓存,用户控制输出缓存

通常,缓存整个页面是不可行的,因为页面的某些部分是为用户自定义的。 但是,页面的其他部分可能是整个应用程序共有的。 这些是使用片段缓存和用户控件进行缓存的完美候选项。 应使用此技术缓存菜单和其他布局元素,尤其是从数据源动态生成的布局元素。 如果需要,可以将缓存控件配置为根据对其控件 (或其他属性) 或页面级别输出缓存支持的任何其他变体的更改而有所不同。 使用相同控件的数百个页面也可以共享这些控件的缓存条目,而不是为每个页面保留单独的缓存版本。

实现

片段缓存使用与页面级输出缓存相同的语法,但应用于用户控件 (.ascx 文件) ,而不是应用于 web 窗体 (.aspx 文件) 。 除 Location 属性外,用户控件还支持 Web 窗体上 OutputCache 指令支持的所有属性。 用户控件还支持名为 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 的控件) 。

缓存 API,使用缓存对象

页面和用户控制级别输出缓存是提高站点性能的一种快速而简单的方法,但在 ASP.NET 中缓存的真正灵活性和功能是通过 Cache 对象公开的。 使用 Cache 对象,可以存储任何可序列化的数据对象,并根据一个或多个依赖项的组合控制该缓存条目的过期方式。 这些依赖项可以包括自缓存项以来经过的时间、自上次访问该项以来经过的时间、对文件和/或文件夹的更改、对其他缓存项的更改,或者 (对数据库中特定表进行一些工作) 更改。

在缓存中存储数据

在缓存中存储数据的最简单方法是使用键分配数据,就像 HashTable 或 Dictionary 对象一样:

Cache[“key”] = “value”;

这会将项存储在缓存中,没有任何依赖项,因此它不会过期,除非缓存引擎将其删除,以便为其他缓存数据腾出空间。 若要包含特定的缓存依赖项,请使用 Add () 或 Insert () 方法。 其中每个重载都有多个重载。 Add () 和 Insert () 之间的唯一区别是,Add () 返回对缓存对象的引用,而 C# 中没有返回值 (void、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));

滑动过期:此示例将缓存一些常用数据。 数据将保留在缓存中,直到一分钟过后,没有任何内容引用它。 请注意,滑动过期和绝对过期不能一起使用。

更多选项

除了上述依赖项之外,还可以指定项的优先级 (从低到高到不可移动(在 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()定义的任何逻辑来记录缓存中的数据过期的原因, (此处未包含,请参阅 将条目写入事件日志。 记录从缓存中删除的项并记下删除的原因,可以确定是否有效使用缓存,或者是否需要增加服务器上的内存。 请注意,回调是 VB) 方法中共享的静态 (,建议这样做,因为否则,持有回调函数的类实例将保留在内存中,以支持回调 (而静态/共享方法) 不需要该回调。

此功能的一个潜在用途是在后台刷新缓存的数据,以便用户无需等待数据填充,但数据保持相对最新。 遗憾的是,实际上,这不适用于当前版本的缓存 API,因为在从缓存中删除缓存项之前,回调不会触发或完成执行。 因此,用户会经常发出请求,尝试访问缓存的值,发现该值为 null,并被迫等待它重新填充。 在 ASP.NET 的未来版本中,我希望看到一个额外的回调,该回调可能称为 CachedItemExpiredButNotRemovedCallback,如果已定义,则必须在删除缓存项之前完成执行。

缓存数据引用模式

每当尝试从缓存访问数据时,都应假设数据可能不再存在。 因此,以下模式应普遍应用于缓存数据的访问。 在本例中,我们将假定已缓存的对象是 DataTable。

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,则只返回 null。
  • 持续时间存储在配置文件中。 理想情况下,所有缓存依赖项(无论是基于文件、基于时间还是其他)都应存储在配置文件中,以便可以轻松进行更改并测量性能。 我还建议指定默认缓存持续时间,如果未为正在使用的 cacheKey 指定持续时间,则 GetCacheSecondsFromConfig () 方法使用默认值。

关联的代码示例是一个帮助程序类,它将处理上述所有内容,但允许使用一行或两行代码访问缓存的数据。 下载 CacheDemos.msi

总结

缓存可以为应用程序提供巨大的性能优势,因此在设计应用程序以及测试性能时应考虑缓存。 我尚未遇到无法从某些容量的缓存中受益的应用程序,当然某些应用程序比其他应用程序更合适。 深入了解 ASP.NET 中可用的缓存选项是任何 ASP.NET 开发人员掌握的重要技能。

Microsoft ASP.NET MVP 史蒂文·史密斯是 ASPAlliance.com 的总裁和所有者。 他还是 ASPSmith Ltd 的所有者和主教练。以 NET 为中心的培训公司。 他创作了两本书, ASP.NET 开发人员的食谱ASP.NET 示例,以及 MSDN® 和 AspNetPRO 杂志上的文章。 Steve 每年在几次会议中发言,是 INETA 发言人局的成员。 Steve 拥有工商管理硕士学位和计算机科学工程理学学士学位。 可通过 联系 ssmith@aspalliance.comSteve。

终极缓存:输出和片段选项

ASP.NET 应用程序缓存查看器

有效的缓存过期时间

ASP.NET 应用程序缓存和SQL Server;使缓存项失效

@OutputCache

ASP.NET 中的输出缓存

© Microsoft Corporation. 保留所有权利。