ASP.NET站点性能提升-缓存
缓存可以将web页面保存在访问者的浏览器、中间代理和服务器内存中。这样,就不用每次请求都重新生成页面,降低了CPU和数据库的负载。
- 浏览器缓存:保存文件在浏览器缓存中,这样浏览器就不需要下载页面。
- 代理缓存:保存文件在代理服务器上。
- 输出缓存:在web服务器上缓存.aspx页面,这样就不用重新生成页面,降低了CPU和数据库的负载。
- 基于IIS的缓存:使用IIS内置的缓存。
- 数据缓存:缓存独立的对象。
浏览器缓存
服务器可以通过cache-control响应头要求浏览器缓存内容。浏览器缓存有以下优点:
- 快速文件获取:浏览器不需要访问Internet。
- 减少带宽成本和服务器处理负载。
- 可扩展性。流量越大,就有更多的浏览器缓存数据。
浏览器缓存也有缺点:
- 不可预测的生命周期:因为其它网站也使用同样的缓存空间,浏览器可能随时删除你的文件。
- 过期的内容:如果服务器上的文件发生变化,没有任何方法更新浏览器缓存。对于图片、CSS文件,可以使用在文件名中加入版本号的方法解决这个问题。
- 安全风险。因为文件是存储在访问者的计算机中,一个没有经过授权的人只要可以访问计算机,就可以读文件。
OutputCache指令
在aspx页面的顶部加入OutputCache指令开启浏览器缓存。
1 2 | <%@ Page ... %> <%@ OutputCache Duration= "300" Location= "Any" VaryByParam= "None" %> |
这会使得服务器在响应中加入头:
1 2 | Cache-Control: public , max-age=300 Expires: <date and time> |
这告诉浏览器和中间代理可以缓存响应300秒。至于浏览器和中间代理是否这样做,那由浏览器和中间代理决定,没有保证它们会必须这么做。
VaryByParam是必须的,否则会收到编译错误。
Location值包括:
Location值 | 缓存位置 | 使用场景 |
Any | 服务器、浏览器、代理服务器 | 所有情况。除非有以下的列举的理由不这么做。 |
None | 无 | 必须使用文件的最新版本。 |
Client | 浏览器 | 服务器上没有足够的内存或者页面对于不同的访问是不同的。因为安全问题不能使用代理服务器。 |
DownStream | 浏览器、代理服务器 | 服务器上没有足够的内存或者页面对于不同的访问是不同的。代理服务器将页面发送给错误的访问者也没关系。 |
Server | 服务器 | 对浏览器使用的文件的版本完全控制。 |
ServerAndClient | 服务器和浏览器 | 因为安全问题不能使用代理服务器。 |
如果把Location值设为Client或ServerAndClient,就禁止了代理服务器缓存。这会导致以Cache-Control头中加入“private”,告诉代理不要缓存文件:
1 2 | Cache-Control: private , max-age=300 Expires: <date and time> |
代码开启缓存
1 | 开启缓存: |
1 2 | Response.Cache.SetMaxAge( new TimeSpan(0, 5, 0)); Response.Cache.SetCacheability(HttpCacheability.Public); |
关闭服务器缓存:
1 | Response.Cache.SetNoServerCaching(); |
关闭代理服务器缓存:
1 | Response.Cache.SetCacheability(HttpCacheability.Private); |
禁用缓存
Response.Cache.SetCacheability(HttpCacheability.NoCache);
代理缓存
请求和响应在服务器和浏览器间可能通过代理。代理可以缓存内容,可以使用这些内容响应来自其它访问者的请求。
使用代理缓存的优点:
- 如果代理一个用户访问网站时缓存了文件,它就可以使用缓存的文件响应其它不相关的访问者。
- 更快获取文件。浏览器在地理上比web服务器更接近代理。
- 与浏览器缓存相同,减少了web服务器负载、带宽。有更好的扩展性。
缺点:
- 安全风险。代理可能将访问者相关的数据发送给完全不相关的访问者。
- 代理不缓存有查询字符串的文件。
- 不能用于设置cookie的响应。
- 与浏览器缓存相同,代理随时丢弃缓存内容,并且当内容发生改变时,也不能更新代理缓存。
缓存同一页面的不同版本
根据请求头,同一个页面,可能有不同的版本。例如,当接收到一个有如下头的请求,可能发送英文版本:
1 | Accept-Language: en-US |
当接收到一个有如下头的请求,可能发送德文版本:
1 | Accept-Language: de-DE |
解决方案是告诉代理URL和Accept-Language请求头必须都匹配时,才能缓存中发送页面。可以在响应中发送Vary头:
1 | Vary: Accept-Language |
可以在OutputCache指令中包括VaryByHeader让ASP.NET发送那个头:
1 2 | <%@ OutputCache Duration= "300" Location= "Any" VaryByParam= "None" VaryByHeader= "Accept-Language" %> |
也可以使用代码设置:
1 | Response.Cache.VaryByHeaders[ "Accept-Language" ] = true ; |
IIS有一个功能可以压缩响应,以节省带宽和传输时间。然后,当你打开动态文件压缩功能,这样IIS就可以压缩.aspx文件,IIS会覆盖Vary头:
1 | Vary: Accept-Encoding |
这样做是为了保证压缩的内容不会发送到不能解压缩的访问者。所以,如果需要根据头区分响应,并且使用动态压缩,不要使用代理缓存,将Location设置为Client、Server或ServerAndClient。
Cookies
Cookies和代理缓存不能结合的很好。当服务器发送了cookie,响应中就会包括一个Set-Cookie响应头:
1 | Set-Cookie: MyCookie=LanguagePreference=Dutch; path=/ |
从那以后,浏览器就会在以后的每次请求的Cookie请求头中都会返回这人cookie,直到cookie过期:
1 | Cookie: MyCookie=LanguagePreference=Dutch |
当代理缓存页面时,也会缓存响应头。问题是cookies是用户相关的,所以不应该发送给另一个用户。牢记当第一次在session state中存储数据时,ASP.NET会使用session ID设置cookie。当用户成功登录时,ASP.NET也会设置cookie,记住用户。
一些用户不会缓存有Set-Cookie头的页面。但并不能得到保证。
所以,当设置cookie时,不要允许代理缓存。设置Location为Client、Server或ServerAndClient。
从URL中移除查询字符串
如果URL中包含?,即包含查询字符串时,很多代理不会缓存响应。
解决这个问题,例如default.aspx?id=55,可以使用default/55.aspx。如果使用System.Web.Routing命名空间中的类映射请求URL至处理程序,就可以很容易通过更新路由解决它,如果使用ASP.NET MVC也是一样的。否则,可以使用以下两种方法:
- 使用IIS7的URLRewrite扩展。(最容易)
- 使用Global.asax中的RewritePath方法。
IIS7 URLRewrite扩展
URLRewrite扩展允许在web.config中包含重写规则。
首先,安装URL Rewriting模块,下载地址:
http://www.iis.net/download/URLRewrite。
然后,在web.config中加入:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <configuration> ... <system.webServer> <rewrite> <rules> <rule name= "restore query string" > <match url= "(.*)/(\d+)\.aspx$" /> <action type= "Rewrite" url= "{R:1}.aspx?id={R:2}" /> </rule> </rules> </rewrite> </system.webServer> ... </configuration> |
1 | 这个规则匹配所有满足正则表达式(.*)/(\d+)\.aspx$。如果URL匹配,URL重写为{R:1}.aspx?id={R:2}。{R:1}包括第一组(.*)中的内容,{R:2}包括第二组(\d+)中的内容。 |
Global.asax中的RewritePath方法
如果使用IIS6,或者不能安装URLRewrite扩展,可以在Global.asax的Application_BeginRequest中使用RewritePath方法。
在Global.asax中加入代码:
1 2 3 4 5 6 7 8 9 10 | private void Application_BeginRequest( Object source, EventArgs e) { HttpApplication application = (HttpApplication)source; HttpContext context = application.Context; string rawUrl = context.Request.RawUrl; string newUrl = Regex.Replace(rawUrl, @"/(\d+)\.aspx$" , @".aspx?id=$1" ); context.RewritePath(newUrl, false ); } |
重置form action属性
ASP.NET页面post back到它们自己,它们的HTML form标签的action属性设置为页面的URL自身。现在,URL重写成内部格式,ASP.NET也将action属性设置成内部URL。在这里,这不是个问题,当重写代码得到表单的URL:default.aspx?id=55,它不匹配正则表达式。所以它不会修改URL,不会影响表单功能。
但是,如果需要将表单的action属性设置为原来的URL(例如/default/55.aspx),进行以下操作:
- 从HtmlForm继承一个新的控件:RawUrlForm。
- 覆写RenderAttributes方法:12345678910
using
System.Web.UI;
using
System.Web.UI.HtmlControls;
public
class
RawUrlForm : HtmlForm
{
protected
override
void
RenderAttributes(HtmlTextWriter writer)
{
this
.Action = Page.Request.RawUrl;
base
.RenderAttributes(writer);
}
}
- 编辑web.config,告诉ASP.NET使用新控件代替HtmlForm。这样,当它生成form标签时,它就可以自动使用新控件代替HtmlForm,不用修改HTML:123456
<pages>
<tagMapping>
<add tagType=
"System.Web.UI.HtmlControls.HtmlForm"
mappedTagType=
"RawUrlForm"
/>
</tagMapping>
</pages>
输出缓存
输出缓存可以在服务器上缓存整个页面或部分页面。它有这些优点:
- 灵活性:通过查询字符串变量、请求头或自定义变量支持缓存不同版本的文件,
- 允许缓存部分页面。
- 缓存项的生存时间相对来说是可预测的,因为没有其它网站的缓存空间竞争。如果使用共享主机,在同一个web服务器上有多个网站,则不能保证。
输出缓存也有缺点:
- 没有必要缓存用户相关页面,因为这会存储很多低点击率页面。
- 占用服务器内存。
- 不能跨服务器缓存。在服务器园中,每一个服务器都有自己的缓存。在每个服务器缓存中会重复存在,占用更多的内存并且导致不同缓存间的不一致。
- 如果应用程序池回收或web服务器重启,缓存内存消失。
可以通过将缓存放置到单独的缓存服务器上解决后两个缺点。考虑使用Memcached(http://memcached.org)和Windows Server AppFabrid(http://msdn.microsoft.com/en-us/windowsserver/ee695849.aspx)。另外,也可以实现输出缓存提供程序。
缓存什么不缓存什么
因为输出缓存占用内存,所以必须仔细考虑缓存什么,不缓存什么。一般情况下:下列内容是值得缓存的:
- 频繁被请求的内容。这些内容更新不频繁,或者提供了过期的内容也不是大问题。
- 生成操作很昂贵的内容。
- 占用空间很小的内容。
首页是主要的候选者,因为它很可能是最繁忙的页面。在一个电子商务网站,分类页面或报表显示页面也可能需要缓存,因为生成它们很昂贵。
另一方面,用户相关的内容只会被一个用户获取。如果有成千上万个产品页面,缓存所有页面也没有意义。对于postback的响应也不应该被缓存,因为他们依赖于POST请求的值。
启用输出缓存
启用输出缓存和启用浏览器缓存和代理缓存方式相同,在页面顶部加入OutputCache指令。
1 2 | <%@ Page ... %> <%@ OutputCache Duration= "300" Location= "Any" VaryByParam= "None" %> |
这里设置失效时间为300秒。如果页面的静态程度更高,设置更长的时间。
输出缓存示例
1 2 3 4 | <%@ Page Language= "C#" AutoEventWireup= "true" CodeFile="CachedTime. aspx.cs " Inherits=" CachedTime" %> <%@ OutputCache Duration= "10" Location= "Any" VaryByParam= "None" %> <%= DateTime.Now.ToString() %> |
VaryByParam
前面的OutputCache指令都没有考虑将查询字符串。为了解决这个问题,在OutputCache指令中使用VaryByParam告诉ASP.NET根据查询字符串参数id缓存页面的一个版本:
1 | <%@ OutputCache Duration= "10" Location= "Any" VaryByParam= "id" %> |
如果页面使用多个查询字符串参数,使用分号分隔它们:
1 | <%@ OutputCache Duration= "10" Location= "Any" VaryByParam= "id;location" %> |
使用星号根据所有参数缓存,不管它们的名称:
1 | <%@ OutputCache Duration= "10" Location= "Any" VaryByParam= "*" %> |
VaryByHeader
根据请求头存储不同的版本,使用VaryByHeader:
1 | <%@ OutputCache Duration= "10" Location= "Any" VaryByHeader= "Accept-Language" VaryByParam= "None" %> |
Http请求头完整列表:
1 | <a title= "http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html" href= "http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html" rel= "noopener nofollow" >http://www.w3.org/Protocols/HTTP/HTRQ_Headers.html</a> |
VaryByCustom
如何不通过查询字符串参数或请求头缓存页面?假设有一个页面根据工作日进行缓存。
首先在OutputCache指令中加入VaryByCustom属性:
1 | <%@ OutputCache Duration= "300" Location= "Any" VaryByCustom= "weekday" VaryByParam= "None" %> |
1 | 显然,ASP.NET不知道怎么解释weekday。它会调用Global.asax中的GetVaryByCustomString方法: |
1 2 3 4 5 6 7 8 9 10 11 12 | public override string GetVaryByCustomString(HttpContext context, string custom) { if (custom == "weekday" ) { return DateTime.Now.DayOfWeek.ToString(); } else { return base .GetVaryByCustomString(context, custom); } } |
根据浏览器VaryByCustom
因为很多页面有浏览器相关的页面,所以需要根据浏览器类型进行缓存。VaryByCustom能够识别浏览器,不用实现GetVaryByCustomString方法:
1 2 3 | <%@ Page Language= "C#" AutoEventWireup= "true" %> <%@ OutputCache Duration= "300" Location= "Any" VaryByCustom= "browser" VaryByParam= "None" %> <%= Request.UserAgent %> |
它会使用浏览器名和主版本号区分浏览器。
可以混合使用VaryByParam、VaryByHeader和VaryByCustom。但是要注意到底创建了多少个版本,会占用多少空间。
部分缓存
如果只缓存部分页面,将这些部分放到用户控件中,缓存这些控件。通过在用户控件顶部包括OutputCache指令缓存用户控件:
1 2 3 | <%@ Control Language= "C#" %> <%@ OutputCache Duration= "10" VaryByParam= "None" %> Cached user control: <%= DateTime.Now.ToString() %> |
在用户控件的OutputCache指令中不能使用Location。
可以像使用非缓存用户控件一样使用缓存用户控件。然而,如果用户控件向页面暴露了属性,当用户控件缓存时,不能访问这些属性。
如果在不同的页面上使用同一个用户控件,每一个页面都会缓存用户控件。page1.aspx上的用户控件可能和page2.aspx上的同一个用户控件有不同的输出。如果想让不同的页面上用户控件有相同的输出,在OutputCache指令中使用Shared属性,这样用户控件只会缓存一次:
1 | <%@ OutputCache Duration= "300" VaryByParam= "None" Shared= "true" %> |
Post-cache substitution
Post-cache substitution是部分缓存的反面。部分缓存是在非缓存页面缓存部分内容,Post-cache substitution是在缓存页面不缓存部分内容。
使用OutputCache指令缓存页面。
如果希望页面上放置非缓存内容,放置一个substitution控件。当页面生成时或从缓存中获取页面时,会调用一个静态方法。这个静态方法返回一个字符串,插入到Substitution控件的位置。
1 | <asp:Substitution MethodName= "GetSubstitutedTime" runat= "server" /> |
当运行时调用静态方法时,会传入HttpContext,提供请求的信息。不能依靠页面对象、生命周期事件或其它控件,因为当从缓存中获取页面时,这些都不会触发。
1 2 3 4 | private static string GetSubstitutedTime(HttpContext context) { return DateTime.Now.ToString(); } |
如果希望针对每个请求都执行一段代码,例如登录,可以使用Post-cache substitution执行代码。
创建输出缓存提供程序
输出缓存提供程序是从OutputCacheProvider继承的类。在这个类中覆写四个方法:Set,Add,Get和Remove。输出缓存提供程序的框架如下:
1 2 3 4 5 6 7 8 9 10 | using System.Web.Caching; public class FileCacheProvider : OutputCacheProvider { public override void Set( string key, object item, DateTime expiry) { ... } public override object Add( string key, object item, DateTime expiry) { ... } public override object Get( string key) { ... } public override void Remove( string key) { ... } } |
Set
这个方法的任务是给定一个键值,存储一个条目。它也接收一个UTC日期/时间,表示条目失效时间。失效时间需要与条目一同存储,这样当获取条目时,就可以检查它。如果给定键值的条目已经在缓存中已经存在,它的内容和失效时间就要被覆盖。
Get
这个方法只是给定一个键值从缓存中获取条目并返回它的内容。如果条目不存在或者已经失效,Get返回null。
Add
这个方法有点像Set。如果条目在缓存中不存在,存储条目,并且由方法返回。但是,如果缓存中已经存在条目,那么不会进行更新操作,返回缓存中已经存在的条目。
Remove
这个方法根据给定的键值从缓存中移除条目。
使用输出缓存提供程序
修改web.config:
1 2 3 4 5 6 7 8 9 10 11 12 13 | <configuration> <system.web> ... <caching> <outputCache defaultProvider= "FileCache" > <providers> <add name= "FileCache" type= "OutputCacheProviderSample.FileCacheProvider, OutputCacheProviders" /> </providers> </outputCache> </caching> ... </system.web> </configuration> |
通过设置属性defaultProvider为新提供程序的名字,保证ASP.NET默认使用这个新提供程序提供输出缓存。
如果只希望对特定页面或用户控件使用新提供程序。
- 在web.config中配置新提供程序,但默认使用ASP.NET内置的提供程序。
- 对特定页面使用新提供程序。
- 对特定用户控件使用新提供程序。
首先,在web.config中设置默认提供程序为AspNetInternalProvider:
1 2 3 4 5 6 7 | <outputCache defaultProvider= "AspNetInternalProvider" > <providers> <add name= "FileCache" type="OutputCacheProviderSample.FileCacheProvider, OutputCacheProviderSample" /> </providers> </outputCache> |
在Global.asax中覆写GetOutputCacheProviderName方法,根据请求的文件名选择输出缓存提供程序。
1 2 3 4 5 6 7 | public override string GetOutputCacheProviderName(HttpContext context) { if (context.Request.Path.EndsWith( "CachedWeekday.aspx" )) return "FileCache" ; else return base .GetOutputCacheProviderName(context); } |
在用户控件中指定输出缓存提供程序更简单,只要在OutputCache指令中加入ProviderName属性:
1 | <%@ OutputCache Duration= "10" VaryByParam= "None" ProviderName= "FileCache" %> |
内核缓存和IIS 7输出缓存
当使用服务器上的输出缓存时,也可以使用内核缓存和IIS 7输出缓存。IIS 7首先引入输出缓存,但IIS6也已经支持内核缓存。从这些缓存中响应请求要比从ASP.NET输出缓存中快,因为它们是集成在IIS中的。下面是工作原理。
当一个新的请求到达web服务器时,最先由内核驱动http.sys处理。这个驱动首先尝试从内核缓存中响应请求。如果不能,它会将请求发送到IIS的一个监听线程端口。这个IIS线程试图从IIS 7的输出缓存中响应请求。如果不能,将请求转交ASP.NET,这会激活另一个线程处理请求。ASP.NET尝试从它自己的缓存中响应请求,如果不能,再生成输出。结果会发送给第三个线程,再发送给浏览器。
这意味着,如果可以从内核缓存中提供文件,可以节省三个线程切换和切换到用户模式的开销。这使得从内核缓存中提供文件非常快。IIS 7输出缓存需要一个线程切换和切换到用户模式,但仍然要比ASP.NET缓存快。
配置IIS缓存
使用服务器缓存OutputCache指令时,内核缓存和IIS 7输出缓存会启用。但是,如果使用HTTP Handler生成一个PNG图片时,就没办法加OutputCache指令。通过在IIS管理器中配置缓存开启内核缓存和IIS 7输出缓存。
- 打开IIS管理器。
- 选择服务器或站点。
- 双击输出缓存图标。
- 点击添加,打开添加缓存规则对话框。对于两种类型的类型,可以指定被缓存文件的扩展名、失效时间、是否使用文件更改通知。IIS 7输出缓存也允许根据查询字符串变量和HTTP头缓存不同的版本。点击“高级”进行设置。
内存缓存的局限
- 1
不能动态压缩响应。静态压缩是可以的。
- 1
不能缓存默认文档,所以不能缓存站点首页。如果访问http:
//mydomain.com没有键入文件名,不会缓存。如果访问http://mydomain.com/default.aspx,就会被缓存。
- 1
请求不能包括查询字符串。当然,可以使用代理缓存中使用的方法重写URL。但这不会起作用:如果在OutputCache指令中使用VaryByParam或VaryByHeaders,不会缓存。如果不使用VaryByParam,ASP.NET缓存只会存储一个版本,而无论是否有查询字符串。
1 | 其它更多不能缓存响应的情况: |
检查内核缓存的内容
使用命令检查内核缓存内容:
netsh http show cachestate
注意,即使进行了配置,一个响应也不会立即被缓存。只有当达到一定的访问频率也会被缓存,默认阈值是每10秒超过2次。
数据缓存
数据缓存中的目标是在代码中缓存单独的对象,而不是通过页面指令缓存页面或用户控件。它允许在服务器的缓存中保存键值对。同一网站的所有请求都可以访问这些键值对。但和输入缓存一样,如果是由多台服务器缓存的web园,每一台服务器只能访问它自己的缓存。数据缓存是非常灵活的,可以指定失效时间,优先级和依赖项,例如文件和数据库对象。
基本用法
访问缓存就像使用一个字典。最简单的向缓存中加入对象的方法是给一个键名赋值一个对象:
1 | Cache[ "key" ] = myObject; |
获取对象:
1 | MyObject myObject = (MyObject)Cache[ "key" ]; |
当获得一个对象,实际上是获得了缓存中对象的引用。这个对象在所有的处理请求的线程间是共享的。所以要保证对象是线程安全的,或者在获取对象后进行深拷贝。
一个条目不能保证在缓存中,因为由于内存压力,缓存管理器可能移除其中的条目。如果请求的条目不在缓存中,就会得到null。所以从缓存中获取条目的模式如下:
1 2 3 4 5 6 7 | MyObject myObject = (MyObject)Cache[ "key" ]; if (myObject == null ) { myObject = new MyObject(); // Load data into myObject ... Cache[ "key" ] = myObject; } |
1 | 其中一个问题是缓存由多个线程访问。对缓存的单个操作是线程安全,但一个操作序列是不安全的。当一个线程将数据载入到myObject中,另一个线程进入,发现myObject在缓存中找不到,也开始加载数据。所以要使用锁: |
1 2 3 4 5 6 7 8 9 10 11 12 | public Object lockObject = new Object(); ... lock (lockObject) { MyObject myObject = (MyObject)Cache[ "key" ]; if (myObject == null ) { myObject = new MyObject(); // Load data into myObject ... Cache[ "key" ] = myObject; } } |
牢记Cache只是返回缓存条目的一个引用,它可能随时消失,即使代码在运行中。所以避免写这样的代码:
1 2 3 4 5 | // Don't do this MyObject myObject = (MyObject)Cache[ "key" ]; // Lengthy operation ... // Access myObject as retrieved from cache string s = myObject.data; |
失效
ASP.NET允许为缓存条目设置绝对失效时间。也支持相对失效时间:当一个条目没有被获取一段时间后将被移除。
指定绝对失效时间:
1 | using System.Web.Caching; |
1 | Cache.Insert( "key" , myObject, null , DateTime.Now.AddMinutes(30), Cache.NoSlidingExpiration); |
指定相对失效时间:
1 | Cache.Insert( "key" , myObject, null , Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(30)); |
优先级
一些条目的创建操作要比其它对象昂贵。你可以指定一个条目的优先级。当有内存压力时,低优先级的条目会比优先级的条目先移除。
优先级有六级:Low、BelowNormal、Normal、AboveNormal、High和NotRemovable。如果未指定优先级,默认是Normal。
设定优先级:
Cache.Insert("key", expensiveObject, null, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.AboveNormal, null);
文件依赖
当向缓存中插入条目时,可以指定条目依赖其它条目、文件或数据库条目。这样,当这些依赖项改变时,缓存条目会自动移除。这个特性对缓存文件和缓存数据库记录非常有用,因为这将防止缓存条目过期。
指定文件依赖:
1 2 3 | using System.Web.Caching; CacheDependency cd = new CacheDependency(filePath); Cache.Insert(key, fileContent, cd); |
数据库依赖
数据库依赖要比文件依赖复杂。因为,跟踪文件修改很容易,而数据库依赖需要跟踪一张表。为了得到需要依赖的表,需要分析生成数据的查询语句。所以,查询语句就有很多限制。还需要对数据库进行一些配置。
当数据库执行一个可能改变由查询语句返回的数据的命令时,它会通知web服务器,服务器会移除缓存条目。命令不需要来自web服务器,来自任何地方的会影响数据的命令都可以。
显然这有一些额外的开销,所以只缓存不经常改变的数据库项。
SQL Server 2005以上版本支持数据依赖。SQL Server 2000不支持,但是通过一种更复杂的方式可以实现这个功能。
查询语句限制
数据库依赖只能使用查询语句的一个小子集。如果超过这个子集,无论数据是否发生变化,都会移除缓存项。最重要的限制:
- 只能使用单一的SELECT语句或包括单一SELECT语句的存储过程。
- 如果使用存储过程,当创建存储过程时,ANSI_NULLS和QUOTED_IDENTIFIER必须是ON。
- 在存储过程中不要使用SET NOCOUNT ON。也不要使用TRY CATCH或RETURN。
- 不要使用SELECT *。指定所有列名。
- 使用两部分表名,例如dbo.Books。不要在表名中包括数据库名。
- 不要包括计算字段。
- 不要使用聚合表达式。如果使用GROUP BY,COUNT_BIG和SUM是允许的。但SUM不能用在可为空列。不要使用HAVING、CUBE或ROLLUP。
- 不能引用视图、系统表、派生表、临时表或表变量。
- 查询语句中双精度/实型数据类型不能包括比较或表达式。
- 不能使用TOP。
其它的一些限制:
- 不能使用未命名列,或重复列名。
- SELECT语句中做为表达式的列不能出现超过一次。
- 不能使用PIVOT或UNPIVOT操作符。
- 不能使用UNION、INTERSECT或EXCEPT。不能使用DISTINCE、COMPUTE或COMPUTE BY或INTO。
- 不能引用服务器全局变量。
- 不能包含子查询,全连接或自连接。
- 不能使用text、ntext和image类型。
- 不能使用CONTAINS或FREETEXT全文谓词。
- 不能使用数据集函数,包括OPENROWSET和OPENQUERY。
- 不能使用非确定性函数,包括ranking和windowing函数。
- 不能包括FOR BROWSE信息。
- 不能引用队列。
- 不能包括不会改变和不会返回结果集的条件语句(例如,WHERE 1=0)。
- 不能指定READPAST锁提示。
- 不能引用Service Broker队列。
- 不能引用同义词。
一个可以使用的存储过程的示例:
1 2 3 4 5 6 7 8 9 10 11 12 | SET ANSI_NULLS ON GO SET QUOTED_IDENTIFIER ON GO CREATE PROCEDURE GetData AS BEGIN --Do NOT use SET NOCOUNT ON; SELECT [BookId], [Title], [Author], [Price] FROM [dbo].[Book] END GO |
如果需要使用数据库依赖,通过这些步骤实现:
- 开启SQL Server Service Broker。
- 在网站中开启监听服务。
- 在缓存中插入条目时创建依赖。
开启Service Broker
- 重启数据库服务器,保证数据库没有活动的会话,包括来自Visual Studio的会话。
- 执行命令(使用数据库替换TuneUp):12
USE TuneUp
ALTER
DATABASE
TuneUp
SET
ENABLE_BROKER
WITH
ROLLBACK
IMMEDIATE
开启监听服务
在Global.asax中的Application_Start中开启监听服务:
1 2 3 4 5 6 7 8 9 10 11 | <%@ Application Language= "C#" %> <%@ Import Namespace= "System.Data.SqlClient" %> <%@ Import Namespace= "System.Web.Configuration" %> <script runat= "server" > void Application_Start( object sender, EventArgs e) { // Code that runs on application startup string connectionString = ...; SqlDependency.Start(connectionString); } </script> |
创建依赖
1 2 3 4 5 6 7 8 9 10 | using (SqlCommand cmd = new SqlCommand(sql, connection)) { using (SqlDataAdapter adapter = new SqlDataAdapter(cmd)) { SqlCacheDependency cd = new SqlCacheDependency(cmd); dataSet = new DataSet(); adapter.Fill(dataSet); Cache.Insert( "key" , dataSet, cd); } } |
条目移除回调方法
可以指定一个方法,当一个条目过期时,由ASP.NET调用。
1 2 3 4 5 6 7 8 | private void ItemRemovedCallback( string itemKey, object itemValue, CacheItemRemovedReason cacheItemRemovedReason) { ... } Cache.Insert( "key" , expensiveObject, null , Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Default, new CacheItemRemovedCallback(ItemRemovedCallback)); |
CacheItemRemovedReason的可能值:
值 | 描述 |
Removed | 通过调用Cache.Remove移除或通过调用Cache.Insert替换条目。 |
Expired | 因为绝对或相对失效时间到达而失效。 |
Underused | ASP.NET释放内存而移除。 |
DependencyChanged | 因为关联的依赖发生改变而移除。 |
注意事项
- 条目可能独立于请求失效。当条目移除回调方法运行时,不能保存存在请求上下文,所以HttpContext.Current可能为null。所以,如果需要在回调方法中访问缓存,使用HttpRuntime.Cache,不要使用HttpContext.Current.Cache。
- 回调方法应该是静态的。如果是一个对象的实例方法,垃圾收集器不会清除这个对象,因为缓存条目对它有引用。
- 回调方法不仅在条目从缓存移除时执行,当条目被替换时也会执行。替换时,老的条目首先被移除,然后新的条目被插入。
服务器缓存的正确使用
使用ASP.NET提供的缓存相关计数器可以查看缓存的使用情况。如果使用输出缓存:
分类:ASP.NET Applications
Output Cache Hit Ratio:从输出缓存中提供服务的请求比率。
Output Cache Hits:输出缓存命中总数。
Output Cache Misses:输出缓存未命中总数。
Output Cache Entries:输出缓存中的条目数量。
Output Cache Turnover Rate:每秒输出缓存添加和移除的数量。
如果使用数据缓存:
分类:ASP.NET Applications
Cache API Hit Ratio:从数据缓存中提供服务的请求比率。
Cache API Hits:数据缓存命中总数。
Cache API Misses:数据缓存未命中总数。
Cache API Entries:数据缓存中的条目数量。
Cache API Turnover Rate:每秒数据缓存添加和移除的数量。
当解释这些数字时,注意:
- 当有足够的内存时,期望命中率在80%以上。
- 缓存中条目的数量越多,就需要越多的内存取得期望的命中率,就越有可能造成内存不足。
- 每秒数据缓存添加和移除的数量越多,重新生成数据需要的资源越多。
更多资料
- ASP.NET Caching at http://msdn.microsoft.com/en-us/library/xsbfdd8c.aspx。
- ASP.NET Caching: Techniques and Best Practices at http://msdn.microsoft.com/en-us/library/aa478965.aspx。
- Top 10 Performance Improvements in IIS 7.0 at http://technet.microsoft.com/en-us/magazine/2008.09.iis.aspx#id0110054。
- The “Reluctant Cache” Pattern at http://weblogs.asp.net/gavinjoyce/pages/The-Reluctant-Cache-Pattern.aspx。
- HTTP 1/1 definition, section 14 Header Field Definitions at http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html。
- ASP.NET Routing… Goodbye URL rewriting at http://chriscavanagh.wordpress.com/2008/03/11/aspnet-routing-goodbye-url-rewriting/。
- Configuring Output Caching in IIS 7 at http://technet.microsoft.com/en-us/library/cc732475(WS.10).aspx。
- Extensible Output Caching with ASP.NET 4 (VS 2010 and .NET 4.0 Series) at http://weblogs.asp.net/scottgu/archive/2010/01/27/extensible-output-caching-with-asp-net-4-vs-2010-and-net-4-0-series.aspx.
- OutputCacheProvider Class at http://msdn.microsoft.com/en-us/library/system.web.caching.outputcacheprovider.aspx。
- OutputCache Element for caching at http://msdn.microsoft.com/en-us/library/ms228124.aspx。
- Response caching in IIS 7 at http://blogs.iis.net/ksingla/archive/2006/11/16/caching-in-iis7.aspx。
- Walkthrough: IIS 7 Output Caching at http://learn.iis.net/page.aspx/154/walkthrough-iis-70-output-caching/。
- Caching<caching> at http://www.iis.net/ConfigReference/system.webServer/caching。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 如何编写易于单元测试的代码
· 10年+ .NET Coder 心语,封装的思维:从隐藏、稳定开始理解其本质意义
· .NET Core 中如何实现缓存的预热?
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 10年+ .NET Coder 心语 ── 封装的思维:从隐藏、稳定开始理解其本质意义
· 地球OL攻略 —— 某应届生求职总结
· 提示词工程——AI应用必不可少的技术
· Open-Sora 2.0 重磅开源!
· 周边上新:园子的第一款马克杯温暖上架