為什么需要討論緩存?緩存是一個(gè)中大型系統所必須考慮的問(wèn)題。為了避免每次請求都去訪(fǎng)問(wèn)后臺的資源(例如數據庫),我們一般會(huì )考慮將一些更新不是很 頻繁的,可以重用的數據,通過(guò)一定的方式臨時(shí)地保存起來(lái),后續的請求根據情況可以直接訪(fǎng)問(wèn)這些保存起來(lái)的數據。這種機制就是所謂的緩存機制。
根據緩存的位置不同,可以區分為:
①客戶(hù)端緩存(緩存在用戶(hù)的客戶(hù)端,例如瀏覽器中)
②服務(wù)器緩存(緩存在服務(wù)器中,可以緩存在內存中,也可以緩存在文件里,并且還可以進(jìn)一步地區分為本地緩存和分布式緩存兩種)
MVC3中的緩存功能
ASP.NET MVC3 繼承了ASP.NET的優(yōu)良傳統,內置提供了緩存功能支持。主要表現為如下幾個(gè)方面:
①可以直接在Controller,Action或者ChildAction上面定義輸出緩存(這個(gè)做法相當于原先的頁(yè)面緩存和控件緩存功能)
②支持通過(guò)CacheProfile的方式,靈活定義緩存的設置(新功能)
③支持緩存依賴(lài),以便當外部資源發(fā)生變化時(shí)得到通知,并且更新緩存
④支持使用緩存API,還支持一些第三方的緩存方案(例如分布式緩存)
那么,下面我們就逐一來(lái)了解一下
一、范例準備
我準備了一個(gè)空白的MVC 3項目,里面創(chuàng )建好了一個(gè)Model類(lèi)型:Employee
然后,我還準備了一個(gè)HomeController
是的,我們可以這么做,而且也很容易做到這一點(diǎn)。MVC中內置了一個(gè)OutputCache的ActionFilter,我們可以將它應用在某個(gè)Action或者ChildAction上面
【備注】ChildAction是MVC3的一個(gè)新概念,本質(zhì)上就是一個(gè)Action,但通常都是返回一個(gè)PartialView。通常這類(lèi) Action,可以加上一個(gè)ChildActionOnly的ActionFilter以標識它只能作為Child被請求,而不能直接通過(guò)地址請求。
【備注】我們確實(shí)可以在Controller級別定義輸出緩存,但我不建議這么做。緩存是要經(jīng)過(guò)考慮的,而不是不管三七二十一就全部緩存起來(lái)。緩存不當所造成的問(wèn)題可能比沒(méi)有緩存還要大。
下面的代碼啟用了Index這個(gè)Action的緩存功能,我們讓他緩存10秒鐘。
那么,也就是說(shuō),第一次請求這個(gè)Index的時(shí)候,里面的代碼會(huì )執行,并且結果會(huì )被緩存起來(lái),然后在10秒鐘內,第二個(gè)或者后續的請求,就不需要再次執行,而是直接將結果返回給用戶(hù)即可。
這個(gè)OutputCache的Attribute,實(shí)際上是一個(gè)ActionFilter,它有很多參數,具體的請參考 http://msdn.microsoft.com/zh-cn/library/system.web.mvc.outputcacheattribute.aspx
這些參數中,Duration是必須的,這是設置一個(gè)過(guò)期時(shí)間,以秒為單位,這個(gè)我想大家都很好理解。我重點(diǎn)要一下下面幾個(gè):VaryByContentEncoding、VaryByCustom、VaryByHeader、VaryByParam。
這四個(gè)參數的意思是,決定緩存中如何區分不同請求,就是說(shuō),哪些因素將決定使用還是不使用緩存。默認情況下,如果不做任何設置,那么在規定的時(shí)間內(我們稱(chēng)為緩存期間),所有用戶(hù),不管用什么方式來(lái)訪(fǎng)問(wèn),都是直接讀取緩存。
VaryByParam,可以根據用戶(hù)請求的參數來(lái)決定是否讀取緩存。這個(gè)參數主要指的就是QueryString。例如
如果有多個(gè)參數的話(huà),可以用逗號分開(kāi)他們。例如 VaryByParam=”name,Id”
【備注】這里其實(shí)會(huì )有一個(gè)潛在的風(fēng)險,由于針對不同的參數(以及他們的組合)需要緩存不同的數據版本,假設有一個(gè)惡意的程序,分別用不同的參數發(fā)起大量的請求,那么就會(huì )導致緩存爆炸的情況,極端情況下,會(huì )導致服務(wù)器出現問(wèn)題。(當然,IIS里面,如果發(fā)現緩存的內容不夠用了,會(huì )自動(dòng)將一些數據清理掉,但這就同樣導致了程序的不穩定性,因為某些正常需要用的緩存可能會(huì )被銷(xiāo)毀掉)。這也就是我為什么強調說(shuō),緩存設計是一個(gè)比較復雜的事情。
VaryByHeader,可以根據用戶(hù)請求中所提供的一些Header信息不同而決定是否讀取緩存。我們可以看到在每個(gè)請求中都會(huì )包含一些Header信息,如下圖所示

這個(gè)也很有用,例如根據不同的語(yǔ)言,我們顯然是有不同的版本的?;蛘吒鶕脩?hù)瀏覽器不同,也可以緩存不同的版本??梢酝ㄟ^(guò)這樣設置
上面兩個(gè)是比較常用的。當然還有另外兩個(gè)屬性也可以設置
VaryByContentEncoding,一般設置為Accept-Encoding里面可能的Encoding名稱(chēng),從上圖也可以看出,Request里面是包含這個(gè)標頭的。
VaryByCustom,則是一個(gè)完全可以定制的設置,例如我們可能需要根據用戶(hù)角色來(lái)決定不同的緩存版本,或者根據瀏覽器的一些小版本號來(lái)區分不同 的緩存版本,我們可以這樣設置:VaryByCustom=”Role,BrowserVersion”,這些名稱(chēng)是你自己定義的,光這樣寫(xiě)當然是沒(méi)有用 的,我們還需要在Global.asax文件中,添加一個(gè)特殊的方法,來(lái)針對這種特殊的需求進(jìn)行處理。
上面四個(gè)屬性,可以改變緩存使用的行為。另外還有一個(gè)重要屬性將影響緩存保存的位置,這就是Location屬性,這個(gè)屬性有如下幾個(gè)可選項,我從文檔中摘錄過(guò)來(lái)

這里要思考一個(gè)問(wèn)題,設置為Client與設置為Server有哪些行為上面的不同
如果設置為Client,那么第一次請求的時(shí)候,得到的響應標頭里面,會(huì )記錄好這個(gè)頁(yè)面應該是要緩存的,并且在10秒之后到期。如下圖所示

而如果設置為Server的話(huà),則會(huì )看到客戶(hù)端是沒(méi)有緩存的。

看起來(lái)不錯,不是嗎?如果你不加思索地就表示同意,我要告訴你,你錯了。所以,不要著(zhù)急就下結論,請再試一下設置為Client的情況,你會(huì )發(fā)現, 如果你刷新頁(yè)面,那么仍然會(huì )發(fā)出請求,而且Result也是返回200,這表示這是一個(gè)新的請求,確實(shí)也返回了結果。這顯然是跟我們預期不一樣的。
為了做測試,我特意加了一個(gè)時(shí)間輸出,如果僅僅設置為Client的話(huà),每次刷新這個(gè)時(shí)間都是不一樣的。這說(shuō)明,服務(wù)器端代碼被執行了。

同樣的問(wèn)題也出現在,如果我們將Location設置為ServerAndClient的時(shí)候,其實(shí)你會(huì )發(fā)現Client的緩存好像并沒(méi)有生效,每次都仍然是請求服務(wù)器,只不過(guò)這一種情況下,服務(wù)器端已經(jīng)做了緩存,所以在規定時(shí)間內,服務(wù)器代碼是不會(huì )執行的,所以結果也不會(huì )變。但是問(wèn)題在于,既然設置了客戶(hù)端緩存,那么理應就直接使用客戶(hù)端的緩存版本,不應該去請求服務(wù)器才對。
這個(gè)問(wèn)題,其實(shí)屬于是ASP.NET本身的一個(gè)問(wèn)題,這里有一篇文章介紹 http://blog.miniasp.com/post/2010/03/30/OutputCacheLocation-ServerAndClient-problem-fixed.aspx
我們可以看一下,將Location設置為ServerAndClient, 對代碼稍作修改

我們看到,從第二次請求開(kāi)始,狀態(tài)碼是304,這表示該頁(yè)被緩存了,所以瀏覽器并不需要請求服務(wù)器的數據。而且你可以看到Received的字節為221B,而不是原先的1.25KB。
但是,如果僅僅設置為Client,則仍然無(wú)法真正實(shí)現客戶(hù)端緩存(這個(gè)行為是有點(diǎn)奇怪的)。這個(gè)問(wèn)題我確實(shí)也一直沒(méi)有找到辦法,如果我們確實(shí)需要使用客戶(hù)端緩存,索性我們還是設置為ServerAndClient吧。
使用客戶(hù)端緩存,可以明顯減少對服務(wù)器發(fā)出的請求數,這從一定意義上更加理想。
三、使用緩存配置文件
第一節中,我們詳細地了解了MVC中,如何通過(guò)OutputCache這個(gè)ActionFilter來(lái)設置緩存。但是,因為這些設置都是通過(guò)C#代碼直接定義在A(yíng)ction上面的,所以未免不是很靈活,例如我們可能需要經(jīng)常調整這些設置,該如何辦呢?
ASP.NET 4.0中提供了一個(gè)新的機制,就是CacheProfile的功能,我們可以在配置文件中,定義所謂的Profile,然后在OutputCache這個(gè)Attribute里面可以直接使用。
通過(guò)下面的例子,可以很容易看到這種機制的好處。下面的節點(diǎn)定義在system.web中
然后,代碼中可以直接地使用這個(gè)Profile了
這個(gè)例子很直觀(guān),有了Profile,我們可以很輕松地在運行時(shí)配置緩存的一些關(guān)鍵值。
使用緩存API
通過(guò)上面的兩步,我們了解到了使用OutputCache,并且結合CacheProfile,可以很好地實(shí)現靈活的緩存配置。但是有的時(shí)候,我們可能 還希望對緩存控制得更加精細一些。因為OutputCache是對Action的緩存,不同的Action之間是不能共享數據的,假如某些數據,我們是在 不同的Action之間共享的,那么,簡(jiǎn)單地采用OutputCache來(lái)做,就會(huì )導致對同一份數據,緩存多次的問(wèn)題。
所以,ASP.NET除了提供OutputCache這種基于聲明的輸出緩存設置之外,還允許我們在代碼中,自己控制要對哪些數據進(jìn)行緩存,并且提供了更多的選項。
關(guān)于如何通過(guò)API的方式添加或者使用緩存,請參考http://msdn.microsoft.com/zh-cn/library/18c1wd61%28v=VS.80%29.aspx
基本上就是使用HttpContext.Cache類(lèi)型,可以完成所有的操作,而且足夠靈活。
值得一提的是,我知道不少公司在項目中都會(huì )采用一些ORM框架,某些ORM框架中也允許實(shí)現緩存。例如NHibernate就提供了較為豐富的緩存功 能,大致可以參考一下 http://www.cnblogs.com/RicCC/archive/2009/12/28/nhibernate-cache-internals.html
需要注意的是,微軟自己提供的Entity Framework本身并沒(méi)有包含緩存的功能。
這里仍然要特別提醒一下,使用這種基于A(yíng)PI的緩存方案,需要仔細推敲每一層緩存的設置是否合理,以及更新等問(wèn)題。
使用緩存依賴(lài)
很早之前,在A(yíng)SP.NET中設計緩存的時(shí)候,我們就可以使用緩存依賴(lài)的技術(shù)。關(guān)于緩存依賴(lài),詳細的信息請參考 http://msdn.microsoft.com/zh-cn/library/ms178604.aspx
實(shí)際上,這個(gè)技術(shù)確實(shí)很有用,ASP.NET默認提供了一個(gè)SqlCacheDependency,可以通過(guò)配置,連接SQL Server數據庫,當數據庫的表發(fā)生變化的時(shí)候,會(huì )通知到ASP.NET,該緩存就會(huì )失效。
值得一提的是,不管是采用OutputCache這樣的聲明式的緩存方式,還是采用緩存API的方式,都可以使用到緩存依賴(lài)。而且使用緩存API的話(huà), 除了使用SqlCacheDependency之外,還可以使用標準的CacheDependency對象,實(shí)現對文件的依賴(lài)。
上面提到的手段都很不錯,如果應用系統不是很龐大的話(huà),也夠用了。需要注意的是,上面所提到的緩存手段,都是在Web服務(wù)器本地內存中進(jìn)行緩存,這種做法的問(wèn)題在于,如果我們需要做負載均衡(一般就會(huì )有多臺服務(wù)器)的時(shí)候,就不可能在多臺服務(wù)器之間共享到這些緩存。正因為如此,分布式緩存的概念就應運而生了。
談到分布式緩存,目前比較受到大家認可的一個(gè)開(kāi)源框架是 memcached。顧名思義,它仍然使用的是內存的緩存,只不過(guò),它天生就是基于分布式的,它的訪(fǎng)問(wèn)都是直接通過(guò)tcp的方式,所以可以訪(fǎng)問(wèn)遠程服務(wù)器,也可以多臺Web服務(wù)器訪(fǎng)問(wèn)同一臺緩存服務(wù)器。
需要注意的是,分布式緩存不是為了來(lái)提高性能的(這可能是一個(gè)誤區),并且可以肯定的是,它的速度一定會(huì )被本地慢一些。如果你的應用只有一臺服務(wù)器就能滿(mǎn)足要求,你就沒(méi)有必要使用memcached。它的最大好處就是跨服務(wù)器,跨應用共享緩存。
聯(lián)系客服