談談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。它的最大好處就是跨服務器,跨應用共享緩存。

相關文章
相關標籤/搜索