應該說,緩存的設計是一門較為復雜的學問,主要考慮的問題包括:要不要緩存?要緩存哪些數據?要緩存多少數據?要緩存多久?如何更新緩存(手動還是自 動)?將緩存放在哪里?本文將以較為通俗易懂的方式,來看一看在MVC3的項目中,如何使用緩存功能。對於上述提到的一些具體業務問題,我這里不會進行太 過深入地探討。
為什么需要討論緩存?緩存是一個中大型系統所必須考慮的問題。為了避免每次請求都去訪問后台的資源(例如數據庫),我們一般會考慮將一些更新不是很 頻繁的,可以重用的數據,通過一定的方式臨時地保存起來,后續的請求根據情況可以直接訪問這些保存起來的數據。這種機制就是所謂的緩存機制。
根據緩存的位置不同,可以區分為:
①客戶端緩存(緩存在用戶的客戶端,例如瀏覽器中)
②服務器緩存(緩存在服務器中,可以緩存在內存中,也可以緩存在文件里,並且還可以進一步地區分為本地緩存和分布式緩存兩種)
MVC3中的緩存功能
ASP.NET MVC3 繼承了ASP.NET的優良傳統,內置提供了緩存功能支持。主要表現為如下幾個方面:
①可以直接在Controller,Action或者ChildAction上面定義輸出緩存(這個做法相當於原先的頁面緩存和控件緩存功能)
②支持通過CacheProfile的方式,靈活定義緩存的設置(新功能)
③支持緩存依賴,以便當外部資源發生變化時得到通知,並且更新緩存
④支持使用緩存API,還支持一些第三方的緩存方案(例如分布式緩存)
那么,下面我們就逐一來了解一下
一、范例准備
我准備了一個空白的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>
是的,我們可以這么做,而且也很容易做到這一點。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。例如
如果有多個參數的話,可以用逗號分開他們。例如 VaryByParam=”name,Id”
【備注】這里其實會有一個潛在的風險,由於針對不同的參數(以及他們的組合)需要緩存不同的數據版本,假設有一個惡意的程序,分別用不同的參數發起大量的請求,那么就會導致緩存爆炸的情況,極端情況下,會導致服務器出現問題。(當然,IIS里面,如果發現緩存的內容不夠用了,會自動將一些數據清理掉,但這就同樣導致了程序的不穩定性,因為某些正常需要用的緩存可能會被銷毀掉)。這也就是我為什么強調說,緩存設計是一個比較復雜的事情。
VaryByHeader,可以根據用戶請求中所提供的一些Header信息不同而決定是否讀取緩存。我們可以看到在每個請求中都會包含一些Header信息,如下圖所示
這個也很有用,例如根據不同的語言,我們顯然是有不同的版本的。或者根據用戶瀏覽器不同,也可以緩存不同的版本。可以通過這樣設置
上面兩個是比較常用的。當然還有另外兩個屬性也可以設置
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屬性,這個屬性有如下幾個可選項,我從文檔中摘錄過來
這里要思考一個問題,設置為Client與設置為Server有哪些行為上面的不同
如果設置為Client,那么第一次請求的時候,得到的響應標頭里面,會記錄好這個頁面應該是要緩存的,並且在10秒之后到期。如下圖所示
而如果設置為Server的話,則會看到客戶端是沒有緩存的。
看起來不錯,不是嗎?如果你不加思索地就表示同意,我要告訴你,你錯了。所以,不要着急就下結論,請再試一下設置為Client的情況,你會發現, 如果你刷新頁面,那么仍然會發出請求,而且Result也是返回200,這表示這是一個新的請求,確實也返回了結果。這顯然是跟我們預期不一樣的。
為了做測試,我特意加了一個時間輸出,如果僅僅設置為Client的話,每次刷新這個時間都是不一樣的。這說明,服務器端代碼被執行了。
同樣的問題也出現在,如果我們將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 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; } } }
我們看到,從第二次請求開始,狀態碼是304,這表示該頁被緩存了,所以瀏覽器並不需要請求服務器的數據。而且你可以看到Received的字節為221B,而不是原先的1.25KB。
但是,如果僅僅設置為Client,則仍然無法真正實現客戶端緩存(這個行為是有點奇怪的)。這個問題我確實也一直沒有找到辦法,如果我們確實需要使用客戶端緩存,索性我們還是設置為ServerAndClient吧。
使用客戶端緩存,可以明顯減少對服務器發出的請求數,這從一定意義上更加理想。
三、使用緩存配置文件
第一節中,我們詳細地了解了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,我們可以很輕松地在運行時配置緩存的一些關鍵值。
使用緩存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的緩存方案,需要仔細推敲每一層緩存的設置是否合理,以及更新等問題。
使用緩存依賴
很早之前,在ASP.NET中設計緩存的時候,我們就可以使用緩存依賴的技術。關於緩存依賴,詳細的信息請參考 http://msdn.microsoft.com/zh-cn/library/ms178604.aspx
實際上,這個技術確實很有用,ASP.NET默認提供了一個SqlCacheDependency,可以通過配置,連接SQL Server數據庫,當數據庫的表發生變化的時候,會通知到ASP.NET,該緩存就會失效。
值得一提的是,不管是采用OutputCache這樣的聲明式的緩存方式,還是采用緩存API的方式,都可以使用到緩存依賴。而且使用緩存API的話, 除了使用SqlCacheDependency之外,還可以使用標准的CacheDependency對象,實現對文件的依賴。
上面提到的手段都很不錯,如果應用系統不是很龐大的話,也夠用了。需要注意的是,上面所提到的緩存手段,都是在Web服務器本地內存中進行緩存,這種做法的問題在於,如果我們需要做負載均衡(一般就會有多台服務器)的時候,就不可能在多台服務器之間共享到這些緩存。正因為如此,分布式緩存的概念就應運而生了。
談到分布式緩存,目前比較受到大家認可的一個開源框架是 memcached。顧名思義,它仍然使用的是內存的緩存,只不過,它天生就是基於分布式的,它的訪問都是直接通過tcp的方式,所以可以訪問遠程服務器,也可以多台Web服務器訪問同一台緩存服務器。
需要注意的是,分布式緩存不是為了來提高性能的(這可能是一個誤區),並且可以肯定的是,它的速度一定會被本地慢一些。如果你的應用只有一台服務器就能滿足要求,你就沒有必要使用memcached。它的最大好處就是跨服務器,跨應用共享緩存。