[转] ASP.NET 缓冲: 技术及最佳实践

ASP.NET 提供了三种主要形式的缓冲: 页面级输出缓冲, 用户控件及输出缓冲 (或片断型缓冲), 以及 Cache 编程接口. 输出缓冲和片断缓冲有着非常易于实现的优点, 并且在许多情况下已经足够好用. Cache 编程接口提供了额外的灵活性, 可以用来使得应用程序的每一层都能充分利用到缓冲技术.

       在ASP.NET 的众多特性中, 缓冲支持无疑是我最喜欢的, 和所有其它ASP.NET的特性比起来, 它对应用程序的性能有着最大的潜在影响, 而且它使得开发者愿意接受使用相对重量级的控件如 DataGrid 来建站所带来的额外开销, 却不用害怕性能会爱到太大的影响. 为了在你的应用程序中看到缓冲所带来的最多的好处, 你应该考虑在程序的所有层中实现缓冲的方法.

Steve 的缓冲技巧

Cache Early,  Cache Often

 
      在你应用程序的每一层实现缓冲. 给数据层, 业务逻辑层, 以及界面或输出层添加缓冲支持. 在整个应用程序中以聪明的方式来实现缓冲, 你可以成功地获得巨大的性能提升.

Caching Hides Many Sins
 
       缓冲可以是一个很好的办法来取得足够好的性能而不必要花费很多的时间或分析. 如果你能通过缓存30秒输出来得到你想需要的性能, 而不是花费一天或一周的时间来试图优化你的代码或数据库, 那么使用缓冲吧 (假设 30 秒前的旧有数据没有问题). 缓冲是那些能够用20%的工作换取80%的收益的技术之一, 因此它应该是你试图提升性能的首选. 最后, 糟糕的设计很可能困扰着你, 所以你当然应该试图正确地设计你的应用. 但如果你仅需要现在获得足够好的性能, 缓冲可能是非常棒的, 它为你争取了时间, 使得你可以在晚些时候重构你的应用.

页面级输出缓冲
 
        最简单形式的缓冲, 输出缓冲简单地在内存中维持一份HTML的拷贝, 它是发送过的对一个请求的应答. 后续的请求将会被发送这个缓存的
输出直到缓存到期, 结果可以获取非常大的潜在性能提升 (取决于创建原始的页面输出需要多大的开销--发送缓冲的输出总是很快的而且相对
稳定).

实现

 要实现页面输出缓冲, 只要简单地在页面中添加 OutputCache 指示符.

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

       这个指示符和其它的指示符一起应该被放在ASPX页面的顶部, 在任何输出之前. 它支持5个属性(或参数), 其中2个是必须的.

Duration:   必须. 以秒表示的时间, 页面应该被缓冲. 必须是一个正数.
Location:   指定输出应该在哪里被缓冲. 如果指定该属性, 它的值必须是 Any, Client, Downstream, None, Server 或  ServerAndClient 之一.
VaryByParam: 必须. 请求中变量的名字, 这个会导致不同的缓冲入口, 可以用 "none" 表示没有区别, "*" 用来为每一套不同的 变量创建新的缓冲入口. 用 ";" 来划分变量.
VaryByHeader: 基于指定的头字段的变化来改变缓冲入口.
VaryByCustom: 允许在 global.asax 中指定自定义的变化.如 "Browser".
 
       大多数的情形可以由必须的 Duration 及 VaryByParam 来处理. 比方说, 如果你有一个产品目录, 它允许用户基于 CategoryID 和一个页面变量来查看相应目录的页面, 你可以缓冲它一段时间 (一个小时应该是可以接受的, 除非这些产品时刻在变化, 因此一个3600秒的 duration) 用一个值为 "categoryID; page" 的 VaryByParam 属性. 这会为每个目录的目录页面创建不同的缓冲入口. 每一个入口会从它的第一次请求开始持续一个小时.

         VaryByHeader 和 VaryByCustom 主要被用来根据访问用户的不同允许页面外观或内容的个性化. 也许相同的页面要在浏览器和手机上呈现, 需要缓冲不同的版本. 或者页面对IE进行了优化但需要能够在Netscape或 Opera 上稍微地降低要求 (而不是崩溃).  最后一个示例我们将展现一个怎样做的例子.

例子: VaryByCustom 支持浏览器个性化
        要能够为每一个浏览器区分缓冲入口, VaryByCustom 可被设为一个 "browser" 值. 这个功能内建在缓冲模块里, 会为每一个浏览器名及主版本号插入不同缓冲版本的页面.

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

片断缓冲, 用户控件输出缓冲
 
       在很多情况下, 缓冲整个页面并不可行, 因为页面的特定部分是为不同用户定制的. 但是, 可能页面的其它部分对于整个应用程序来说是通用的. 这些最适合被缓冲, 使用片断缓冲和用户控件缓冲. 菜单和其它而已元素, 尤其是从一个数据源动态产生的, 应该用这种技术来缓冲.  如果需要, 缓冲的控件可被配置为基于子控件 (或其它属性) 或任何其它页面级输出缓冲支持的变更而不同. 许许多多使用相同控件的页面
也可以共享这些控件的缓冲入口, 而不是为每一个页面分别保存一个缓冲的版本.

实现

        片断缓冲使用和页面级输出缓冲一样的语法, 但是应用在一个用户控件上 (.ascx文件) 而不是一个web窗体 (.aspx文件). 除了 Location属性外, 所有web窗体支持的 OutputCache 指示符属性同样被用户控件支持. 用户控件来支持一个 OutputCache 属性称为 VaryByControl, 它会根据该控件成员的值来变更缓冲 (典型情况是一个页面上的控件, 如 DropDownList),  如果指定了 VaryByControl ,  VaryByParam 将被忽略. 默认情况下, 每一个页面上的用户控件会被分别缓冲, 但是, 如果用户控件在应和程序中不同的页面间没有变化而且在所有这些页面中使用同样的命名, 可以在其上应用 Shared="true" 参数, 这会使该用户控件的缓冲版本被所有引用该控件的页面使用.

例子:

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

      这会将用户控件缓冲60秒, 并且为每一个不同查询字符串 query string , 每一个引用该控件的页面, 分别创建缓冲入口.
 
 <%@ OutputCache Duration="60" VaryByParam="none" VaryByControl="CategoryDropDownList" %>

     这将会缓冲这个用户控件60秒, 并且为 CategoryDropDownList 控件的每一个不同值, 每一个引用该控件的页面, 分别创建一个缓冲入口,

 <%@ OutputCache Duration="60" VaryByParam="none" VaryByCustom="browser" Shared="true" %>

      最后, 这个示例缓冲该用户控件60秒, 并且为每一个不同的浏览器名和主版本号创建一个缓冲入口. 每个浏览器的缓冲入口会被所有引用该控件的页面共享 ( 只要所有页面用同样的 ID 来引用它).

缓冲编程接口中, 使用缓冲对象

       页面和用户控件级输出缓冲可能是一个快速和简便的方法来提升你的站点性能, 但是缓冲的真正的灵活性和强大的功能是通过 Cache对象展示的. 使用 Cache 对象, 你可以存储任何可序列化的数据对象, 并且控制缓冲入口如何基于一个或多个依赖项的组合来到期 (expire).  这些依赖项包括从数据项缓冲以来经过的时间, 从上次访问以来经过的时间, 对文件或文件夹的更改, 对其它缓冲数据项的更改, 或对数据库
中特定表的更改.

在 Cache 中存储数据

     在 Cache 中存储数据的最简单的方法就是直接分配, 使用一个 key, 就象哈希表或字典对象一样:
 
 Cache["key"] = "value";

       这将会把这个数据项存储在缓存中而不包含任何依赖项, 因此除非缓存引擎要为给其它额外缓冲对象腾出空间而移除它, 否则它不会过期. 要明确包含依赖项, 我们使用 Add() 或 Insert() 方法. 每一个方法都有一些重载版本. 它们之间仅有区别在于 Add() 返回一个缓冲对象的引用, 而 Insert() 不返回任何值 ( void in c#, Sub in VB).

例子

 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", myFrequestlyAccessedData, null, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(1));

        变化的过期时间: 这个例子缓冲一些频繁访问的数据. 数据将保持在缓冲中直到无任何对象引用它达到一分钟以上. 注意它和绝对过期时间不能混用.

更多选项

       除了在上面提到的依赖以外, 我们还可以指定数据项的优先级 (从低到高至不可移除, 在System.Web.Caching.CacheItemPriority枚举中定义) 和一个 CacheITemRemovedCallback 方法, 当数据项在缓冲中到期时调用. 大多情况下, 默认优先级已经足够--让缓冲引擎来决定怎样做更好及处理缓冲的内存管理. CacheItemRemovedCallback 选项允许一些有趣的结果, 但在实践中, 几乎不使用它. 但是, 为了演示这个技术, 我给出一个用法的示例:

CacheItemRemovedCallback 示例

 CacheItemRemovedCallback callback = new CacheItemRemovedCallback (OnRemove);
 Cache.Insert("key",myFile,null,  Cache.NoAbsoluteExpiration, TimeSpan.Zero, CacheItemPriority.Default, callback);
  . . .
 public static void OnRemove(string key,  object cacheItem, CacheItemRemovedReason reason)
 {
  AppendLog("The cached value with key '" + key + "' was removed from the cache.  Reason: " + reason.ToString());
 }
 
        该示例将数据在缓冲中过期的原因记入日志, 使用在 AppendLog() 方法中定义的逻辑. 当数据项从缓冲中移除时记录它并注明原因可以允许你测定你是否有效地使用了缓冲, 或者也许你需要增加服务器上的内存. 注意这个回调方法是一个静态方法, 推荐使用这种方法,  因为如果不这样, 将有一个保存这个回调方法的对象实例在内存中, 而静态方法则不需要. 这个特性的一个可能的用处是在后台刷新缓冲的数据, 这样用户永远不需要等待数据的产生, 但数据保持相对较新. 但在实践中很不
幸的是在当前版本的缓冲编程接口中, 这不是工作得很好, 因为回调方法不在数据项从缓冲中移除之前激发或完全执行. 这样当用户将频繁发出请求试图访问缓冲的值, 却发现它为空, 而且必须要等待它重新产生. 在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;
 }

 关于这个模式有几点要指出:

 1. 值, 比如 cacheKey, cacheItem 以及 cache duration 被定义了一次且仅仅一次.
 2, 如果需要的话, 缓冲可以被忽略--比方说, 刚刚注册了一个新客户并重定向到一个客户列表, 很可能忽略缓冲并用最新数据重新产生列表是最好的, 这样会包含这个新加入的客户.
 3. 缓冲仅被访问一次. 这会有一些性能上的好处而且保证空指针异常不会发生, 因为数据项在第一次检查的时候被呈现, 但在第二次检查之前已经过期.
 4.  该模式使用强类型检查. c#中的 "as" 运算符试图将 object 转换成一个类并且失败后简单返回一个null.
 5.  持续时间存放在一个配置文件中. 所有缓冲依赖项, 无论是基于文件, 基于时间或其它的, 理想情况下应该被放在一个配置文件中, 这样可以更方便地进行更改及性能测试. 我还建议指定一个默认的缓冲持续时间, 而且 GetCacheSecondsFromConfig()
方法在当前使用的cacheKey没有指定持续时间时使用默认的时间.

posted @ 2007-10-23 23:17  Anson2020  阅读(395)  评论(0编辑  收藏  举报