ASP.NET MVC:缓存功能的设计及问题
ASP.NET MVC:緩存功能的設計及問題
??? 這是非常詳盡的asp.net mvc中的outputcache 的使用文章。[原文:陳希章 http://www.cnblogs.com/chenxizhang/archive/2011/12/14/2288062.html]
前言
為什么需要討論緩存?緩存是一個中大型系統(tǒng)所必須考慮的問題。為了避免每次請求都去訪問后臺的資源(例如數(shù)據(jù)庫),我們一般會考慮將一些更新不是很頻繁的,可以重用的數(shù)據(jù),通過一定的方式臨時地保存起來,后續(xù)的請求根據(jù)情況可以直接訪問這些保存起來的數(shù)據(jù)。這種機制就是所謂的緩存機制。
根據(jù)緩存的位置不同,可以區(qū)分為:
應該說,緩存的設計是一門較為復雜的學問,主要考慮的問題包括
本文將以較為通俗易懂的方式,來看一看在MVC3的項目中,如何使用緩存功能。對于上述提到的一些具體業(yè)務問題,我這里不會進行太過深入地探討。
?
MVC3中的緩存功能
ASP.NET MVC3 繼承了ASP.NET的優(yōu)良傳統(tǒng),內(nèi)置提供了緩存功能支持。主要表現(xiàn)為如下幾個方面
-
可以直接在Controller,Action或者ChildAction上面定義輸出緩存(這個做法相當于原先的頁面緩存和控件緩存功能)
-
支持通過CacheProfile的方式,靈活定義緩存的設置(新功能)
-
支持緩存依賴,以便當外部資源發(fā)生變化時得到通知,并且更新緩存
-
支持使用緩存API,還支持一些第三方的緩存方案(例如分布式緩存)
那么,下面我們就逐一來了解一下吧
0.范例準備
我準備了一個空白的MVC 3項目,里面創(chuàng)建好了一個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(){//這里目前作為演示,是直接硬編碼,實際上可能是讀取數(shù)據(jù)庫的數(shù)據(jù)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>所以,當前的應用程序運行起來看到的效果大致是下面這樣的
這個例子很簡單,沒有太多需要解釋的。
?
1.使用輸出緩存
那么,現(xiàn)在我們假設這個讀取員工的數(shù)據(jù)很頻繁,但是數(shù)據(jù)又更新不是很頻繁,我們就會想到,能不能對這部分數(shù)據(jù)進行緩存,以便減少每次執(zhí)行的時間。
是的,我們可以這么做,而且也很容易做到這一點。MVC中內(nèi)置了一個OutputCache的ActionFilter,我們可以將它應用在某個Action或者ChildAction上面
【備注】ChildAction是MVC3的一個新概念,本質(zhì)上就是一個Action,但通常都是返回一個PartialView。通常這類Action,可以加上一個ChildActionOnly的ActionFilter以標識它只能作為Child被請求,而不能直接通過地址請求。
【備注】我們確實可以在Controller級別定義輸出緩存,但我不建議這么做。緩存是要經(jīng)過考慮的,而不是不管三七二十一就全部緩存起來。緩存不當所造成的問題可能比沒有緩存還要大。
?
下面的代碼啟用了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(){//這里目前作為演示,是直接硬編碼,實際上可能是讀取數(shù)據(jù)庫的數(shù)據(jù)var employees = new[]{new Employee(){ID=1,Name="ares",Gender="Male"}};return View(employees);}} }?
那么,也就是說,第一次請求這個Index的時候,里面的代碼會執(zhí)行,并且結果會被緩存起來,然后在10秒鐘內(nèi),第二個或者后續(xù)的請求,就不需要再次執(zhí)行,而是直接將結果返回給用戶即可。
這個OutputCache的Attribute,實際上是一個ActionFilter,它有很多參數(shù),具體的請參考
http://msdn.microsoft.com/zh-cn/library/system.web.mvc.outputcacheattribute.aspx
這些參數(shù)中,Duration是必須的,這是設置一個過期時間,以秒為單位,這個我想大家都很好理解。我重點要一下下面幾個
- VaryByContentEncoding
VaryByCustom
VaryByHeader
VaryByParam
這四個參數(shù)的意思是,決定緩存中如何區(qū)分不同請求,就是說,哪些因素將決定使用還是不使用緩存。默認情況下,如果不做任何設置,那么在規(guī)定的時間內(nèi)(我們稱為緩存期間),所有用戶,不管用什么方式來訪問,都是直接讀取緩存。
VaryByParam,可以根據(jù)用戶請求的參數(shù)來決定是否讀取緩存。這個參數(shù)主要指的就是QueryString。例如
如果我們緩存了http://localhost/Home/Index,那么用這個地址來訪問的時候,規(guī)定時間內(nèi)都是讀取緩存。但如果用http://localhost/Home/Index?name=chenxizhang這樣的地址過來訪問,顯然我們希望不要讀取緩存,因為參數(shù)不一樣了。要實現(xiàn)這樣的需求,也就是說,希望根據(jù)name參數(shù)的不同緩存不同的數(shù)據(jù)。則可以設置VaryByParam=”name”。
如果有多個參數(shù)的話,可以用逗號分開他們。例如 VaryByParam=”name,Id”
??? 【備注】這里其實會有一個潛在的風險,由于針對不同的參數(shù)(以及他們的組合)需要緩存不同的數(shù)據(jù)版本,假設有一個惡意的程序,分別用不同的參數(shù)發(fā)起大量的請求,那么就會導致緩存爆炸的情況,極端情況下,會導致服務器出現(xiàn)問題。(當然,IIS里面,如果發(fā)現(xiàn)緩存的內(nèi)容不夠用了,會自動將一些數(shù)據(jù)清理掉,但這就同樣導致了程序的不穩(wěn)定性,因為某些正常需要用的緩存可能會被銷毀掉)。這也就是我為什么強調(diào)說,緩存設計是一個比較復雜的事情。
VaryByHeader,可以根據(jù)用戶請求中所提供的一些Header信息不同而決定是否讀取緩存。我們可以看到在每個請求中都會包含一些Header信息,如下圖所示
這個也很有用,例如根據(jù)不同的語言,我們顯然是有不同的版本的。或者根據(jù)用戶瀏覽器不同,也可以緩存不同的版本。可以通過這樣設置
VaryByHeader=”Accept-Language,User-Agent”
上面兩個是比較常用的。當然還有另外兩個屬性也可以設置
VaryByContentEncoding,一般設置為Accept-Encoding里面可能的Encoding名稱,從上圖也可以看出,Request里面是包含這個標頭的。
VaryByCustom,則是一個完全可以定制的設置,例如我們可能需要根據(jù)用戶角色來決定不同的緩存版本,或者根據(jù)瀏覽器的一些小版本號來區(qū)分不同的緩存版本,我們可以這樣設置: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=9394801public 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 parametersnew { 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 | 輸出緩存可位于產(chǎn)生請求的瀏覽器客戶端、參與請求的代理服務器(或任何其他服務器)或處理請求的服務器上。(這是默認值) |
| Client | 輸出緩存位于產(chǎn)生請求的瀏覽器客戶端上。 |
| Downstream | 輸出緩存可存儲在任何 HTTP 1.1 可緩存設備中,源服務器除外。這包括代理服務器和發(fā)出請求的客戶端。 |
| Server | 輸出緩存位于處理請求的 Web 服務器上。 |
| None | 對于請求的頁,禁用輸出緩存。 |
| ServerAndClient | 輸出緩存只能存儲在源服務器或發(fā)出請求的客戶端中。代理服務器不能緩存響應。 |
這里要思考一個問題,設置為Client與設置為Server有哪些行為上面的不同
如果設置為Client,那么第一次請求的時候,得到的響應標頭里面,會記錄好這個頁面應該是要緩存的,并且在10秒之后到期。如下圖所示
而如果設置為Server的話,則會看到客戶端是沒有緩存的。
看起來不錯,不是嗎?如果你不加思索地就表示同意,我要告訴你,你錯了。所以,不要著急就下結論,請再試一下設置為Client的情況,你會發(fā)現(xiàn),如果你刷新頁面,那么仍然會發(fā)出請求,而且Result也是返回200,這表示這是一個新的請求,確實也返回了結果。這顯然是跟我們預期不一樣的。
為了做測試,我特意加了一個時間輸出,如果僅僅設置為Client的話,每次刷新這個時間都是不一樣的。這說明,服務器端代碼被執(zhí)行了。
同樣的問題也出現(xiàn)在,如果我們將Location設置為ServerAndClient的時候,其實你會發(fā)現(xiàn)Client的緩存好像并沒有生效,每次都仍然是請求服務器,只不過這一種情況下,服務器端已經(jīng)做了緩存,所以在規(guī)定時間內(nèi),服務器代碼是不會執(zhí)行的,所以結果也不會變。但是問題在于,既然設置了客戶端緩存,那么理應就直接使用客戶端的緩存版本,不應該去請求服務器才對。
這個問題,其實屬于是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();//這里目前作為演示,是直接硬編碼,實際上可能是讀取數(shù)據(jù)庫的數(shù)據(jù)var employees = new[]{new Employee(){ID=1,Name="ares",Gender="Male"}};return View(employees);}} }我們看到,從第二次請求開始,狀態(tài)碼是304,這表示該頁被緩存了,所以瀏覽器并不需要請求服務器的數(shù)據(jù)。而且你可以看到Received的字節(jié)為221B,而不是原先的1.25KB。
但是,如果僅僅設置為Client,則仍然無法真正實現(xiàn)客戶端緩存(這個行為是有點奇怪的)。這個問題我確實也一直沒有找到辦法,如果我們確實需要使用客戶端緩存,索性我們還是設置為ServerAndClient吧。
使用客戶端緩存,可以明顯減少對服務器發(fā)出的請求數(shù),這從一定意義上更加理想。
?
2.使用緩存配置文件
第一節(jié)中,我們詳細地了解了MVC中,如何通過OutputCache這個ActionFilter來設置緩存。但是,因為這些設置都是通過C#代碼直接定義在Action上面的,所以未免不是很靈活,例如我們可能需要經(jīng)常調(diào)整這些設置,該如何辦呢?
ASP.NET 4.0中提供了一個新的機制,就是CacheProfile的功能,我們可以在配置文件中,定義所謂的Profile,然后在OutputCache這個Attribute里面可以直接使用。
通過下面的例子,可以很容易看到這種機制的好處。下面的節(jié)點定義在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();//這里目前作為演示,是直接硬編碼,實際上可能是讀取數(shù)據(jù)庫的數(shù)據(jù)var employees = new[]{new Employee(){ID=1,Name="ares",Gender="Male"}};return View(employees);}} }這個例子很直觀,有了Profile,我們可以很輕松地在運行時配置緩存的一些關鍵值。
?3.使用緩存API
通過上面的兩步,我們了解到了使用OutputCache,并且結合CacheProfile,可以很好地實現(xiàn)靈活的緩存配置。但是有的時候,我們可能還希望對緩存控制得更加精細一些。因為OutputCache是對Action的緩存,不同的Action之間是不能共享數(shù)據(jù)的,假如某些數(shù)據(jù),我們是在不同的Action之間共享的,那么,簡單地采用OutputCache來做,就會導致對同一份數(shù)據(jù),緩存多次的問題。
所以,ASP.NET除了提供OutputCache這種基于聲明的輸出緩存設置之外,還允許我們在代碼中,自己控制要對哪些數(shù)據(jù)進行緩存,并且提供了更多的選項。
關于如何通過API的方式添加或者使用緩存,請參考
http://msdn.microsoft.com/zh-cn/library/18c1wd61%28v=VS.80%29.aspx
基本上就是使用HttpContext.Cache類型,可以完成所有的操作,而且足夠靈活。
?
值得一提的是,我知道不少公司在項目中都會采用一些ORM框架,某些ORM框架中也允許實現(xiàn)緩存。例如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數(shù)據(jù)庫,當數(shù)據(jù)庫的表發(fā)生變化的時候,會通知到ASP.NET,該緩存就會失效。
值得一提的是,不管是采用OutputCache這樣的聲明式的緩存方式,還是采用緩存API的方式,都可以使用到緩存依賴。而且使用緩存API的話,除了使用SqlCacheDependency之外,還可以使用標準的CacheDependency對象,實現(xiàn)對文件的依賴。
http://msdn.microsoft.com/zh-cn/library/system.web.caching.cachedependency%28v=VS.80%29.aspx
?
5.分布式緩存
上面提到的手段都很不錯,如果應用系統(tǒng)不是很龐大的話,也夠用了。需要注意的是,上面所提到的緩存手段,都是在Web服務器本地內(nèi)存中進行緩存,這種做法的問題在于,如果我們需要做負載均衡(一般就會有多臺服務器)的時候,就不可能在多臺服務器之間共享到這些緩存。正因為如此,分布式緩存的概念就應運而生了。
談到分布式緩存,目前比較受到大家認可的一個開源框架是 memcached。顧名思義,它仍然使用的是內(nèi)存的緩存,只不過,它天生就是基于分布式的,它的訪問都是直接通過tcp的方式,所以可以訪問遠程服務器,也可以多臺Web服務器訪問同一臺緩存服務器。
關于memcached以及它在.NET中的使用,之前有一個朋友有寫過一個介紹,可以參考使用一下
http://www.cnblogs.com/zjneter/archive/2007/07/19/822780.html
?
需要注意的是,分布式緩存不是為了來提高性能的(這可能是一個誤區(qū)),并且可以肯定的是,它的速度一定會被本地慢一些。如果你的應用只有一臺服務器就能滿足要求,你就沒有必要使用memcached。它的最大好處就是跨服務器,跨應用共享緩存。
總結
以上是生活随笔為你收集整理的ASP.NET MVC:缓存功能的设计及问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 韩国2000年石墓被破坏 韩网友泪崩:这
- 下一篇: 苹果公布自混传感器头戴设备专利 头显今年