输出缓存与CachePanel
缓存的级别
缓存的作用自不必说,提高系统性能最重要的手段之一。上至应用框架,下至文件系统乃至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事件中检查是否命中缓存:
- 执行ResolveCacheKey事件以确认CacheKey。
- 随机选取副本编号。
- 根据CacheKey和副本编号确认被缓存的数据所使用的key(如果CacheKey为空,则使用默认的CacheKey,它保证了同一页面中的位置相同的CachePanel实例共享缓存版本)。
- 如果缓存命中,则清除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的更多相关文章
- 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 ...
- PHP的OB缓存(输出缓存)
使用PHP自带的缓存机制 原则:如果ob缓存打开,则echo的数据首先放在ob缓存.如果是header信息,直接放在程序缓存.当页面执行到最后,会把ob缓存的数据放到程序缓存,然后依次返回给浏览器. ...
- IntelliJ IDEA修改Output输出缓存区大小【应对:too much output to process】
IntelliJ IDEA默认的Output输出缓存区大小只有1024KB,超过大小限制的就会被清除,而且还会显示[too much output to process],可通过如下配置界面进行修改O ...
- ASP.NET缓存全解析2:页面输出缓存 转自网络原文作者李天平
页面输出缓存是最为简单的缓存机制,该机制将整个ASP.NET页面内容保存在服务器内存中.当用户请求该页面时,系统从内存中输出相关数据,直到缓存数据过期.在这个过程中,缓存内容直接发送给用户,而不必再次 ...
- [置顶]
MVC输出缓存(OutputCache参数详解)
1.学习之前你应该知道这些 几乎每个项目都会用到缓存,这是必然的.以前在学校时做的网站基本上的一个标准就是1.搞定增删改查2.页面做的不要太差3.能运行(ps真的有这种情况,答辩验收的时候几个人在讲台 ...
- PHP输出缓存ob系列函数
ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额外的负担. ob的基本原则:如果 ...
- MVC输出缓存(OutputCache参数详解)
版权声明:本文为博主原创文章,未经博主允许转载随意. https://blog.csdn.net/kebi007/article/details/59199115 1.学习之前你应该知道这些 几乎每个 ...
- PHP输出缓存ob系列函数详解
PHP输出缓存ob系列函数详解 ob,输出缓冲区,是output buffering的简称,而不是output cache.ob用对了,是能对速度有一定的帮助,但是盲目的加上ob函数,只会增加CPU额 ...
- ASP.NET MVC如何使用输出缓存
通过这篇文章你将学习到在MVC中如何使用输出缓存,业务逻辑我就不多介绍了,主要是Outputcache的基本使用.至于数据缓存还是等我的下一篇文章吧,一步一步来不急的. 输出缓存的使用方法是在Co ...
随机推荐
- linux c编程:信号(五) sigsuspend
更改进程的信号屏蔽字可以阻塞所选择的信号,或解除对它们的阻塞.使用这种技术可以保护不希望由信号中断的代码临界区.如果希望对一个信号解除阻塞,然后pause等待以前被阻塞的信号发生,则又将如何呢?假定信 ...
- Excel 文件下载
INCLUDE OLE2INCL * ALV输出 CALL FUNCTION 'REUSE_ALV_GRID_DISPLAY' EXPORTING I_CALLBACK_PRO ...
- C# Lambda表达式与Linq
, , , , , , }; //linq写法 var res = from i in arry select i; //lambda写法 var res = arry.Select(i => ...
- python 局部local和全局global变量
global和local变量 虽然简单,但是还是记录一下,主要是转载 转载自:http://blog.sina.com.cn/s/blog_436992740102ux8z.html 先看一段代码 ...
- 深入理解利用new创建对象的执行过程以Person p=new Person("张三",20);为例
代码如下: class Person { private String name="haha"; private int age; private static String co ...
- C#判断VS是否处于设计模式
public class CheckDesingModel { public static bool IsDesingMode() { bool ReturnFlag = false; if (Lic ...
- 【leetcode刷题笔记】Longest Valid Parentheses
Given a string containing just the characters '(' and ')', find the length of the longest valid (wel ...
- 【leetcode刷题笔记】Remove Duplicates from Sorted List II
Given a sorted linked list, delete all nodes that have duplicate numbers, leaving only distinct numb ...
- 《python基础教程(第二版)》学习笔记 字符串(第3章)
<python基础教程(第二版)>学习笔记 字符串(第3章)所有的基本的序列操作(索引,分片,乘法,判断成员资格,求长度,求最大最小值)对字符串也适用.字符串是不可以改变的:格式化输出字符 ...
- Linux_笔记_01_设置静态IP与 SecureCRT连接Linux
步骤一至三,即可设置好静态IP 步骤四至九,使SecureCRT连接Linux 步骤一:编辑ifcfg-eth0 文件 1.打开ifcfg-eth0 文件 使用命令:vi /etc/sysconfig ...