緩存是內存中保存創建代價高的信息副本的一種技術。服務器內存是有限的資源,如果在其中保存了太多的信息,某些信息就會保存到硬盤的頁面文件上,這樣可能會減慢整個系統。最佳的緩存策略(如內置在 ASP.NET 中的)是自我約束的。
信息的生命周期由服務器自行管理,如果緩存滿了或者其他應用程序消耗了大量的內存,信息將會選擇性的從緩存移除以保持性能。正是這種自我管理,使得緩存如此強大(也正因為如此,實現你自己的緩存是非常復雜的)。
理解 ASP.NET 緩存
ASP.NET 有 2 種類型的緩存。你的應用程序能夠也應該同時使用這 2 種類型,因為它們是互補的。
- 輸出緩存:這是最簡單的緩存類型。它保存最終生成的發送到客戶端的 HTML 頁面的一個副本。下一個客戶再次請求這個頁面時,頁面沒有真正運行。運行頁面及其代碼的時間完全被省下來了。
- 數據緩存:它由你的代碼手工引入。使用數據緩存時,你將重建起來比較耗時(如從數據庫獲得DataSet)的重要對象保存在緩存內。其他頁面可用檢查這一信息是否已經存在,然后重用它,這樣可以省略獲取它們所需的步驟。
此外,還有建立在這兩個模型上的兩類特殊緩存:
- 部分頁緩存:它是輸出緩存的特殊類型,僅緩存部分 HTML 。部分緩存保存頁面上用戶控件的 HTML 輸出,下一次執行頁面時,同樣的頁面事件還會發生(這樣你的頁面代碼仍會執行),但是相應的用戶控件的代碼不會被執行。
- 數據源緩存:這種緩存建立在數據源控件(包括 SqlDataSource、ObjectDataSource 和 XmlDataSource)內。就技術而言,數據源緩存使用數據緩存。差別在於你不必顯式處理這一過程。你只需要配置適當的屬性,數據源控件就會自行管理緩存的保存和讀取。
輸出緩存
使用輸出緩存時,頁面最終產生的 HTML 被緩存,當再次請求相同的頁面時,不會創建控件對象,不會開始新的頁面生命周期,你的代碼頁不會被執行。理論上,輸出緩存可以達到最大的性能提升。
protected void Page_Load(object sender, EventArgs e)
{
lblDate.Text = "The time is now:<br />" + DateTime.Now.ToString();
}
有兩個辦法把頁面加入到輸出緩存中。最常見的辦法是在 .aspx 文件頂端 Page 指令下面加入:
<%@ OutputCache Duration="20" VaryByParam="None" %>
Duration 特性告訴 ASP.NET 將頁面緩存 20 秒。20秒看似很短,不過對於一個高訪問量的網站來說,已經帶來顯著的差別。數據庫每分鍾最多只會被訪問 3 次,而不使用緩存,客戶端每次請求時都要訪問數據庫,這些請求很容易就達到每分鍾數十次或更高!
當然,並不會因為你設置了20秒,它就一定會保存那么久,系統可能會因為內存緊張而提前把頁面從緩存中移除。(這個機制也保證了你可以自由使用緩存,無須過多擔心因為大量使用內存而影響你的應用程序。)
緩存和查詢字符串
把 VaryByParam 特性設置為“*”,它表示頁面要使用查詢字符串,同時告訴 ASP.NET 按不同的查詢字符串參數緩存頁面的獨立副本:
<%@ OutputCache Duration="20" VaryByParam="*" %>
現在當你請求帶有查詢字符串信息的頁面時,ASP.NET 會首先檢查查詢字符串,如果字符串和以前的請求匹配且該頁面的緩存副本存在,那么它將被重用。否則 ASP.NET 會創建一個新的頁面並單獨緩存它。
下一過程能幫助你更好的理解輸出緩存:
- 不使用查詢字符串請求頁面,獲得頁面的副本 A
- 使用參數 ProductID=1 請求頁面,獲得頁面的副本 B
- 另一個用戶使用 ProductID=2 請求頁面,獲得頁面的副本 C
- 另一個用戶使用 ProductID=1 請求頁面,如果緩存中的 B 還沒有過期,他獲得頁面的副本 B
- 這個用戶不使用查詢字符串請求頁面,如果 A 還沒有過期,A 也會從緩存中送出
使用特定查詢字符串參數的緩存
這項技術也有潛在的問題。可以接收很大范圍的查詢字符串參數的頁面就不適用於輸出緩存,可能的參數值數量巨大,潛在的重用性就非常低,盡管這些頁面會在需要內存時自動從緩存中移除,但它們還是可能使其他更為重要的信息提前從緩存移除或者減慢其他操作。
大多數情況下,把 VaryByParam 設為“*”不合適。通常,通過名稱明確指定重要的查詢字符串變量會更好一些:
<%@ OutputCache Duration="20" VaryByParam="ProductID" %>
這樣,ASP.NET 會在查詢字符串中查找 ProductID 參數,使用不同的 ProductID 參數的請求被分別緩存,而其他參數都被忽略。
通過使用分號分隔,還可以指定多個參數:
<%@ OutputCache Duration="20" VaryByParam="ProductID;CurrencyType" %>
此時,會按照 ProductID 或者 CurrencyType 分別緩存獨立版本的頁面。
自定義緩存控制
ASP.NET 還允許你創建自己的過程來確定是保存一個新的頁面版本還是使用現有的緩存。這樣的代碼檢查任意合適的信息並返回一個字符串,ASP.NET 使用這個字符串實現緩存。
自定義緩存的一個應用是基於瀏覽器的類型緩存不同版本的頁面。這樣,使用了 FireFox 瀏覽器的用戶將獲得為 FireFox 優化的頁面,IE 的用戶也會獲得為 IE 優化的 HTML。
為了建立這樣的邏輯,首先要添加 OutputCache 指令,然后為 VaryByCustom 特性指定你創建的自定義緩存的類型的名稱(你可以選擇任何你喜歡的名字)。
<%@ OutputCache Duration="10" VaryByParam="none" VaryByCustom="browser" %>
接下來要創建用於產生自定義緩存字符串的過程。這個過程必須寫在 global.asax 應用程序文件中:
public override string GetVaryByCustomString(HttpContext context, string arg)
{
if (arg == "browser")
{
string browserName = context.Request.Browser.Browser;
browserName += context.Request.Browser.MajorVersion.ToString();
return browserName;
}
else
{
return base.GetVaryByCustomString(context, arg);
}
}
使用 HttpCachePolicy 類進行緩存
你的代碼還可以使用內置的特殊屬性 Response.Cache,它提供 System.Web.HttpCachePolicy 類的一個實例,這個對象提供的屬性允許你打開當前頁面的緩存,這允許你通過編程啟用輸出緩存。
protected void Page_Load(object sender, EventArgs e)
{
// Cache this page on the server
Response.Cache.SetCacheability(HttpCacheability.Public);
// Use the cached copy of this page for the next 60 seconds.
Response.Cache.SetExpires(DateTime.Now.AddSeconds(10));
// 某些瀏覽器在刷新頁視圖時會將 HTTP 緩存無效標頭發送到 Web 服務器並從緩存中收回該頁。
// 當 validUntilExpires 參數為 true 時,ASP.NET 會忽略緩存無效標頭
// 而該頁將保留在緩存中直到過期為止。
Response.Cache.SetValidUntilExpires(true);
lblDate.Text = "The time is now:<br />" + DateTime.Now.ToString();
}
從設計的角度來看,可編程的緩存不夠清晰。把緩存代碼直接加到頁面中通常不太靈活,如果你還要包含其他初始化代碼,它甚至會把事情搞得一團糟。因為 Page.Load 事件處理程序內的代碼僅在頁面不在緩存中時執行。
緩存后替換和部分頁緩存
有時候你會發現不能緩存整個頁面,但你還是很樂意緩存某些創建昂貴且不怎么變化的內容。有兩個方法應對這一挑戰:
- 部分頁緩存:你需要找出緩存的內容,把它們封裝到一個專用的用戶控件內,然后緩存該控件的輸出。
- 緩存后替換:你需要找出不想緩存的動態內容,然后使用某些 Substitution 控件的東西替代這部分內容。
這兩個方式中,部分頁緩存最容易實現。但究竟適用於哪種方法往往基於你要緩存的內容的大小。如果要緩存的內容小且單一,部分頁緩存是最有效的辦法。反之,緩存后替換是更簡單的辦法。這兩個辦法的性能相近。
1. 部分頁緩存
要實現部分頁緩存,需要為緩存的部分創建一個用戶控件,然后在用戶控件上加入 OutputCache 指令。從概念上講,部分頁緩存和頁面緩存相同。但有一個缺點,如果頁面獲得的是用戶控件的一個緩存版本,它就不能通過代碼和控件交互。使用緩存版本的用戶控件時,只是把一段 HTML 代碼插入到頁面上,相應的用戶控件對象不可用。
2. 緩存后替換
緩存后替換功能涉及到 HttpResponse 類新增的一個方法:WriteSubstitution(),它接受一個參數,一個指向頁面類中回調方法的委托。這個回調方法返回頁面要替換的部分。
就本質而言,總體思想是你創建一個生成動態內容的方法,確保這個方法每次都會被調用且它的內容從來不會被緩存。
用於生產動態內容的方法必須是靜態的。因為即使頁面類的實例不可用(很顯然,當頁面內容從緩存提供時,頁面對象不會被創建),ASP.NET 也要調用這個方法。這個方法的簽名很簡單,它接受一個代表當前請求的 HttpContext 對象,返回一個帶有新 HTML 內容的字符串。
private static string GetDate(HttpContext context)
{
return "<b>" + DateTime.Now.ToString() + "</b>";
}
// 為了在頁面中獲得日期,需要在某個地方使用 Response.WriteSubstitution()方法
protected void Page_Load(object sender, EventArgs e)
{
Response.Write("This date is cached with the page: ");
Response.Write(DateTime.Now.ToString() + "<br />");
Response.Write("This date is not: ");
Response.WriteSubstitution(new HttpResponseSubstitutionCallback(GetDate));
}
現在,即使頁面被緩存了,第二個日期也會在每次請求后更新,因為回調繞過了緩存過程。
通常,設計 ASP.NET 頁面時,你根本不會使用 Response 對象,而是可以使用 Web 控件,這些 Web 控件使用 Response 控件對象生成它們的內容。如果像前面的示例那樣使用 Response 對象,它帶來的一個問題是:你會失去在頁面其他部分定位內容的能力。唯一可行的辦法是把動態內容封裝在某個控件內,這樣,控件自身呈現時可以使用 Response.WriteSubstitution()方法,這個我們以后會詳細闡述。
不過,如果不願意只為了獲得緩存后替換的功能而自定義控件的話,ASP.NET 還有一個快捷方式:一個通用的 Substitution 控件,它使用這項技術來使自己的全部內容是動態的。
Substitution 控件可以和其他 ASP.NET 控件放到一起,這樣就可以精確控制動態內容出現的位置了。
重寫上面這個示例:
This date is cached with the page:
<asp:Label ID="lblDate" runat="server" Text="Label"></asp:Label><br />
This date is not:
<asp:Substitution ID="Substitution" runat="server" MethodName="GetDate" />
protected void Page_Load(object sender, EventArgs e)
{
lblDate.Text = DateTime.Now.ToString();
}
private static string GetDate(HttpContext context)
{
return "<b>" + DateTime.Now.ToString() + "</b>";
}
緩存后替換只允許你執行靜態方法!ASP.NET 還是跳過了頁面的生命周期,它不會創建任何控件對象或產生任何控件事件。因為在回調中這些控件對象不可用,所有如果動態內容依賴於其他控件的值,你就需要使用其他技術(如 數據緩存)。
緩存用戶配置
輸出緩存的一個問題是你必須把緩存命令嵌入到頁面內:OutputCache 指令或者代碼中調用 Response.Cache 對象的部分方法。如果有數十個緩存頁面,它們會存在嚴重的管理問題(例如把時間從 10秒 設置到 20秒)。
ASP.NET 允許你對一組頁面應用相同的緩存設置,這個功能叫做緩存用戶配置。它允許你在 web.config 中定於緩存配置,這些設置和一個名字關聯,然后可以用這個名字對多個頁面應用這些設置。
<system.web>
<caching>
<outputCacheSettings>
<outputCacheProfiles>
<add name="ProductItemCacheProfile" duration="5"/>
</outputCacheProfiles>
</outputCacheSettings>
</caching>
</system.web>
<%@ OutputCache CacheProfile="ProductItemCacheProfile" VaryByParam="None" %>
輸出緩存擴展
ASP.NET 的緩存模型在各種 Web 應用程序里工作的都非常好。它使用簡單、運行飛快,因為緩存服務運行在 ASP.NET 進程內並且把數據保存在物理內存中。
但是,如果想長時間緩存大量數據,ASP.NET 的緩存系統就不那么適合了。例如大型電子商務網站的產品目錄,假設產品目錄不會頻繁變化,你可能希望緩存數以千計的產品頁面從而節省創建它們的開銷。但對於這樣海量的數據,使用 Web 服務器的內存是很有風險的。
相反,你可能會選用其他類型的存儲,它比內存慢但還是比重建頁面要快(並且不太可能造成資源瓶頸)。這個存儲可能基於磁盤、數據庫、或者像 Windows Server AppFabric 這樣的分布式存儲系統。
1. 構建自定義的緩存提供程序
使用基於磁盤的文件系統緩存要比基於內存的緩存要慢,但使用它基於很重要的兩點:
- 可持久化的緩存:由於輸出緩存保存在磁盤上,即使Web程序域重新啟動了,它還繼續保持在那里。
- 低內存使用:當緩存的頁面被重用時,它直接從硬盤提供,因此不需要把數據塊讀取到內存中。1)對於大型緩存文件非常有用。2)如果按查詢字符串策略進行緩存輸出且查詢字符串的變化非常多時,它特別有用。這 2 種情況下,使用內存很難構建成功的緩存策略。
創建自定義緩存提供程序很簡單。只要繼承 System.Web.Caching 空間的 OutputCacheProvider 類,然后重寫下列方法:
Initialize() | 當提供程序第一次加載時完成一些初始化的任務。(這是這個列表里唯一不一定需要重寫的方法) |
Add() | 如果項不存在,則添加到緩存里。 |
Set() | 把項添加到緩存里,可以覆蓋原始項。 |
Get() | 如果存在,從緩存里獲取項。這個方法應該強制使用基於時間的過期策略,檢查過期時間移除項。 |
Remove() | 把項從緩存中移除。 |
為了實現序列化,創建了一個叫做 CacheItem 的類,把初始要緩存的項和過期時間封裝到了一起:
[Serializable]
public class CacheItem
{
public DateTime ExpiryDate; // 有效期
public object Item;
public CacheItem(object item, DateTime expiryDate)
{
this.ExpiryDate = expiryDate;
this.Item = item;
}
}
現在只要重寫 Add()、Set()、Get()、Remove()方法即可。這些方法接收一個唯一標識緩存內容的鍵值。這個鍵基於緩存頁面的文件名。
例如,對網站 CustomCacheProvider 的 OutputCaching.aspx 頁面使用輸出緩存時,代碼接收到的鍵可能是:
a2/customcacheprovider/outputcaching.aspx
為了把它轉換為有效的文件名,代碼只要把其中的 "/" 轉換為 "-" 即可。同時還增加了擴展名 .txt ,以區分真正的 ASP.NET 頁面和緩存內容並方便在調試時打開和查看緩存文件的內容。轉換后文件名的示例:
a2-customcacheprovider-outputcaching.aspx.txt
public class FileCacheProvider : System.Web.Caching.OutputCacheProvider
{
public FileCacheProvider()
{
}
public string CachePath { get; set; }
private string ConvertKeyToPath(string key)
{
string file = key.Replace('/', '-');
file += ".txt";
// 將兩個字符串組合成一個路徑。
return Path.Combine(CachePath, file);
}
// 檢查內容是否已存在
// 同時還要返回緩存對象
public override object Add(string key, object entry, DateTime utcExpiry)
{
// Transform the key to a unique filename.
string path = ConvertKeyToPath(key);
if (!File.Exists(path))
{
Set(key, entry, utcExpiry);
}
return entry;
}
// 總是保存內容
// 勿忘序列化要引入下列命名空間
// System.Runtime.Serialization.Formatters.Binary;
public override void Set(string key, object entry, DateTime utcExpiry)
{
CacheItem item = new CacheItem(entry, utcExpiry);
string path = ConvertKeyToPath(key);
using (FileStream fs = File.OpenWrite(path))
{
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(fs, entry);
}
}
// 檢查項是否存在
// 檢查項是否過期,如過期需要移除
public override object Get(string key)
{
string path = ConvertKeyToPath(key);
if (!File.Exists(path))
{
return null;
}
CacheItem item = null;
using (FileStream fs = File.OpenRead(path))
{
BinaryFormatter bf = new BinaryFormatter();
item = (CacheItem)bf.Deserialize(fs);
}
// 將當前 System.DateTime 對象的值轉換為協調世界時 (UTC)。
if (item.ExpiryDate <= DateTime.Now.ToUniversalTime())
{
Remove(key);
return null;
}
return item;
}
// 總是刪除
public override void Remove(string key)
{
string path = ConvertKeyToPath(key);
if (File.Exists(path))
{
File.Delete(path);
}
}
}
2. 使用自定義緩存提供程序
在使用自定義緩存提供程序之前,需要把它添加到 <caching> 配置節。
<system.web>
<caching>
<outputCache defaultProvider="FileCache">
<providers>
<add name="FileCache" type="FileCacheProvider" cachePath="~/Cache"/>
</providers>
</outputCache>
......
</system.web>
如果該類是一個單獨程序集的一部分(上例假設在 APP_CODE 目錄里),則需要同時指定程序集的名稱。例如,名為 CacheExtensibility 程序集中命名空間 CustomCaching 中的 FileCacheProvider 應該這樣配置:
<add name="FileCache" type="CustomCaching.FileCacheProvider,CacheExtensibility" cachePath="~/Cache"/>
還有一個細節,這個節包括一個自定義特性 cachePath 。ASP.NET 忽略額外添加的這一部分,但是你的代碼可以自由讀取並使用它。例如,FileCacheProvider 可以使用 Initialize()方法讀取這個信息並設置路徑(對於這個例子,它應當是當前 Web 應用程序目錄的 Cache 子目錄)。
public override void Initialize(string name, System.Collections.Specialized.NameValueCollection config)
{
// 初始化提供程序。
// name:
// 該提供程序的友好名稱。
// config:
// 名稱/值對的集合,表示在配置中為該提供程序指定的、提供程序特定的屬性。
base.Initialize(name, config);
// Retrieve the web.config settings.
CachePath = HttpContext.Current.Server.MapPath(config["CachePath"]);
}
如果不使用 defaultProvider 特性,就會由 ASP.NET 來決定何時使用標准的內存內的緩存服務,以及何時使用自定義緩存提供程序。【你可能會希望在頁面指令里處理這一問題,但不行,緩存是在頁面讀取之前進行的(如果成功的話,頁面會完全忽略頁面標記)】。
相反,需要重寫 global.asax 文件的 GetOutputCacheProviderName()方法。這個方法檢查當前請求並返回處理當前請求的緩存提供程序的名稱。這個示例告訴 ASP.NET 對頁面 OutputCaching.aspx 使用 FileCacheProvider (其他的不使用):
public override string GetOutputCacheProviderName(HttpContext context)
{
// 獲取請求的虛擬路徑
// 通過 Path.GetFileName() 獲取文件名和擴展名
string pageAndQuery = System.IO.Path.GetFileName(context.Request.Path);
if (pageAndQuery.StartsWith("OutputCaching.aspx"))
{
return "FileCache";
}
else
{
return base.GetOutputCacheProviderName(context);
}
}