缓存的级别

缓存的作用自不必说,提高系统性能最重要的手段之一。上至应用框架,下至文件系统乃至CPU,计算机中各部分设计都能见到缓存的身影。许多朋友一直在追求如何提高Web应用程序的性能,其实最容易被理解和采纳的一条估计就是“缓存”了。也正因为如此Live Journal才会开发出memcached,而微软也推出了Velocity。

有朋友说生成静态页?好吧,在老赵看来,其实这只是把页面内容缓存至硬盘罢了。不过这就涉及到了缓存的某些“级别”了。撇开硬件(如CPU)和系统(例如文件系统,数据库系统)的缓存不说,如果只谈论应用级别——也就是我们平时“代码”中会遇到的缓存来说,一般可以将缓存分为两大类:缓存“数据”和“输出”。啥叫缓存数据呢?例如在代码中,我们将一个表示用户的User实例放入内存,以避免第二次读取时访问较慢的存储设备,这就是缓存了“数据”。至于缓存输出,最典型的也就莫过于刚才提到的“静态页”了。生成了静态页之后,大量的请求将直接获得结果,而不需要进行动态处理,这性能自然就提高了。

这两大类缓存其实也就是缓存的两个级别。之所以把它们分个“高低”或“上下”(并非褒贬,仅指“位置”),是因为在一个应用中,这两类缓存在使用和处理上有所不同。

对于数据缓存,如果用典型的三层架构(有些朋友会单独提出一个“缓存层”,这我就不多作区分了,该往哪层靠拢相信大家自能分清)来举例,往往发生在业务逻辑层和数据访问层。数据访问层里的缓存自然与业务逻辑无关,因此缓存的大都以“单个对象”为主,而缓存的命中与过期策略由“对象”所表示的“数据”的特性来决定,例如Hibernate的一级和二级缓存会根据对象的ID进行确定命中或过期操作。有些数据访问框架提供了更复杂的缓存功能,例如Hibernate也能够根据Query生成Key来进行缓存。而业务逻辑层中的缓存策略则可能较为复杂,命中与过期都与业务需求密切相关。例如一个用户的好友列表(假设是一个int数组)将会根据用户修改其好友信息时过期——当然也可能不是简单的过期,而是直接在内存中进行修改,不过这时候就要考虑到并发性等等。

至于“输出”缓存则容易理解多了。任何应用最终都要有所输出,输出往往就要导致格式的改变,例如变成HTML,XML,亦或是二进制流(有人说其实一切都是二进制流——没错没错,不过我们还是在略高层次上看这个问题吧)。这种直接和表现相关的缓存自然是放在“表现层”了。缓存“输出”相对于缓存“数据”的优点便是有更高的性能。“数据”还需要通过运算才能变成“输出”(试想我们可能需要操作了10K的数据只为了生成1K的有效内容),而缓存“输出”则直接面向了输出内容,性能的提高难道不是显而易见的吗?至于缺点,那就是降低了缓存的复用几率,一个HTML形式的数据自然无法被需要SOAP格式的输出所复用。降低了复用几率则意味着降低了缓存命中率,则意味着提高了存储所需的空间——或者降低了性能。等等,这不是和之前所说“较高性能”的有所矛盾吗?没错,我们刚开始学算法就会接触到一个词:“时间换空间、空间换时间”,这就道出玩儿编程的本质:一切都是在权衡,世上没有真正完美的做法。缓存“输出”其实玩的就是“空间换时间”的游戏,我们缓存的便是同样数据的不同表现形式。

当然缓存输出还有个问题可能就是不太容易设计合适缓存策略(主要是过期策略),对于实时性要求高的内容,往往不得不放弃输出缓存而启用数据缓存。

话说回来,在很多情况下,我们的业务真需要很高的实时性吗?例如一张新建的照片真的需要被立即索引到吗?照片的访问次数真要在列表中即时更新吗?很多时候需求的一些细节膨胀只是一厢情愿,对于用户来说并没有太大意义——尤其是在Web 2.0应用中,因为用户是很容易被诱导的,而且并不会对很多数据进行追究。因此技术架构师在和领域专家进行探讨时不如多提一些建议,一个常用的“句式”是:“XX要求的目的是不是为了YY?如果ZZ的话您能否接受呢?”。不过,如果一个用户删除了自己的一篇文章,却发现它还在自己的管理列表中出现,那么就实在不能说是一个“能够令人接受”的现象了。

ASP.NET的Output Cache及其缺陷

ASP.NET作为一个成熟、强大的应用程序框架,缓存相关的设计自然是它不可或缺的一部份。而ASP.NET作为一个面向Web应用的框架,实现的便是表现层的方方面面,因此它所涉及到的缓存,自然就是上文所说的“输出缓存”了(HTTP协议的缓存不在本文讨论范围之内)。ASP.NET中的输出缓存即为所谓的“OutputCache”。Output Cache的使用可以在任意一本ASP.NET程序设计的书中找到,不过最详细的描述自然莫过于MSDN了。下文将对于ASP.NET的Output Cache进行简单描述,仅仅是为了形成完整的讨论内容。

OutputCache为WebForms框架的一部分,可以在Page和User Control中使用。OutputCache的命中可以让对于某个控件,甚至是整个请求内容的处理直接获得HTML内容,而不需要对页面或控件进行处理。Output Cache主要通过在aspx或ascx文件顶部添加<@ OutputCache />标记来使用,在标记中可以定义缓存的各种特性,例如缓存在多少时间后失效(Duration),缓存的存储位置(Location),缓存的版本控制(VaryByControl / Header / Param / Custom / ContentEncoding)以及缓存与哪个SqlDependency依赖等等。而User Control中的OutputCache标记相对于Page还可以指定一个Shared属性,如果该属性为true,则表示UserControl的缓存可以跨Page命中,反之则会为不同的页面生成不同的缓存内容。此外,ASP.NET还提供了<asp:Substitution />控件,该控件在User Control或Page被缓存的时候依然会被执行,使程序员可以通过编程的方式为缓存内容中的特定部分进行改变。

ASP.NET OutputCache使用起来非常容易,但是在实际运用中往往会显得不够。例如有一个Users控件,用于显示UserIDs所指定的用户。这很容易。不过我们现在有个“特别”的需求,就是在同一个页面中放置两个这样的控件:

<jeffz:Users runat="server" UserIDs="1, 2, 3, 4, 5" /> <jeffz:Users runat="server" UserIDs="6, 7, 8, 9, 10" />

两个控件“实例”的UserIDs参数截然不同,自然输出的内容也会大相径庭。不过如果我们为Users控件添加了OutputCache(并且使用了最传统的VaryByParam="*")之后,问题就出现了。猜猜看结果如何?第二个控件实例的输出和第一个完全相同了,都是参数“1, 2, 3, 4, 5”的结果。这是因为VaryByParam的Param是指QueryString,或Post时的Form数据,而我们的页面在请求时哪有这方面的变化?于是我们就需要开始寻找解决方案了,翻遍了MSDN有关OutputCache的内容,可能只有一个VaryByCustom有些接近。可是VaryByCustom将某个特定参数传给GlobalApplication的GetVaryByCustomString方法中,其余的信息就只有个HttpContext了。所以VaryByCustom也只能通过公用的信息来判断是否使用之前缓存的版本,而无法根据哪个页面的具体哪个控件实例,以及某个控件实例的状态来决定缓存的版本。因此我们可以这么认为,在这种情况下,我们的Users控件无法使用ASP.NET的OutputCache。

<p不过事实上,老赵也曾经用过一种恶心地令人发指的方法来让getvarybycustomstring方法获得当前正在进行处理的控件及其属性,可惜十分复杂,不值得推广。< p="">

还有一种情况就是需要“无条件”地为控件保存多个版本的缓存。例如有个需求是写一个RandomUsers控件,根据UserIDs指定的数据中随机挑选出几个用于展示的用户数据,例如:

<jeffz:RandomUsers runat="server" UserIDs="1, 2, 3, 4, 5" Count="3" />

那么现在还能够使用ASP.NET的OutputCache吗?至少老赵不知道该如何做。因此我们需要一个额外的缓存输出的解决方案,它的要求其实只有两个:

  • 可以自由地定义缓存版本。
  • 可以为每个版本缓存不同的副本,并随机输出。

这就是老赵下面要提到的这个解决方案:CachePanel的需求来源。

CachePanel的构建与使用

其实CachePanel很简单,相信已经有一些朋友能够想象出这个组件的大致逻辑了。根据老赵的习惯,我们还是使用“用例驱动开发”的方式来进行组件开发。例如老赵期望的使用方式是这样的:

<jeffz:CachePanel runat="server" Duration="00:15:00" CopyCount="10" CacheKey="RandomUsers" ResolveCacheKey="CachePanel_ResolveCacheKey" >  <jeffz:RandomUsers runat="server" UserIDs="1, 2, 3, 4, 5" Count="3" /> </asp:CachePanel>

以下是CachePanel的各种成员描述与代码:

public class CachePanel : Control { public TimeSpan Duration { get; set; }  public int CopyCount { get; set; }  public string CacheKey { get; set; }  public EventHandler ResolveCacheKey { get; set; }  ... }
  • Duration属性:每份缓存副本的有效时间长度。这里使用TimeSpan的字符串表示法,老赵认为相较传统的秒数,这样能够更直接地设定和读取一段时间长度。
  • CopyCount属性:每个缓存版本的副本数量,输出时将任意选择一个副本输出。在上例中,我们通过生成10个副本让用户看起来的确是在输出随机的结果,而其实我们只是缓存了10个副本而已。
  • CacheKey属性:不同的CacheKey决定了不同的缓存版本。请注意这个CacheKey是全局的,因此不同页面中的CachePanel如果CacheKey相同,将会得到相同的缓存结果(排除CopyCount属性的影响)。
  • ResolveCacheKey事件:提供了一个动态指定缓存版本的可能。开发人员可以响应该事件,根据上下文环境的不同(例如QueryString,Form或Header的不同)对CacheKey进行改变。

可以看到,其实只是这简单的四个成员就能满足上文的要求(而且事实上,在理论上CopyCount也能够省略,因为我们有ResolveCacheKey事件,不是吗?)。

至于与缓存相关的具体逻辑,其实非常简单。首先是在OnInit事件中检查是否命中缓存:

  1. 执行ResolveCacheKey事件以确认CacheKey。
  2. 随机选取副本编号。
  3. 根据CacheKey和副本编号确认被缓存的数据所使用的key(如果CacheKey为空,则使用默认的CacheKey,它保证了同一页面中的位置相同的CachePanel实例共享缓存版本)。
  4. 如果缓存命中,则清除CachePanel内的所有子控件。

代码如下:

public class CachePanel : Control { ... private static Random s_random = new Random(DateTime.Now.Millisecond);  public bool CacheHit { get; private set; }  private string m_cacheKey; private string m_cachedContent;  protected override void OnInit(EventArgs e) { var resolveCacheKey = this.ResolveCacheKey; if (resolveCacheKey != null) { resolveCacheKey(this, EventArgs.Empty); }  int copyIndex = s_random.Next(this.CopyCount); this.m_cacheKey = this.GetCacheKey(copyIndex);  this.m_cachedContent = this.Context.Cache.Get(this.m_cacheKey) as string; this.CacheHit = (this.m_cachedContent != null); if (this.CacheHit) this.Controls.Clear();  base.OnInit(e); }  private string GetCacheKey(int copyIndex) { var cacheKeyBase = this.CacheKey ?? this.GetDefaultCacheKeyBase(); return "$CachePanel$" + cacheKeyBase + "_" + copyIndex; }  private string GetDefaultCacheKeyBase() { return this.Context.Request.AppRelativeCurrentExecutionFilePath + "_" + this.UniqueID; } ... }

由于内容被清空,然后到了生成内容阶段,事情就好办了——简单的缓存子控件生成的HTML内容即可:

public partial class CachePanel : Control {  ... protected override void RenderChildren(HtmlTextWriter writer) { if (this.m_cachedContent == null) { StringBuilder sb = new StringBuilder(); HtmlTextWriter innerWriter = new HtmlTextWriter(new StringWriter(sb)); base.RenderChildren(innerWriter);  this.m_cachedContent = sb.ToString(); this.Context.Cache.Insert(this.m_cacheKey, this.m_cachedContent, null,  DateTime.Now.Add(this.Duration), Cache.NoSlidingExpiration); }  writer.Write(this.m_cachedContent); } }

至此,CachePanel就制作完成了,其实只是短短的几十行代码而已。到这里老赵不禁又要发一句感慨:只要了解了框架的运行规则,开发出各种扩展又有多少难度呢?一切都只是看您有多少想象力而已。

不过大家在使用CachePane时可能还需要注意以下几点:

  • CacheKey的作用域是整个ASP.NET应用程序,因此如果您要指定CacheKey的话,请给出清晰而明确的值。
  • CachePanel能够缓存页面中任意部分的内容,不过在使用时可能就需要您根据CacheHit属性的值来判断是否需要为控件填充数据,否则可能就会无法达到缓存的目的。
  • CachePanel将会在缓存命中时清空所有子控件,因此在操作时也请注意这一点,以免出现不可预知(Unpredictable)的结果。

转自:http://www.cnblogs.com/JeffreyZhao/archive/2008/07/28/CachePanel.html

输出缓存与CachePanel的更多相关文章

  1. PHP flush sleep 输出缓存控制详解

    1 2 3 4 5 6 ob_start,flush,ob_flush for($i=0;$i<</SPAN>10;$i++) { echo $i.''; flush(); slee ...

  2. PHP的OB缓存(输出缓存)

    使用PHP自带的缓存机制 原则:如果ob缓存打开,则echo的数据首先放在ob缓存.如果是header信息,直接放在程序缓存.当页面执行到最后,会把ob缓存的数据放到程序缓存,然后依次返回给浏览器. ...

  3. IntelliJ IDEA修改Output输出缓存区大小【应对:too much output to process】

    IntelliJ IDEA默认的Output输出缓存区大小只有1024KB,超过大小限制的就会被清除,而且还会显示[too much output to process],可通过如下配置界面进行修改O ...

  4. ASP.NET缓存全解析2:页面输出缓存 转自网络原文作者李天平

    页面输出缓存是最为简单的缓存机制,该机制将整个ASP.NET页面内容保存在服务器内存中.当用户请求该页面时,系统从内存中输出相关数据,直到缓存数据过期.在这个过程中,缓存内容直接发送给用户,而不必再次 ...

  5. [置顶] MVC输出缓存(OutputCache参数详解)

    1.学习之前你应该知道这些 几乎每个项目都会用到缓存,这是必然的.以前在学校时做的网站基本上的一个标准就是1.搞定增删改查2.页面做的不要太差3.能运行(ps真的有这种情况,答辩验收的时候几个人在讲台 ...

  6. PHP输出缓存ob系列函数

    ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额外的负担. ob的基本原则:如果 ...

  7. MVC输出缓存(OutputCache参数详解)

    版权声明:本文为博主原创文章,未经博主允许转载随意. https://blog.csdn.net/kebi007/article/details/59199115 1.学习之前你应该知道这些 几乎每个 ...

  8. PHP输出缓存ob系列函数详解

    PHP输出缓存ob系列函数详解 ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额 ...

  9. ASP.NET MVC如何使用输出缓存

    通过这篇文章你将学习到在MVC中如何使用输出缓存,业务逻辑我就不多介绍了,主要是Outputcache的基本使用.至于数据缓存还是等我的下一篇文章吧,一步一步来不急的.   输出缓存的使用方法是在Co ...

随机推荐

  1. mac上傻瓜式java安装环境配置

    适用于mac新手用户或者黑苹果用户 首先,打开终端 输入 java -version 检查是否已安装好Java运行环境 显示我现在电脑没有安装 如果返回版本号,说明运行环境成功 对于windows用过 ...

  2. leetcode 1049 Last Stone Weight II(最后一块石头的重量 II)

    有一堆石头,每块石头的重量都是正整数. 每一回合,从中选出任意两块石头,然后将它们一起粉碎.假设石头的重量分别为 x 和 y,且 x <= y.那么粉碎的可能结果如下: 如果 x == y,那么 ...

  3. tomcat异常处理经验汇总

    1.Https: Feb 21, 2018 5:22:02 PM org.apache.coyote.AbstractProtocol initSEVERE: Failed to initialize ...

  4. [原创] Windows下Eclipse连接hadoop

    1 下载hadoop-eclipse-plugin :我用的是hadoop-eclipse-plugin1.2.1 ,百度自行下载 2 配置插件:将下载的插件解压,把插件放到..\eclipse\pl ...

  5. 【leetcode刷提笔记】Permutations

    Given a collection of numbers, return all possible permutations. For example,[1,2,3] have the follow ...

  6. python操作mysql(一)原生模块pymysql

    一.下载安装 pymsql是Python中操作MySQL的模块,其使用方法和MySQLdb几乎相同. 下载安装 C:\Users\Administrator>pip install pymysq ...

  7. 删除power by dedecms的方法

    在include/dedesql.class.php文件,会发现最新的include/dedesql.class.php文件会多出第588到第592行的那几段代码,代码如下图: $arrs1 = ar ...

  8. shell 比较运算符

    运算符   描述 示例 文件比较运算符 -e filename 如果 filename 存在,则为真(不管文件或目录) [ -e /var/log/syslog ] -d filename 如果 fi ...

  9. SrpingCloud 之SrpingCloud config分布式配置中心实时刷新

    默认情况下是不能及时获取变更的配置文件信息 Spring Cloud分布式配置中心可以采用手动或者自动刷新 1.手动需要人工调用接口   监控中心 2.消息总线实时通知  springbus 动态刷新 ...

  10. adobe flash player 下载地址

    1. https://www.adobe.com/cn/products/flashplayer/distribution3.html