谈谈MVC项目中的缓存功能设计的相关问题

今天这一篇文章我来谈一谈在MVC 3项目中的缓存功能,以及针对缓存的一些设计上的考量,给你们参考参考html

前言

为何须要讨论缓存?缓存是一个中大型系统所必须考虑的问题。为了不每次请求都去访问后台的资源(例如数据库),咱们通常会考虑将一些更新不是很频繁的,能够重用的数据,经过必定的方式临时地保存起来,后续的请求根据状况能够直接访问这些保存起来的数据。这种机制就是所谓的缓存机制。web

根据缓存的位置不一样,能够区分为:sql

  1. 客户端缓存(缓存在用户的客户端,例如浏览器中)
  2. 服务器缓存(缓存在服务器中,能够缓存在内存中,也能够缓存在文件里,而且还能够进一步地区分为本地缓存和分布式缓存两种)

应该说,缓存的设计是一门较为复杂的学问,主要考虑的问题包括数据库

  1. 要不要缓存?
  2. 要缓存哪些数据?
  3. 要缓存多少数据?
  4. 要缓存多久?
  5. 如何更新缓存(手动仍是自动)
  6. 将缓存放在哪里?

本文将以较为通俗易懂的方式,来看一看在MVC3的项目中,如何使用缓存功能。对于上述提到的一些具体业务问题,我这里不会进行太过深刻地探讨。浏览器

 

MVC3中的缓存功能

ASP.NET MVC3 继承了ASP.NET的优良传统,内置提供了缓存功能支持。主要表现为以下几个方面缓存

  • 能够直接在Controller,Action或者ChildAction上面定义输出缓存(这个作法至关于原先的页面缓存和控件缓存功能)服务器

  • 支持经过CacheProfile的方式,灵活定义缓存的设置(新功能)mvc

  • 支持缓存依赖,以便当外部资源发生变化时获得通知,而且更新缓存负载均衡

  • 支持使用缓存API,还支持一些第三方的缓存方案(例如分布式缓存)框架

那么,下面咱们就逐一来了解一下吧

0.范例准备

我准备了一个空白的MVC 3项目,里面建立好了一个Model类型:Employee

using System;
using System.Collections.Generic; using System.Linq; using System.Web; namespace MvcApplicationCacheSample.Models { public class Employee { public int ID { get; set; } public string Name { get; set; } public string Gender { get; set; } } }

 

而后,我还准备了一个HomeController

using System;
using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using MvcApplicationCacheSample.Models; namespace MvcApplicationCacheSample.Controllers { public class HomeController : Controller { // // GET: /Home/ public ActionResult Index() { //这里目前做为演示,是直接硬编码,实际上多是读取数据库的数据 var employees = new[]{ new Employee(){ID=1,Name="ares",Gender="Male"} }; return View(employees); } } } 

同时,为这个Action生成了一个View

@model IEnumerable<MvcApplicationCacheSample.Models.Employee>

@{
    ViewBag.Title = "Index";
}

<h2>Index</h2>

<p>
    @Html.ActionLink("Create New", "Create") </p> <table> <tr> <th> Name </th> <th> Gender </th> <th></th> </tr> @foreach (var item in Model) { <tr> <td> @Html.DisplayFor(modelItem => item.Name) </td> <td> @Html.DisplayFor(modelItem => item.Gender) </td> <td> @Html.ActionLink("Edit", "Edit", new { id=item.ID }) | @Html.ActionLink("Details", "Details", new { id=item.ID }) | @Html.ActionLink("Delete", "Delete", new { id=item.ID }) </td> </tr> } </table> 

因此,当前的应用程序运行起来看到的效果大体是下面这样的

image

这个例子很简单,没有太多须要解释的。

 

1.使用输出缓存

那么,如今咱们假设这个读取员工的数据很频繁,可是数据又更新不是很频繁,咱们就会想到,能不能对这部分数据进行缓存,以便减小每次执行的时间。

是的,咱们能够这么作,并且也很容易作到这一点。MVC中内置了一个OutputCache的ActionFilter,咱们能够将它应用在某个Action或者ChildAction上面

【备注】ChildAction是MVC3的一个新概念,本质上就是一个Action,但一般都是返回一个PartialView。一般这类Action,能够加上一个ChildActionOnly的ActionFilter以标识它只能做为Child被请求,而不能直接经过地址请求。

【备注】咱们确实能够在Controller级别定义输出缓存,但我不建议这么作。缓存是要通过考虑的,而不是无论三七二十一就所有缓存起来。缓存不当所形成的问题可能比没有缓存还要大。

 

下面的代码启用了Index这个Action的缓存功能,咱们让他缓存10秒钟。

using System;
using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using MvcApplicationCacheSample.Models; namespace MvcApplicationCacheSample.Controllers { public class HomeController : Controller { // // GET: /Home/  [OutputCache(Duration=10)] public ActionResult Index() { //这里目前做为演示,是直接硬编码,实际上多是读取数据库的数据 var employees = new[]{ new Employee(){ID=1,Name="ares",Gender="Male"} }; return View(employees); } } } 

 

那么,也就是说,第一次请求这个Index的时候,里面的代码会执行,而且结果会被缓存起来,而后在10秒钟内,第二个或者后续的请求,就不须要再次执行,而是直接将结果返回给用户便可。

这个OutputCache的Attribute,其实是一个ActionFilter,它有不少参数,具体的请参考

http://msdn.microsoft.com/zh-cn/library/system.web.mvc.outputcacheattribute.aspx

这些参数中,Duration是必须的,这是设置一个过时时间,以秒为单位,这个我想你们都很好理解。我重点要一下下面几个

  • VaryByContentEncoding
    VaryByCustom
    VaryByHeader
    VaryByParam

这四个参数的意思是,决定缓存中如何区分不一样请求,就是说,哪些因素将决定使用仍是不使用缓存。默认状况下,若是不作任何设置,那么在规定的时间内(咱们称为缓存期间),全部用户,无论用什么方式来访问,都是直接读取缓存。

VaryByParam,能够根据用户请求的参数来决定是否读取缓存。这个参数主要指的就是QueryString。例如

若是咱们缓存了http://localhost/Home/Index,那么用这个地址来访问的时候,规定时间内都是读取缓存。但若是用http://localhost/Home/Index?name=chenxizhang这样的地址过来访问,显然咱们但愿不要读取缓存,由于参数不同了。要实现这样的需求,也就是说,但愿根据name参数的不一样缓存不一样的数据。则能够设置VaryByParam=”name”。

若是有多个参数的话,能够用逗号分开他们。例如 VaryByParam=”name,Id”

    【备注】这里其实会有一个潜在的风险,因为针对不一样的参数(以及他们的组合)须要缓存不一样的数据版本,假设有一个恶意的程序,分别用不一样的参数发起大量的请求,那么就会致使缓存爆炸的状况,极端状况下,会致使服务器出现问题。(固然,IIS里面,若是发现缓存的内容不够用了,会自动将一些数据清理掉,但这就一样致使了程序的不稳定性,由于某些正常须要用的缓存可能会被销毁掉)。这也就是我为何强调说,缓存设计是一个比较复杂的事情。

VaryByHeader,能够根据用户请求中所提供的一些Header信息不一样而决定是否读取缓存。咱们能够看到在每一个请求中都会包含一些Header信息,以下图所示

image

这个也颇有用,例如根据不一样的语言,咱们显然是有不一样的版本的。或者根据用户浏览器不一样,也能够缓存不一样的版本。能够经过这样设置

VaryByHeader=”Accept-Language,User-Agent”

上面两个是比较经常使用的。固然还有另外两个属性也能够设置

VaryByContentEncoding,通常设置为Accept-Encoding里面可能的Encoding名称,从上图也能够看出,Request里面是包含这个标头的。

VaryByCustom,则是一个彻底能够定制的设置,例如咱们可能须要根据用户角色来决定不一样的缓存版本,或者根据浏览器的一些小版本号来区分不一样的缓存版本,咱们能够这样设置:VaryByCustom=”Role,BrowserVersion”,这些名称是你本身定义的,光这样写固然是没有用的,咱们还须要在Global.asax文件中,添加一个特殊的方法,来针对这种特殊的需求进行处理。

using System;
using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using System.Web.Routing; using System.Web.Security; namespace MvcApplicationCacheSample { // Note: For instructions on enabling IIS6 or IIS7 classic mode, // visit http://go.microsoft.com/?LinkId=9394801 public class MvcApplication : System.Web.HttpApplication { public static void RegisterGlobalFilters(GlobalFilterCollection filters) { filters.Add(new HandleErrorAttribute()); } public static void RegisterRoutes(RouteCollection routes) { routes.IgnoreRoute("{resource}.axd/{*pathInfo}"); routes.MapRoute( "Default", // Route name "{controller}/{action}/{id}", // URL with parameters new { controller = "Home", action = "Index", id = UrlParameter.Optional } // Parameter defaults ); } protected void Application_Start() { AreaRegistration.RegisterAllAreas(); RegisterGlobalFilters(GlobalFilters.Filters); RegisterRoutes(RouteTable.Routes); }  public override string GetVaryByCustomString(HttpContext context, string custom) { switch(custom) { case "Role": { return string.Join(",", Roles.GetRolesForUser()); } case "BrowserVersion": { return context.Request.Browser.Type; } default: break; } return string.Empty; } } }

 

上面四个属性,能够改变缓存使用的行为。另外还有一个重要属性将影响缓存保存的位置,这就是Location属性,这个属性有以下几个可选项,我从文档中摘录过来

成员名称

说明

Any 输出缓存可位于产生请求的浏览器客户端、参与请求的代理服务器(或任何其余服务器)或处理请求的服务器上。(这是默认值)
Client 输出缓存位于产生请求的浏览器客户端上。
Downstream 输出缓存可存储在任何 HTTP 1.1 可缓存设备中,源服务器除外。这包括代理服务器和发出请求的客户端。
Server 输出缓存位于处理请求的 Web 服务器上。
None 对于请求的页,禁用输出缓存。
ServerAndClient 输出缓存只能存储在源服务器或发出请求的客户端中。代理服务器不能缓存响应。

这里要思考一个问题,设置为Client与设置为Server有哪些行为上面的不一样

若是设置为Client,那么第一次请求的时候,获得的响应标头里面,会记录好这个页面应该是要缓存的,而且在10秒以后到期。以下图所示

image

而若是设置为Server的话,则会看到客户端是没有缓存的。

image

看起来不错,不是吗?若是你不加思索地就表示赞成,我要告诉你,你错了。因此,不要着急就下结论,请再试一下设置为Client的状况,你会发现,若是你刷新页面,那么仍然会发出请求,并且Result也是返回200,这表示这是一个新的请求,确实也返回告终果。这显然是跟咱们预期不同的。

为了作测试,我特地加了一个时间输出,若是仅仅设置为Client的话,每次刷新这个时间都是不同的。这说明,服务器端代码被执行了。

image

一样的问题也出如今,若是咱们将Location设置为ServerAndClient的时候,其实你会发现Client的缓存好像并无生效,每次都仍然是请求服务器,只不过这一种状况下,服务器端已经作了缓存,因此在规定时间内,服务器代码是不会执行的,因此结果也不会变。可是问题在于,既然设置了客户端缓存,那么理应就直接使用客户端的缓存版本,不该该去请求服务器才对。

这个问题,其实属因而ASP.NET自己的一个问题,这里有一篇文章介绍http://blog.miniasp.com/post/2010/03/30/OutputCacheLocation-ServerAndClient-problem-fixed.aspx

咱们能够看一下,将Location设置为ServerAndClient, 对代码稍做修改

using System;
using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using MvcApplicationCacheSample.Models; using System.Web.UI; namespace MvcApplicationCacheSample.Controllers { public class HomeController : Controller { // // GET: /Home/ [OutputCache(Duration=10,Location=OutputCacheLocation.ServerAndClient)] public ActionResult Index() {  Response.Cache.SetOmitVaryStar(true);  ViewBag.CurrentTime = DateTime.Now.ToString(); //这里目前做为演示,是直接硬编码,实际上多是读取数据库的数据 var employees = new[]{ new Employee(){ID=1,Name="ares",Gender="Male"} }; return View(employees); } } } 

image

咱们看到,从第二次请求开始,状态码是304,这表示该页被缓存了,因此浏览器并不须要请求服务器的数据。并且你能够看到Received的字节为221B,而不是原先的1.25KB。

可是,若是仅仅设置为Client,则仍然没法真正实现客户端缓存(这个行为是有点奇怪的)。这个问题我确实也一直没有找到办法,若是咱们确实须要使用客户端缓存,索性咱们仍是设置为ServerAndClient吧。

使用客户端缓存,能够明显减小对服务器发出的请求数,这从必定意义上更加理想。

 

2.使用缓存配置文件

第一节中,咱们详细地了解了MVC中,如何经过OutputCache这个ActionFilter来设置缓存。可是,由于这些设置都是经过C#代码直接定义在Action上面的,因此未免不是很灵活,例如咱们可能须要常常调整这些设置,该如何办呢?

ASP.NET 4.0中提供了一个新的机制,就是CacheProfile的功能,咱们能够在配置文件中,定义所谓的Profile,而后在OutputCache这个Attribute里面能够直接使用。

经过下面的例子,能够很容易看到这种机制的好处。下面的节点定义在system.web中

    <caching> <outputCacheSettings> <outputCacheProfiles> <add name="employee" duration="10" enabled="true" location="ServerAndClient" varyByParam="none"/> </outputCacheProfiles> </outputCacheSettings> </caching>

 

而后,代码中能够直接地使用这个Profile了

using System;
using System.Collections.Generic; using System.Linq; using System.Web; using System.Web.Mvc; using MvcApplicationCacheSample.Models; using System.Web.UI; namespace MvcApplicationCacheSample.Controllers { public class HomeController : Controller { // // GET: /Home/ [OutputCache(CacheProfile="employee")] public ActionResult Index() { //Response.Cache.SetOmitVaryStar(true); ViewBag.CurrentTime = DateTime.Now.ToString(); //这里目前做为演示,是直接硬编码,实际上多是读取数据库的数据 var employees = new[]{ new Employee(){ID=1,Name="ares",Gender="Male"} }; return View(employees); } } } 

这个例子很直观,有了Profile,咱们能够很轻松地在运行时配置缓存的一些关键值。

 

3.使用缓存API

经过上面的两步,咱们了解到了使用OutputCache,而且结合CacheProfile,能够很好地实现灵活的缓存配置。可是有的时候,咱们可能还但愿对缓存控制得更加精细一些。由于OutputCache是对Action的缓存,不一样的Action之间是不能共享数据的,假如某些数据,咱们是在不一样的Action之间共享的,那么,简单地采用OutputCache来作,就会致使对同一份数据,缓存屡次的问题。

因此,ASP.NET除了提供OutputCache这种基于声明的输出缓存设置以外,还容许咱们在代码中,本身控制要对哪些数据进行缓存,而且提供了更多的选项。

关于如何经过API的方式添加或者使用缓存,请参考

http://msdn.microsoft.com/zh-cn/library/18c1wd61%28v=VS.80%29.aspx

基本上就是使用HttpContext.Cache类型,能够完成全部的操做,并且足够灵活。

 

值得一提的是,我知道很多公司在项目中都会采用一些ORM框架,某些ORM框架中也容许实现缓存。例如NHibernate就提供了较为丰富的缓存功能,大体能够参考一下 http://www.cnblogs.com/RicCC/archive/2009/12/28/nhibernate-cache-internals.html

 

须要注意的是,微软本身提供的Entity Framework自己并无包含缓存的功能。

这里仍然要特别提醒一下,使用这种基于API的缓存方案,须要仔细推敲每一层缓存的设置是否合理,以及更新等问题。

 

4.使用缓存依赖

很早以前,在ASP.NET中设计缓存的时候,咱们就可使用缓存依赖的技术。关于缓存依赖,详细的信息请参考http://msdn.microsoft.com/zh-cn/library/ms178604.aspx

实际上,这个技术确实颇有用,ASP.NET默认提供了一个SqlCacheDependency,能够经过配置,链接SQL Server数据库,当数据库的表发生变化的时候,会通知到ASP.NET,该缓存就会失效。

值得一提的是,无论是采用OutputCache这样的声明式的缓存方式,仍是采用缓存API的方式,均可以使用到缓存依赖。并且使用缓存API的话,除了使用SqlCacheDependency以外,还可使用标准的CacheDependency对象,实现对文件的依赖。

http://msdn.microsoft.com/zh-cn/library/system.web.caching.cachedependency%28v=VS.80%29.aspx

 

5.分布式缓存

上面提到的手段都很不错,若是应用系统不是很庞大的话,也够用了。须要注意的是,上面所提到的缓存手段,都是在Web服务器本地内存中进行缓存,这种作法的问题在于,若是咱们须要作负载均衡(通常就会有多台服务器)的时候,就不可能在多台服务器之间共享到这些缓存。正由于如此,分布式缓存的概念就应运而生了。

谈到分布式缓存,目前比较受到你们承认的一个开源框架是 memcached。顾名思义,它仍然使用的是内存的缓存,只不过,它天生就是基于分布式的,它的访问都是直接经过tcp的方式,因此能够访问远程服务器,也能够多台Web服务器访问同一台缓存服务器。

关于memcached以及它在.NET中的使用,以前有一个朋友有写过一个介绍,能够参考使用一下

http://www.cnblogs.com/zjneter/archive/2007/07/19/822780.html

 

须要注意的是,分布式缓存不是为了来提升性能的(这多是一个误区),而且能够确定的是,它的速度必定会被本地慢一些。若是你的应用只有一台服务器就能知足要求,你就没有必要使用memcached。它的最大好处就是跨服务器,跨应用共享缓存。

相关文章
相关标签/搜索