ASP.NET缓存解决方案和最佳实践
ASP.NET缓存解决方案和最佳实践 1、概述 在ASP.NET应用程序构建过程中,为了提高应用程序的性能,缓存处理无疑是一个非常重要的环节。通常,我们将一些频繁被访问的数据,以及一些需要大量处理时间得出来的数据缓存在内存中,从而提高性能。例如,如果程序需要处理一张报表,这张报表的数据是关联的几张数据库表,并通过大量的计算得到的数据。我们知道表关联是比较耗时的,如果关联之后得出的数据再进行聚合排序等操作的话,那速度会更慢。因此,我们把查询的报表数据缓存起来,等下次用户再次请求时直接从内存中读取已经生成好的报表,这样对用户和程序无疑都是一件非常好的事情,用户减少了等待时间,程序减轻了压力。 那么,何乐而不为呢,既然能让大家都开心的事情我们就去做吧。为此,ASP.NET提供了两种缓存方案。第一种是页输出缓存,它保存页处理输出,并在用户再次请求该页时,重用所保存的输出,而不是再次处理该页。第二种是应用程序缓存,它允许缓存您生成的数据,比如自定义报表对象,DataSet,DataTable等。但是有个问题就是ASP.NET为我们提供的缓存方案只能应用在单服务器中,如果我们的应用程序有几台服务器做负载均衡,或者我们做分布式应用,那么,ASP.NET为我们提供的缓存解决方案发挥的作用就不大了,我们需要其他的解决方案,现在比较成熟的缓存框架有Memcached,此框架用于分布式系统中,适用于Java,ASP.NET,PHP,Ruby等语言环境构建的应用程序。 那么,下面就一一阐述以上提到的缓存方案。 2、页输出缓存 2.1、页面级输出缓存 实现 具体的实现就非常简单了,只要页面顶部加一条OutputCache指令就可以了。 <%@ OutputCache Duration="10" VaryByParam="none" %> 它支持五个属性(Duration,VaryByParam,Location,VaryByCustom,VaryByHeader),有两个(Duration,VaryByParam)是必须的,我们也就研究这两个属性就可以了,也基本够我们日常使用。 l Duration:页面应该被缓存的时间,以秒为单位。必须是正整数。 l VaryByParam :Request 中变量的名称,这些变量名应该产生单独的缓存条目。"none" 表示没有变动。"*" 可用于为每个不同的变量数组创建新的缓存条目。变量之间用 ";" 进行分隔。 l Location :指定应该对输出进行缓存的位置。如果要指定该参数,则必须是下列选项之一:Any、Client、Downstream、None、Server 或 ServerAndClient。 l VaryByHeader :基于指定的标头中的变动改变缓存条目。 l VaryByCustom :允许在 global.asax 中指定自定义变动(例如,"Browser")。 示例 <1>.在Visual Studio .NET新建一个web项目,并且新建一个.aspx页面 <2>.删除页面的上面的默认HTML代码 <3>.把下面的代码COPY到刚新建的那个页面中 <%@ OutputCache Duration="10" VaryByParam="none"%> <html> <head runat="server"> <title>页面输出缓存示例</title> <script type="text/C#" runat="server"> void Page_Load(object sender, EventArgs e) { this.lblTime.Text = "Time:" + DateTime.Now.ToString(); } </script> </head> <body> <strong>页面输出缓存示例</strong> <hr /> <br /> <asp:Label ID="lblTime" runat="server"></asp:Label> <br /> <hr /> <a href="opc.aspx?categoryid=test1">categoryid(test1)</a> <br /> <a href="opc.aspx?categoryid=test2">categoryid(test1)</a> </body> </html> <4>.在浏览器中浏览此页面,您会页面上面的Time会有10秒的缓存,每过10秒,Time会变化一次,这时就是Duration="10"属性在起作用,因为我设置了缓存时间为10秒。好的,我们已经测试了Duration="10"属性。 <5>.我们点击下面的categoryid(test1)和categoryid(test2)两个链接,发现Time是一样的,为什么呢?那是因为我们设置VaryByParam属性为none,我们之前解释过VaryByParam属性为none表示没有变动,意为保存一个缓存,适用于页面只有一个缓存的情况。那我们现在这样一个情况,有一个产品列表数据,其数据是根据产品的分类来决定显示哪些产品,所以我们这里的关键问题是为每个分类产品分别产生缓存。这时就需要用到VaryByParam属性了,它的用途我们已经知道了,现在我们把它的属性设置为categoryid,现在再试试分别点击两个链接,你就会看到两个链接的页面缓存不一样了。 实战友情提示: l 切记,Duration 是用秒进行指定的。 l 在使用 VaryByParam时,一定要注意 Request 变量大小写的变化会导致额外的缓存。比如刚才示例中categoryid=test1和categoryid=Test1会产生两个缓存版本,这里应用时要注意。 2.2、片段缓存(用户控件缓存) 实现 片段缓存的使用语法和页面输出缓存基本一样,但其应用于用户控件(.ascx文件),而页面输出缓存是应用于页面(.aspx文件)。对于其属性,它支持页面级输出缓存(除了Location属性)所有属性。并且用户控件还支持VaryByControl属性,该属性将根据用户控件(通常为用户控件页面上的控件,比如dropdownlist)成员的值改变而改变该控件的缓存。如果指定了VaryByControl,可以省略VaryByParam。 在默认情况下,对每一个页面上面引用的每个用户控件都是单独缓存的。如果一个用户控件不随应用程序中的页面改变而改变,并且在所有的页面中使用相同的名称(ID相同),且使用了Shared="true"参数,那么所有引用该用户控件的缓存版本都是一样的。 示例 <1>. 借用页面级输出缓存建立的WEB项目,新建一个用户控件(.ascx文件) <2>. 把以下代码COPY到刚才新建的.ascx的页面文件中 <%@ OutputCache Duration="10" VaryByControl="ddlcity" Shared="true" %> <script type="text/C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { this.lblUCTime.Text = "usercontrol time:"+ DateTime.Now.ToString(); } </script> <asp:DropDownList ID="ddlcity" runat="server" AutoPostBack="True"> <asp:ListItem Value="1">北京</asp:ListItem> <asp:ListItem Value="2">江苏</asp:ListItem> <asp:ListItem Value="3">上海</asp:ListItem> <asp:ListItem Value="3">南京</asp:ListItem> </asp:DropDownList> <br /> <hr /> <br /> <asp:Label ID="lblUCTime" runat="server"></asp:Label> <br /> <3>. 新建一个.aspx页面,并删除页面文件中的HTML代码,把以下代码复制到页面文件中(注意顶部.cs文件引用别删除,有下划线的需要您替换) <%@ Register Src="您的用户控件.ascx" TagName="ucc" TagPrefix="uc1" %> <html > <head runat="server"> <title>片段缓存示例</title> <script type="text/C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { this.lblSelfTime.Text = "self time:" + DateTime.Now.ToString(); } </script> </head> <body> <form id="form1" runat="server"> <strong>片段缓存示例</strong> <br /> <hr /> <uc1:ucc ID="Ucc1" runat="server" /> <br /> <hr /> <asp:Label ID="lblSelfTime" runat="server"></asp:Label> </form> </body> </html> <4>. 再新建一个.aspx文件,并删除页面文件中的HTML代码,把以下代码复制到页面文件中(注意顶部.cs文件引用别删除,有下划线的需要您替换) <%@ Register Src="您的用户控件.ascx" TagName="ucc" TagPrefix="uc1" %> <html > <head id="Head1" runat="server"> <title>片段缓存示例2</title> <script type="text/C#" runat="server"> protected void Page_Load(object sender, EventArgs e) { this.lblSelfTime.Text = "self 2 time:" + DateTime.Now.ToString(); } </script> </head> <body> <form id="form1" runat="server"> <strong>片段缓存示例2</strong> <br /> <hr /> <uc1:ucc ID="Ucc1" runat="server" /> <br /> <hr /> <asp:Label ID="lblSelfTime" runat="server"></asp:Label> </form> </body> </html> <5>. 在浏览器中浏览您刚才新建的两个.aspx页面,会发现两个页面的用户控件的缓存是一样的(usercontrol time 是同时变化的),当您选择城市列表时,会发现用户控件为每一个城市都缓存了一个版本。不同页面的每个城市的缓存版本一样。 如果需要每个页面缓存版本不一样,就不要设置Shared="true"参数。大家可以通过上面的示例自己测试测试。 实战友情提示: l 如果想每个页面引用的用户控件的缓存版本一样,就必须设置Shared="true"参数,并且用户控件ID一样 3、缓存后替换 页面后台代码: 如上代码所示,Substitution控件有一个重要属性:MethodName。该属性用于获取或者设置当Substitution控件执行时为回调而调用的方法名称。该方法比较特殊,必须符合以下3条标准:
实现 Cache对象位于System.Web.Caching. Cache中,其提供了两种增加缓存的方法,Add()和Insert()方法,这两种方法都有多个重载,且两种方法唯一的区别就是Add()返回已缓存对象的引用,Insert()没有返回值。Cache对象还提供了删除缓存的Remove()方法。 具体的缓存实践我这里提供了简易封装后一个缓存工具类。可以直接用于项目中(适用于ASP.NET 2.0项目)。 using System; using System.Text; using System.Web.Caching; using System.Collections; using System.Collections.Generic; using System.Text.RegularExpressions; namespace DianPing001.Cache { public static class ObjectCache { private static System.Web.Caching.Cache cache; private static double _SaveTime; /// <summary> /// 缓存保存时间,以分钟计算,默认分钟 /// </summary> public static double SaveTime { get { return _SaveTime; } set { _SaveTime = value; } } static ObjectCache() { cache = System.Web.HttpContext.Current.Cache; _SaveTime = 30.0; } /// <summary> /// 获取缓存对象 /// </summary> /// <param name="key">key</param> /// <returns>object</returns> public static object Get(string key) { return cache.Get(key); } /// <summary> /// 获取缓存数据,需要传入类型 /// </summary> public static T Get<T>(string key) { object obj = Get(key); if (obj == null) { return default(T); } else { return (T)obj; } } /// <summary> /// 插入对象到缓存中 /// </summary> /// <param name="key">key</param> /// <param name="value">对象</param> /// <param name="dependency">对象依赖</param> /// <param name="priority">优先级</param> /// <param name="callback">缓存删除时的回调事件</param> public static void Insert(string key, object value, CacheDependency dependency, CacheItemPriority priority, CacheItemRemovedCallback callback) { cache.Insert(key, value, dependency, System.Web.Caching.Cache.NoAbsoluteExpiration, TimeSpan.FromMinutes(SaveTime), priority, callback); } /// <summary> /// 插入对象到缓存中 /// </summary> /// <param name="key">key</param> /// <param name="value">对象</param> /// <param name="dependency">对象依赖</param> /// <param name="callback">缓存删除时的回调事件</param> public static void Insert(string key, object value, CacheDependency dependency, CacheItemRemovedCallback callback) { Insert(key, value, dependency, CacheItemPriority.Default, callback); } /// <summary> /// 插入对象到缓存中 /// </summary> /// <param name="key">key</param> /// <param name="value">对象</param> /// <param name="dependency">对象依赖</param> public static void Insert(string key, object value, CacheDependency dependency) { Insert(key, value, dependency, CacheItemPriority.Default, null); } /// <summary> /// 插入对象到缓存中 /// </summary> /// <param name="key">key</param> /// <param name="value">对象</param> public static void Insert(string key, object value) { Insert(key, value, null, CacheItemPriority.Default, null); } /// <summary> /// 获取所有缓存对象的key /// </summary> /// <returns>返回一个IList对象</returns> public static IList<string> GetKeys() { List<string> keys = new List<string>(); IDictionaryEnumerator cacheItem = cache.GetEnumerator(); while (cacheItem.MoveNext()) { keys.Add(cacheItem.Key.ToString()); } return keys.AsReadOnly(); } /// <summary> /// 删除缓存对象 /// </summary> /// <param name="key">key</param> public static void Remove(string key) { cache.Remove(key); } /// <summary> /// 删除全部缓存 /// </summary> public static void RemoveAll() { IList<string> keys = GetKeys(); foreach (string key in keys) { cache.Remove(key); } } public static IList<string> RegexSearch(string pattern) { List<string> keys = new List<string>(); IDictionaryEnumerator cacheItem = cache.GetEnumerator(); while (cacheItem.MoveNext()) { if (Regex.IsMatch(cacheItem.Key.ToString(), pattern)) { keys.Add(cacheItem.Key.ToString()); } } return keys.AsReadOnly(); } /// <summary> /// 删除符合正则条件的cache /// </summary> /// <param name="pattern">条件</param> public static void RegexRemove(string pattern) { IList<string> keys = RegexSearch(pattern); foreach (string key in keys) { cache.Remove(key); } } } } 具体的使用场景 l 添加缓存 /// <summary> /// 获取全部友情链接 /// </summary> /// <returns></returns> public List<Links> GetAll() { List<Links> linksList = ObjectCache.Get<List<Links>>("c_Links_ALL"); [stone1] if (linksList == null) { linksList = ProviderManager.Factory.Links.GetAll(); ObjectCache.Insert("c_Links_ALL",linksList); } [stone2] return linksList; } l 删除缓存 /// <summary> /// 删除友情链接 /// </summary> /// <param name="id"></param> /// <returns></returns> public int Delete(int id) { int retVar = ProviderManager.Factory.Links.Delete(id); if (retVar > 0) { ObjectCache.RegexRemove("c_Links*");[stone3] } return retVar; } 到这里,已经介绍了ASP.NET为我们提供的缓存方案,从简单的页面级和用户控件缓存,到功能强大、可灵活定制的Cache对象。这些已经基本满足我们日常的需求。当然,缓存的强大之处还需要我们在实战中慢慢体会。 4、分布式缓存 1) Memcached是什么? memcached 是以LiveJournal 旗下Danga Interactive 公司的Brad Fitzpatric 为首开发的一款软件。许多Web应用都将数据保存到RDBMS中,应用服务器从中读取数据并在浏览器中显示。但随着数据量的增大、访问的集中,就会出现RDBMS的负担加重、数据库响应恶化、网站显示延迟等重大影响。 这时就该memcached大显身手了。memcached是高性能的分布式内存缓存服务器。一般的使用目的是,通过缓存数据库查询结果,减少数据库访问次数,以提高动态Web应用的速度、提高可扩展性。 2) Memcached能缓存什么? 通过在内存里维护一个统一的巨大的hash表,Memcached能够用来存储各种格式的数据,包括图像、视频、文件以及数据库检索的结果等。 3) Memcached快吗? 非常快,必须要介绍它的内部实现原理,只要知道有哪些站点在应用就可以了。Memcached已经成为mixi、hatena、Facebook、Vox、LiveJournal等众多服务中提高Web应用扩展性的重要因素。所以我们应该有理由相信Memcached的性能。 4) Memcached特点 memcached作为高速运行的分布式缓存服务器,具有以下的特点。 • 协议简单 • 基于libevent的事件处理 • 内置内存存储方式 • memcached不互相通信的分布式 5) Windows下Memcached的安装使用 a) 安装Memcached Server u 下载memcached的windows稳定版,解压放某个盘下面,比如在d:\memcached u 在CMD下输入 "d:\memcached\memcached.exe -d install" 安装. u 再输入:"d:\memcached\memcached.exe -d start" 启动。 备注:以后memcached将作为windows的一个服务每次开机时自动启动。这样服务器端已经安装完毕了。有几台缓存机器就为这些机器分别安装Memcached服务。 安装常用设置: -p <num> 监听的端口 -l <ip_addr> 连接的IP地址, 默认是本机 -d start 启动memcached服务 -d restart 重起memcached服务 -d stop|shutdown 关闭正在运行的memcached服务 -d install 安装memcached服务 -d uninstall 卸载memcached服务 -u <username> 以<username>的身份运行 (仅在以root运行的时候有效) -m <num> 最大内存使用,单位MB。默认64MB -M 内存耗尽时返回错误,而不是删除项 -c <num> 最大同时连接数,默认是1024 -f <factor> 块大小增长因子,默认是1.25 -n <bytes> 最小分配空间,key+value+flags默认是48 -h 显示帮助 b) 使用Memcached 的.NET客户端 u 下载Memcached的.NET客户端(C#) u 在项目中引用Enyim.Caching.dll文件 u 添加配置文件,WEB项目为web.config,客服端软件项目为App.config,配置代码为 <configuration> <configSections> <sectionGroup name="enyim.com"> <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" /> </sectionGroup> <section name="memcached" type="Enyim.Caching.Configuration.MemcachedClientSection, Enyim.Caching" /> </configSections> <enyim.com> <memcached> <servers> <!-- put your own server(s) here--> <add address="127.0.0.1" port="11211" /> <add address="192.168.111.189" port="11212" /> </servers> <socketPool minPoolSize="10" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" /> </memcached> </enyim.com> <memcached keyTransformer="Enyim.Caching.TigerHashTransformer, Enyim.Caching"> <servers> <add address="127.0.0.1" port="11211" /> <add address="192.168.111.189" port="11212" /> </servers> <socketPool minPoolSize="2" maxPoolSize="100" connectionTimeout="00:00:10" deadTimeout="00:02:00" /> </memcached> </configuration> u 测试代码 using System; using System.Collections.Generic; using System.Text; using Enyim.Caching; using Enyim.Caching.Memcached; using Enyim.Caching.Configuration; using System.Net; namespace TestMemcached1 { class Program { static void Main(string[] args) { string flag = "1"; //输入TAG,为插入缓存,为读取缓存数据,为删除缓存数据 string key = ""; string value = ""; MemcachedClient mc = new MemcachedClient(); Console.WriteLine("请输入操作类型(1插入缓存,读取缓存数据,删除缓存数据)..."); while((flag = Console.ReadLine().Trim()) != "") { switch (flag) { case "1": { Console.WriteLine("请输入要插入缓存的KEY:"); key = Console.ReadLine().Trim(); Console.WriteLine("请输入与KEY对应的值:"); value = Console.ReadLine().Trim(); if (mc.Store(StoreMode.Set, key, value)) { Console.WriteLine("{0}的值({1})插入成功",key,value); } }; break; case "2": { Console.WriteLine("请输入要删除的缓存的KEY"); key = Console.ReadLine().Trim(); if (mc.Get(key) == null) { Console.WriteLine("SORRY,{0}的值不存在", key); } else { if (mc.Remove(key)) { Console.WriteLine("删除缓存({0})成功", key); } else { Console.WriteLine("删除缓存({0})失败", key); } } }; break; case "0": { Console.WriteLine("请输入要读取缓存数据的KEY"); key = Console.ReadLine().Trim(); if (mc.Get(key) == null) { Console.WriteLine("SORRY,{0}的值不存在", key); } else { Console.WriteLine("{0}的缓存数据为{1}",key,mc.Get(key)); } }; break; default: Console.WriteLine("谢谢使用"); break; } } } } } c) 运行结果 我是配置了两台服务器,本机和局域网内的一台机器,从配置文件中也可以看出具体配置了几台机器。 其运行结果也是非常让人满意的,我在本机添加的缓存,在192.168.111.189那台机器上面可以查询到刚刚添加的缓存,同样在189机器添加的缓存,我本机同样可以查询,当然删除也是同步的。 d) 查看Memcached运行情况 使用telnet IP 端口 然后使用stats命令查看Memcached运行情况 e) 实战友情提示: Memcached在修改服务端口时发现CMD下的修改命令并不起效果。后来发现在安装好的Memcached服务的启动项中并没有端口设置(默认值为11211),于是想到进注册表修改其服务启动参数。打开注册表,按照路径HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\memcached Server,找到其中的ImagePath项,其值为:"d:\memcached\memcached.exe" -d runservice,将值修改为:"d:\memcached\memcached.exe" -p 80080 -m 256 -d runservice。重启服务后你就会发现该服务的端口已经变为80080了,-m 256表示设置了256M内存。 5、小结 从最基本的页面级和用户控件级简单缓存,到高灵活性、高性能的Cache缓存对象,以及功能强大、性能卓越的分布式缓存系统Memcached,我们都已经有所了解、有所深入。那么在实践过程中,我们应该根据自己的实际需要去选择具体的缓存方案。比如单服务器中,我们可以选择Cached缓存对象实现缓存,一些长期不 |