http://www.cnblogs.com/cs_net/archive/2011/03/17/1986620.html
一、缓存
缓存(Cache)技术在软件开发过程中有着广泛的用途, 它对提升软件性能和改善客户体验有很大帮助. 所谓缓存, 是指将那些经常重复的操作结果暂时存放起来, 在以后的执行过程中, 只要使用前面的暂存结果即可. 缓存技术在日常生活中随处可见, 就拿排队买票来说吧: 买票时需要先排队, 等轮到自己了, 再告诉售票员你需要买那里的票, 售票员查询完后告诉你有还是没有. 若有, 付款走人; 若没有, 离开队列. 例如, 北京到上海是热门线路, 票很快就卖完了, 但是根据购票的规则, 仍然有许多乘客会先排队, 然后问有没有北京到上海的票, 结果只能默默离开队列. 如果每个人都问一遍的话, 售票员估计得烦死. 于是, 售票员在窗口上挂个牌子, 写上”北京到上海(无票)”, 之后再要购买该票的乘客看到这个牌子就不会在队列中继续等待了. 这就是生活中的一个缓存事例.
由此可见, 缓存的思想就是: 尽量减少执行那些费时的、需要经常重复执行的工作的次数. 在计算机的世界中, 执行速度最慢的操作就是IO操作, 也就是读写硬盘的操作. 因此, 构建一个高效的桌面应用程序或网站应用程序时, 很重要的一个因素就是尽可能少的减少IO操作. 比如, 相同的数据没有必要每次都从数据库中读取, 在第一次读取出来后缓存起来即可, 第二次就不需要在做IO操作了.
那么在我们开发Web网站的过程中, 到底有多少工作可以采用用缓存呢? 或者说, 我们可以在哪些地方使用缓存呢? 见下图: 下图是客户端浏览器和Web服务器之间的一次完整的通信过程, 红色圆圈标示了可以采用缓存的地方.
关于上图, 如果一边描述请求过程, 一边描述缓存可能会不便理解. 像做手术一样, 这里首先弄清楚整个请求过程, 然后从整体上对关键节点设置的缓存进行描述.
上图所示的请求过程:
客户端浏览器首先发送HttpRequest请求到IIS服务器, 当然也有一些企业内部网络或者小型机关网络中会设置代理服务器. 代理服务器在请求过程中可以看作是一个转发请求的中继器, 或者干脆在这里忽略代理服务器也行.
IIS服务器接受到请求后, 会根据请求地址进行判断, 例如: *.html、*.jpg等静态内容的资源, 会直接由IIS寻找资源并返回给客户端浏览器; 例如: *.aspx、*.ashx、*.asmx等asp.net中的动态内容会交给aspnet_isapi.dll处理.
aspnet_isapi.dll将请求转交给asp.net组件处理, 这时asp.net组件会启动一个asp.net的进程(进程名称为w3wp.exe), 该进程会创建一个应用程序域(一个进程可以创建多个应用程序域, 一个应用程序域中可以跑多个线程), 在应用程序域中会创建asp.net执行环境HttpRuntime, 在asp.net执行环境中创建HttpContext对象并启动HttpApplication应用程序管道(HttpRuntime中可以同时创建多个HttpContext和HttpApplication, 也就是说在HttpRuntime中可以同时进行多套处理流程). 其中, HttpContext其实就是对一堆参数的封装, 如: HttpRequest、HttpResponse、HttpServerUtility等, HttpContext可以在整个HttpApplication管道中使用, 可以取出其中的参数, 并且HttpContext本身也可以当作一个字典来使用. HttpApplication也就是asp.net应用程序管道或者叫asp.net应用程序生命周期, 它是实际处理客户端请求的地方, 我们只能通过Global.asax或者IHttpModule接口来人为控制客户端请求的处理过程.
在HttpApplication启动后, 在IIS6.0中会有17个事件(IIS7.0中会更多), 其中包括: 用户验证、用户授权等等, 这些事件中有两个重要的时间点, 第一个时间点是在第7个事件之后, 也就是PostResolveRequestCache事件之后, 映射请求的处理程序(需查找配置文件); 第二个时间点是在第11个事件之后, 也就是PreRequestHandlerExecute事件之后, 执行处理程序, 像对*.aspx的处理程序System.Web.UI.PageHandlerFactory.Page就是实现了IHttpHandler接口的类. 在这个时间点中执行处理程序并进入页面的生命周期, 其中最重要的是页面处理的11个事件(如: Page_Int、Page_Load、Page_PreRender、Page_SaveState、Page_Unload), 我们在这11个事件中, 完成读取aspx页面模板、控件预读、读取视图状态、控件属性赋值等工作, 并且要求我们程序员调用BLL、DAL、DE以及数据库等完成业务逻辑的处理工作. 最后, 在由页面对象中的Render方法将结果发送给客户端浏览器, 客户端浏览器再呈现给用户.
至此, 完成了一次完整的请求过程. 那么, 我们的缓存工作应该在哪里做呢?
缓存该在哪里做, 取决于我们能在整个请求过程的什么位置动手. 我们依然从客户端浏览器开始:
首先, 最好的情况是客户端不发送任何请求直接就能获得数据, 这种情况下, 用于缓存的数据保存在客户端浏览器的缓存中.
其次, 在具有代理服务器的网络环境中, 代理服务器可以针对那些经常访问的网页制作缓存, 当局域网中第一台主机请求了某个网页并返回结果后, 局域网中的第二台主机再请求同一个网页, 这时代理服务器会直接返回上一次缓存的结果, 并不会向网络中的IIS服务器发送请求, 例如: 现在的连接电信和网通线路的加速器等. 但是代理服务器通常有自己专门的管理软件和管理系统, 作为网站开发人员对代理服务器的控制能力有限.
再次, 前面也说过, 当用户将请求地址发送到IIS服务器时, IIS服务器会根据请求地址选择不同的行为, 如: 对于*.aspx页面会走应用程序管道, 而对*.html、*.jpg等资源会直接返回资源, 那么我们可以把那些频繁访问的页面做成*.html文件, 这样用户请求*.html, 将不用再走应用程序管道, 因此会提升效率. 例如: 网站首页、或某些突发新闻或突发事件等, 可以考虑做成静态网页. 就拿”天气预报的发布页面”打比方: 天气预报的发布页面的访问用户非常多, 我们可以考虑将发布页做成静态的*.html网页, 之后在整个网站程序启动时, 在Gboabl.asax的Application_Start事件处理器中, 创建子线程以实现每3个小时重新获取数据生成新的天气发布页面内容.
之后的asp.net的处理流程, 作为程序员我们是无法干涉的. 直到启动HttpApplication管道后, 我们才可以通过Global.asax或IHttpModule来控制请求处理过程, 在应用程序管道中适合做整页或用户控件的缓存. 如: 缓存热门页面, 我们可以自动缓存整个网站中访问量超过一定数值(阀值)的页面, 其中为了减小IO操作, 将缓存的页面放在内容中.
最后, 我们程序员可以操作的地方就是页面处理管道或者称为页面生命周期, 我们可以页面对象的11个事件中, 采用数据缓存的方式减小访问数据库的次数, 也就是减少IO操作. 还有些真假分页等问题涉及网站的优化, 稍后再讨论.
总之, 我们可以采用缓存的地方有以下5处:
1. 客户端浏览器的缓存(非Cookie, Cookie只能放字符串信息)
2. 如果有代理服务器的话, 可以在代理服务器中缓存整个网页(本篇仅讨论网站开发的相关内容)
3. 将频繁访问的资源做成*.html等的静态内容, (我们可以将静态资源缓存在ramdisk等开辟的内存盘中, 以进一步减少对硬盘的访问).
4. 在应用程序处理管道或应用程序生命周期中, 进行整页缓存或用户控件的局部缓存
5. 在页面处理管道或页面生命周期中, 做数据缓存
下面我们来详细论述这些缓存手段:
1. 客户端浏览器上的缓存(非Cookie, Cookie中的内容为: 键和值均为string类型的键值对)
我们可以通过在Http回应中增加特定的头部说明来指定浏览器的缓存策略; 添加头部说明的手段既可以通过页面指令声明设置, 也可以通过编程方式设置.
对于图片、Javascript脚本、CSS等资源, 可以在IIS管理器中, 右击图片等资源, 选择”属性” --> HttpHeaders后, 勾选Enable Content Expiration并设置时间即可. 一种值得推荐的手段是, 将需要缓存的资源分类, 如: image/dynamic/、image/static/, 这样我们可以再文件夹上, 选择属性中的HttpHeaders进行设置. 否则, 针对每一个静态资源设置HttpHeaders将是件非常痛苦的事情. 此外, 还可以采用一款名为CacheRight的工具可以提供缓存资源的统一配置.
查看或设置浏览器缓存位置: IE --> Internet选项 --> 常规 --> 临时Internet文件 --> 设置
Html文件的Head中的缓存设置:
<meta http-equiv="pragma" content="no-cache" />
<meta http-equiv="Cache-Control" content="no-cache" />
<meta http-equiv="expires" content="Wed, 26 Feb 1997 08:21:57 GMT" />
浏览器中关于Cache的3属性:
Cache-Control:
设置相对过期时间, max-age指明以秒为单位的缓存时间. 若对静态资源只缓存一次, 可以设置max-age的值为315360000000 (一万年).
Http协议的cache-control的常见取值及其组合释义:
no-cache: 数据内容不能被缓存, 每次请求都重新访问服务器, 若有max-age, 则缓存期间不访问服务器.
no-store: 不仅不能缓存, 连暂存也不可以(即: 临时文件夹中不能暂存该资源)
private(默认): 只能在浏览器中缓存, 只有在第一次请求的时候才访问服务器, 若有max-age, 则缓存期间不访问服务器.
public: 可以被任何缓存区缓存, 如: 浏览器、服务器、代理服务器等
max-age: 相对过期时间, 即以秒为单位的缓存时间.
no-cache, private: 打开新窗口时候重新访问服务器, 若设置max-age, 则缓存期间不访问服务器.
private, 正数的max-age: 后退时候不会访问服务器
no-cache, 正数的max-age: 后退时会访问服务器
点击刷新: 无论如何都会访问服务器.
Expires:
设置以分钟为单位的绝对过期时间, 优先级比Cache-Control低, 同时设置Expires和Cache-Control则后者生效.
Last-Modified:
该资源的最后修改时间, 在浏览器下一次请求资源时, 浏览器将先发送一个请求到服务器上, 并附上If-Unmodified-Since头来说明浏览器所缓存资源的最后修改时间, 如果服务器发现没有修改, 则直接返回304(Not Modified)回应信息给浏览器(内容很少), 如果服务器对比时间发现修改了, 则照常返回所请求的资源.
注意:
Last-Modified属性通常和Expires或Cache-Control属性配合使用, 因为即使浏览器设置缓存, 当用户点击”刷新”按钮时, 浏览器会忽略缓存继续向服务器发送请求, 这时Last-Modified将能够很好的减小回应开销.
ETag将返回给浏览器一个资源ID, 如果有了新版本则正常发送并附上新ID, 否则返回304, 但是在服务器集群情况下, 每个服务器将返回不同的ID, 因此不建议使用ETag.
以上描述的客户端浏览器缓存是指存储位置在客户端浏览器, 但是对客户端浏览器缓存的实际设置工作是在服务器上的资源中完成的. 虽然刚才我们介绍了有关于客户端浏览器缓存的属性, 但是实际上对这些属性的设置工作都需要在服务器的资源中做设置. 我们有两种操作手段对浏览器缓存进行设置, 一个是通过页面指令声明来设置, 另外一个是通过编程方式来设置.
浏览器缓存的设置手段:
第一: 通过页面指令声明来设置HTTP的缓存
页面指令<%@ OutputCache Location=”Any” Duration=”10” VaryByParam=”ProductId” VaryByHeader=”Accept-Language”%>中的Location用来设置缓存的位置, 该属性常见的值为:
Any(默认): 输出缓存可以位于任何地点, 对应于HttpCacheability.Public. 如: 客户端浏览器、代理服务器或服务器本身.
Client: 只能位于发出请求的客户端浏览器, 对应于HttpCacheability.Private.
Downstream: 输出缓存可以位于除服务器本身的其他任何地方, 如: 客户端浏览器、代理服务器.
Server: 输出缓存位于Web服务器本身, 对应于HttpCacheability.Server
ServerAndClient: 输出缓存只能位于服务器本身或客户端浏览器, 对应于HttpCacheability.Private和HttpCacheability.Server
None: 禁用输出缓存, 对应于HttpCacheability.NoCache.
VaryByParam属性: 根据请求参数的不同而缓存不同的版本. 多个值用分号(;)分隔, *号表示为任意参数或参数组合缓存不同版本, “none”表示只缓存一个版本.
VaryByHeader属性: 根据请求头来缓存不同的版本, 如同一页面的不同语言版本.
VaryByCustom属性: 根据自定义参数来缓存不同的版本, 如: VaryByCunstom=”browser”是系统已实现的, 根据浏览器名称和版本号缓存不同的版本. 也可以, 根据自定义参数来缓存, 如: VaryByCustom=”happy”, 此时系统不知道如何解释happy, 因此需要在Global.asax或IHttpModule实现类中重写GetVaryByCustomString()方法, 来完成处理逻辑.
VaryByControl属性: 根据用户控件中的服务器控件ID来缓存不同版本.
更高级的方式, 是通过配置文件来设置HTTP的缓存.
页面指令为<%@ OutputCache CacheProfile=”cacheconfig”%>, 其中cacheconfig是配置文件中的缓存配置节中CacheProfile的名称.
第二: 通过编程方式设置HTTP的缓存特性
在页面后台文件*.cs文件或*.ashx等其他代码文件中, 我们可以通过HttpResponse类的实例对象上的Cache属性or页面对象的Response属性上的Cache属性来设定缓存特性.
VaryByHeaders[“Accept-Language”] = true: 设置缓存同一页的不同语言版本
SetMaxAge(): 设置活动过期时间
SetExpires()方法: 设置绝对过期时间
SetLastModified(): 设置最后修改时间
SetNoStore(): 设置不要缓存
SetNoServerCaching(): 关闭服务器缓存
SetCacheability(): 设置缓存位置, 其参数类型为HttpCacheability枚举类型, 可选项如下:
NoCache: 将会在Http响应头添加Cache-Control: no-cache标头, 表示禁用缓存.
Private: 只在客户端浏览器缓存(关闭代理服务器缓存).
Public: 可在任意位置缓存.
Server: 只能在服务器上缓存.
ServerAndNoCache: 只能在服务器上缓存, 其余都不能缓存, 将会在Http响应头添加Cache-Control: no-cache标头.
ServerAndPrivate: 只能在服务器和客户端浏览器缓存, 不能再代理服务器上缓存.
注意:
其中, 设置NoCache和ServerAndNoCache, 将会在Http响应头中添加如下内容:
Cache-Control: no-cache
Pragma: no-cache
Expires: -1
以上设置可防止浏览器在自己的历史记录文件夹中缓存该页, 前进和后退按钮都会请求响应新的版本.
SetAllowResponseInBrowserHistory()方法: 允许在浏览器历史记录中响应, 若参数为true, 则可覆盖HttpCacheability的设置, 但如果SetCacheability设置为NoCache或ServerAndNoCache, 则会忽略SetAlloewResponseInBrowerHistory的值.
Cookies和代理缓存不能结合的很好, 当使用cookie时,不要允许代理缓存。设置Location为Private、Server或ServerAndPrivate。
用户控件中的缓存(部分页缓存)不可以使用Location属性, 因为在不同页面中, 同一用户控件缓存的结果将会有多个. 如果希望用户控件缓存同一结果, 可以添加Shared=”true”属性来实现.
页面缓存和用户控件缓存的Duration属性的时间长短问题: 结论是按照时间长的来算. 如: 页面缓存长, 用户控件缓存短, 则用户控件的缓存内容会和页面缓存一起到期; 若用户控件缓存长, 页面缓存短, 则用户控件的缓存内容会一直到用户控件的缓存时间结束.
Http Referer是Request.Header的一部分, 他可以指出是从哪个页面链接过来的, 但是可以伪造. 我们可以通过this.Request.Headers[“Referer”]或this.Request.UrlReferrer获得其内容, 最好通过this.Request.UrlReferrer.OriginalString来避轨编码不一致的问题.
2. 和代理服务器相关的缓存措施
对我们程序员来说, 和代理服务器相关的操作无非也是在服务器上设置的, 参见: 1中的浏览器缓存设置中的操作手段.
3. 将频繁访问的资源做成*.html等的静态内容
像天气预报这样的即时性要求不是很强的页面, 非常适合做成静态的html页面. 我们可以在网站的Global.asax中, 每3个小时重新读取一下数据, 将读取的数据生成新的html页面并覆盖掉老页面. 网站首页也是访问极其频繁的, 做法类似, 只是时间短点. 为简便起见, 直接用txt文件模拟数据库.
//App_Data/WeatherData.txt, 内容随便.
// WeatherReport/Today.html
注意: Global.asax是继承自HttpApplication类的子类, 如果网站存在Global.asax, 它会在HttpRuntime运行环境创建HttpApplication类的对象时, 用Global类的对象替带HttpApplication类的对象, 并且Application_Start、Application_End事件激发只会在服务器启动时进行, 而Application_BeginRequest等事件处理器则每次请求时都会激发. 本例在Application_Start中为保证主线程不被过分占用, 采用子线程创建timer并更新天气信息.
//Global.asax
4. 通过Global.asax或IHttpModule在应用程序处理管道中做整页缓存或用户控件局部缓存
第3所述的情况, 可以认为是在IIS级别的缓存, 它的目的是避免或劲量少走应用程序处理流程. 但是这种IIS级别的缓存只适合那些很少的热门资源, 如果所有页面都做成静态页面就又回到了古老的纯html时代, 当然是得不偿失. 那么大部分的资源还需要是动态的, 也就是说必须要走应用程序处理流程, 程序员可以通过Global.asax和IHttpModule来人为的影响应用程序处理管道. 因此, 在HttpApplication级的缓存是正是通过Global.asax或IHttpModule来完成的.
“动态页面静态化”:
在文章的标题中, 提到过一种称为”动态页面静态化”的技术, 暂且不管这个称谓是否贴切(园子有议论这个的帖子). 无论名称是什么, 我觉得那只是个称谓而已, 关键的是它用来解决什么问题. 早期的搜索引擎并不能很好的收录如*.aspx等的动态页面, 但是对*.html等静态页面收录良好, 于是乎产生了一种UrlRewrite(Url重写)的技术. 另外一种”动态页面静态化”的技术就和今天的缓存挂钩了, 那就是把用户对*.aspx页面的请求结果缓存到html文件中.
第一种: UrlRewrite技术:
它的原理是把用户发出的对静态页面(*.html)的请求转化映射为对动态页面(*.aspx)的请求. UrlRewrite技术主要就是用来解决当时搜索引擎收录的问题, 如今的各大搜索引擎中宣称已经不存在这个问题了. 那么这个技术现今就没用了吗? 在一篇讲Yahoo团队的34条网站优化手段的文章中, 看到一处用UrlWriter的地方, 说的是为了减小客户端浏览器在请求回应过程中传递的数据, 在网站真正实施发布时尽量采用”短名称”, 如: 超链接中用p.html代替product.html、在JS中用d代表window.document等. 这篇文章说短文件名有利于减少传输过程中的数据量, 但是客户体验较差, 这时就可以通过UrlRewrite技术来改善用户体验, 具体做法是在html代码中使用p.html做超链接, 用户请求时依然采用product.html, UrlRewrite的工作就是把请求中的product.html转换映射为p.html. 这和”把*.html映射到*.aspx”是一个道理. 再有一个和缓存相关的用处, 就是url中含有类似*.aspx?id=123的查询字符串时, 很多代理是不会缓存这种请求的, 这时我们可以通过UrlRewrite技术将*.aspx?id=123变成/123.aspx, 从而使代理服务器缓存这种请求.
示例: 静态*html映射到动态页面*.aspx, 因为默认对*.html文件的处理是由IIS来完成的, 因此这里必须在IIS中, Web程序的属性中, “主目录” ---> “应用程序设置” --> “配置”中分别添加*.htm和*.html到aspnet_isapi.dll的配置. 注意: “检查文件是否存在” 的勾一定要去掉, 这时对*.htm和*.html的处理将会交给asp.net处理.
// UrlRewriteDemo/App_Code/ HtmlToAspx.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
namespace HtmlAspxModel
{
/// <summary>
/// Summary description for HtmlToAspx
/// </summary>
public class HtmlToAspx : IHttpModule
{
#region IHttpModule Members
public void Dispose()
{
throw new NotImplementedException();
}
public void Init(HttpApplication application)
{
//更通用的做法是采用IHttpModule来操作HttpApplication管道
//注册事件
application.BeginRequest += new EventHandler(application_BeginRequest);
}
void application_BeginRequest(object sender, EventArgs e)
{
HttpApplication ha = sender as HttpApplication;
HttpContext hc = ha.Context;
string rawurl = ha.Context.Request.RawUrl;
if (rawurl.EndsWith(".htm",StringComparison.OrdinalIgnoreCase)) //判断以*.htm结尾
{
string regnoparam = @"/(\w+)\.htm"; //不含参数
string regparam = @"/(\w+)\=(\d+)"; //含参数
if (System.Text.RegularExpressions.Regex.Match(rawurl, regparam, System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success) //匹配成功
{
System.Text.RegularExpressions.MatchCollection mc = System.Text.RegularExpressions.Regex.Matches(rawurl, regparam, System.Text.RegularExpressions.RegexOptions.IgnoreCase);
rawurl = rawurl.Replace(".htm", "");
for (int i = 0; i < mc.Count; i++)
{
if (i == 0)
{
rawurl = rawurl.Replace(mc[i].Value, ".aspx?"+mc[i].Value.Substring(1,mc[i].Value.Length-1));
}
else
{
rawurl = rawurl.Replace(mc[i].Value, "&" + mc[i].Value.Substring(1, mc[i].Value.Length - 1));
}
}
}
else if (System.Text.RegularExpressions.Regex.Match(rawurl, regnoparam, System.Text.RegularExpressions.RegexOptions.IgnoreCase).Success)
{
rawurl = rawurl.Replace(".htm", ".aspx");
}
hc.RewritePath(rawurl);
}
}
#endregion
}
}
//UrlRewriteDemo/Default.aspx
//UrlRewriteDemo/WebFormNoParam.aspx
//UrlRewriteDemo/WebFormWithParam.aspx
//UrlRewriteDemo/web.config
补充知识 --- 重置Form表单的Action属性:
a. 自定义类RawHtmlForm继承自HtmlForm控件
b. 重写RenderAttribute()方法
c. 在配置文件中, 指明用新控件RawHtmlForm替代HtmlForm控件, 这样在生成form标签时, 自动使用新控件代替旧控件.
第二种: 缓存为Html文件的技术(*.aspx页面的请求结果就是个html页面):
它的原理是客户端请求*.aspx文件, 如果在缓存文件夹中有对应名称的*.html文件, 则将请求地址替换为*.html的文件. 这样就不必走完全部的应用程序处理管道, 响应速度会大大提高; 如果缓存文件夹中没有对应的*.html文件, 则该请求会走完整个应用程序管道, 在应用程序管道中的页面处理部分将*.aspx页面的请求结果返回给客户端同时, 再以流的形式保存(另存)*.aspx请求结果到缓存文件中. 这样做的好处是加快网站的响应速度, 而一个快速响应的网站对提升SEO(Searching Engine Optimization)效果也是有一定帮助的. 这种手法是用对文件的IO操作代替费时的应用程序管道的操作, 对于那些访问量极高的页面, 我们甚至可以把*.aspx页面的请求结果直接保存到内存中, 这样连IO操作也省掉了, 但内存的有限性决定了我们不能够保存太多的页面在内存中, 只能保存那些访问量极高的少量页面.
缓存后替换:
以上我们实现了整页的缓存, 但是缓存完的数据是关于整个页面的, 是没办法修改其内容的. 仍然拿新闻页面来打比方: 如果我想在每个新闻页面中显示下当前日期和时间, 或者我想在每个新闻页面的头部和尾部添加广告, 针对这种大部分内容不变仅有少部分内容发生变化的情况, ASP.NET提供了缓存后替换功能.
缓存后替换功能可以将整个页面进行输出缓存, 但是特定的部分标记为不缓存. Substitution控件指定需要动态创建而不进行缓存的部分, 类似于占位符, 动态生成的内容将输出到Substitution控件所在的位置. ASP.NET为我们提供了3种方式的缓存后替换: a. 以声明的方式使用Substitution控件 b. 以编程的方式使用Substitution控件 c. 使用AdRotator控件.
a. 以声明的方式使用Substitution控件时: Substitution控件调用MethodName属性指定的方法, 该方法提供了在Substitution控件处显示的内容, 并且该方法必须返回字符串类型的结果, 而且必须在Page或UserControl类的代码中包含该控件指定的这个静态方法. MethodName指定的方法并需符合HttpResponseSubstitutionCallback委托的约定(以参数为HttpContext类型), 简言之就是MethodName指的方法, 必须是static、返回string、参数为HttpContext类型的方法.
b. 以编程的方式使用Substitution控件时: 可以将当前页或用户控件后台代码中的静态方法 或 任何对象上的静态方法的方法名, 传递给HttpResponse.WriteSubstitution()方法. 第一次请求该页的时候, WriteSubstitution方法调用HttpResponseSubstitutionCallback委托来产生输出, 之后将替换缓存区中的内容. 该方法会将客户端的缓存能力从public将为server, 从而使浏览器不在缓存而保证能够重新生成静态内容.
c. AdRotator服务器控件: 该服务器控件是在其内部支持缓存后替代功能. 如果将AdRotator控件放在页面上, 则每次请求时都会生成动态的广告. 因此, 包含AdRotator控件的页面仅能在服务器端缓存, 可以通过AdRotator的AdCreated事件对广告内容作高级的处理. 通过AdRotator的AdvertisementFile属性指定储存广告数据的文件, 通常是xml文件. 配置广告信息的xml的格式, 见示例文件XmlAdData.xml.
AdRotator的几个属性释义如下:
ImageUrl: 广告图片地址
NavigateUrl: 点击广告图片的链接地址
AlternateText: 鼠标放在图片上的提示文字
Keyword: 该广告的关键词
Impression: 权重, 广告的显示频率.
示例: 对于新闻类型的网站, 站内的大量文章内容发生改变的频率不是很高, 因此适合将这些文章内容缓存为静态的html页面, 以减少文章内容页重复生成过程的开销.
//CacheHtmlPageDemo/App_code/ CacheHtmlModule.cs --- 若在CacheFile文件夹中, 存在缓存的*.html则返回客户端
//CacheHtmlPageDemo/App_code/HtmlPageBase.cs --- 若不存在缓存的*.html, 则走页面处理管道并将生成的页面保存到CacheFile文件夹中, 我们所要做的只是将页面后台的.cs文件中的类继承成自HtmlPageBase类而非原先的Page类.
//CacheHtmlPageDemo/Default.aspx
//CacheHtmlPageDemo/News1.aspx
//CacheHtmlPageDemo/News3.aspx , 缓存后替换示例, 该页面添加了@OutputCache页面指令, 且后台代码继承自默认的Page类.
//CacheHtmlPageDemo/App_Data/XmlAdData.xml, 为简便而使用xml, 若用数据库也可以, 将XML节点对应表的字段(随便两张图片就行).
// CacheHtmlPageDemo/web.config
使用缓存时, 关于缓存内容的考量:
a. 没有必要缓存用户相关页面, 这会存储很多低点击率的页面
b. 缓存那些频繁请求, 但是内容更新不太频繁或者提供过期内容也影响不大的页面.
c. 生成操作很昂贵的页面, 如: 电子商务网站中的, 分类显示和报表显示页面的生成操作.
d. 内存空间有限, 所以只能缓存那些占用空间小的内容.
e. 对于Postback的响应不应该被缓存, 因为他们依赖于post请求参数.
5. 在页面处理管道或通过IHttpHandler在处理程序中做数据缓存.
说起数据缓存, 肯定跟网站应用程序的数据来源有关了, 需要被缓存的那部分数据当然是放在Cache中. 那么数据本身要么是存放在文件中, 要么是存放在数据库中, 因此对应于这两种数据载体, .net提供了2种依赖机制: 一种是基于文件和目录的CacheDependency, 一种是基于数据库的SqlDependency, SqlDependency是数据库和程序之间的依赖关系, 此外微软还提供了继承自CacheDependency的SqlCacheDependency类, 用来建立数据库和程序缓存之间的依赖关系. 这种缓存依赖解决个什么问题呢, 数据不是已经缓存了吗? 没错, 我们是缓存了数据, 但是如果原始数据发生变化怎么办? 缓存依赖就是当数据发生变化时, 自动清除缓存中数据的这么一种机制, 接下来的下一次请求必然要从新执行读取数据的操作.
缓存依赖执行的操作是删除缓存内容, 这样下一次请求势必会从新获取数据. 这与”缓存后替换”功能是不一样的, 缓存后替换功能是用读取的新数据替换缓存页面的一部分内容.
第一种: 基于文件和目录的CacheDependency是比较简单的, 留意缓存依赖的过期时间、缓存项的优先级、以及删除通知(当被缓存的数据从缓存中移除时激发的事件)即可.
值得注意的地方: 删除通知的回调方法(委托)的回调时机是不可预知的, 很有可能在回调的时候, 页面的生成过程已经完成, 这时候根本没有HttpContext对象, 所以对缓存的操作只能通过HttpRuntime的Cache属性来获得. 而且, 不能在页面上使用实例方法来作为回调方法, 因为回调方法会阻止页面对象的垃圾回收, 内存资源将很快消耗光.
//DataCacheDemo/App_Code/ FileCacheManager.cs
//DataCacheDemo/App_Data/TextFileData.txt, 文字内容随意
//DataCacheDemo/CacheDependency.aspx
//DataCacheDemo/CacheDependency.aspx.cs
第二种: 基于Sql的缓存依赖有两种实现: a. 基于轮询的实现 和 b. 基于通知的实现(仅支持Ms Sql 2005 以上的版本).
- 基于轮询的实现: 就是由 应用程序 每隔一定时间去访问数据库, 查看数据是否发生变化.
注意: 轮询的实际操作, 不可能每次都去查询整个数据库, 轮询机制实际上是通过触发器来维护一张监控表来实现的.
具体操作如下:
创建一张监控信息表, 表的字段主要有两个(被监控的表名, int型字段用来表示数据是否发生变化).
在被监控表上建立触发器, 当表的内容发生变化时, 修改监控表的int字段, 可以通过表的Insert、Delete、Update触发器实现.
对于除Sql Server以外的数据库, 如Oracle、MySql等可采用类似的办法建立轮询机制或通知机制(当然Sql Server也可以这么做). 而Sql Server数据库已经由微软提供了一套轮询实现机制.
(1) . 工具位置: c:\Windows\Microsoft.Net\Framework\v2.0.50727\aspnet_regsql.exe
该工具的参数信息如下(区分大小写):
-S 数据库服务器的名称或ip地址
-E 使用集成验证方式登录数据库
-U 用户名
-P 密码
-d 数据库名称, 如不提供则使用aspnetdb数据库
-ed 为数据库打开Sql缓存依赖支持
-dd 关闭数据库的Sql缓存依赖支持
-et 指定Sql缓存依赖使用的表, 需要使用-t指定表名
-dt 禁用Sql缓存依赖使用的表, 需要使用-t指定表名
-t 指定表名
-lt 列出启用缓存依赖的表名
示例:
启用数据库缓存依赖: aspnet_regsql –S .\MSSQLServer –E –d northwind –ed
启动对被监控表的监控: aspnet_regsql –S .\MSSQLServer –E –d northwind –t calendar –et
列出启用了缓存依赖的表: aspnet_regsql –S .\MSSQLServer –E –d northwind –lt
(2). 运用aspnet_regsql工具配置完数据库后, 还需要在配置文件中增加轮询的设置. sqlCacheDependency enable属性为”true”表示开启抡起机制, polltime设置以毫秒为单位的轮询间隔(>= 500ms, 默认为1分钟).
SqlCacheDependency表示Sql缓存依赖对象, 即支持轮询机制也支持通知机制. SqlCacheDependency scd = new SqlCacheDependency(“配置文件中的数据库那一项的名称, 如northwindCache”, “被监控的表名”);
当构造缓存依赖对象时, 可以通过AggregateCacheDependency创建多个依赖的组合, 在该组合中任一依赖发生变化时将使缓存失效. 组合的缓存依赖内部为一个集合, 例如: 缓存的数据是依赖于Customers和Orders两张表, 我们可以建立2个缓存依赖对象, 并将其添加到一个AggregateCacheDependency对象中, 这时只要任何缓存依赖发生变化, 缓存数据将会失效.
//涉及Sql的相关操作, MsSqlCommand.txt
//DataCacheDemo/App_Code/IDALFactory.cs
//DataCacheDemo/App_Code/SqlDAL.cs
//DataCacheDemo/App_Code/OracleDAL.cs
//DataCacheDemo/App_Code/pollingSqlDependency.aspx
//DataCacheDemo/App_Code/pollingSqlDependency.aspx.cs
//DataCacheDemo/App_Code/web.config
基于Oracle实现轮询操作, 关键点就是对监控表的操作及触发器的编写. 这部分代码我没有写, 但它和Oracle通知机制的缓存依赖很像(见基于通知的缓存依赖Oracle示例). 我说个思路: 首先仍是创建监控表, 之后在被监控表上创建触发器, 对该表做的Insert、Update、Delete操作都会修改监控表(在我的基于通知的缓存依赖Oracle示例中, 我制作了个函数完成添加监控表记录、添加被监控表触发器、以及创建本地文件的操作). 最后, 我们可以再Galobal.asax的Application_Start事件中开辟子线程, 在子线程中读取配置文件中的事件设置, 定期去读取监控表的数据.
再具体点就是, 我们应该事先缓存监控表, 在子线程读取监控表数据后, 比较两个DataTable的内容, 找出发生变化的表的名字. 然后利用索引器查看缓存中是否有以这个表名作key的缓存项, 如果有的话清空这个缓存项, 没有则什么也不做. 大体思路是这样, 代码可以参考Orale的通知机制的实现, 这篇已经写了很久了, 不想再写了.
2. 基于通知的实现: 当数据发生变化时, 由数据库直接通知应用程序. 虽然, 通知机制不需要使用工具做数据库准备工作, 也不需要在配置文件中做任何修改, 但是通知机制只能应用在Ms Sql Server 2005以上的版本中.
具体操作:
使用命令启动数据库通知机制(默认是开启状态, 请务必先检查): alter database 数据库名称 set enable_broker
在Global.asax文件中的Application_Start和Application_End事件中分别设置开启和结束SqlDependency的监听.
通过一个SqlCommand对象来获取数据库查询通知, 该SqlCommand对象必须满足条件, 第一是数据库的表名必须是完全限定名(如: dbo.Region), 第二是Select语句必须显示执行列名, 不能用*、top函数等, 即只能是最简单的Select语句.
构造缓存依赖对象, SqlCacheDependency scd = new SqlCacheDependency(command);
示例:
//涉及Sql的相关操作, OracleCommand.txt.txt
//DataCacheDemo/Global.asax
//DataCacheDemo/App_Code/noticeSqlDependency.aspx.aspx
//DataCacheDemo/App_Code/noticeSqlDependency.aspx.aspx.cs
差点忘了, 还有网站优化:
通常我们面对一个站时, 可以从以下几个方面考虑优化的问题:
a. Ajax, 使用Ajax技术提升客户体验
b. 浏览器缓存(客户端缓存, 缓存后替换)
c. 应用程序处理管道级别的缓存(整页级缓存, 含IIS位置的整页缓存)
d. 控件级缓存(用户控件中使用缓存技术)
e. 数据集缓存(效果较明显)
f. 代码级的优化