ASP.NET MVC 緩存使用示例


應該說,緩存的設計是一門較為復雜的學問,主要考慮的問題包括:要不要緩存?要緩存哪些數據?要緩存多少數據?要緩存多久?如何更新緩存(手動還是自 動)?將緩存放在哪里?本文將以較為通俗易懂的方式,來看一看在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。例如

如果我們緩存了http://localhost/Home/Index,那么用這個地址來訪問的時候,規定時間內都是讀取緩存。但如果用http://localhost/Home/Index?name=chenxizhang這樣的地址過來訪問,顯然我們希望不要讀取緩存,因為參數不一樣了。要實現這樣的需求,也就是說,希望根據name參數的不同緩存不同的數據。則可以設置VaryByParam=”name”。

 

如果有多個參數的話,可以用逗號分開他們。例如 VaryByParam=”name,Id”

【備注】這里其實會有一個潛在的風險,由於針對不同的參數(以及他們的組合)需要緩存不同的數據版本,假設有一個惡意的程序,分別用不同的參數發起大量的請求,那么就會導致緩存爆炸的情況,極端情況下,會導致服務器出現問題。(當然,IIS里面,如果發現緩存的內容不夠用了,會自動將一些數據清理掉,但這就同樣導致了程序的不穩定性,因為某些正常需要用的緩存可能會被銷毀掉)。這也就是我為什么強調說,緩存設計是一個比較復雜的事情。

VaryByHeader,可以根據用戶請求中所提供的一些Header信息不同而決定是否讀取緩存。我們可以看到在每個請求中都會包含一些Header信息,如下圖所示

這個也很有用,例如根據不同的語言,我們顯然是有不同的版本的。或者根據用戶瀏覽器不同,也可以緩存不同的版本。可以通過這樣設置

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屬性,這個屬性有如下幾個可選項,我從文檔中摘錄過來

這里要思考一個問題,設置為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。它的最大好處就是跨服務器,跨應用共享緩存。



免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM