數據緩存是最靈活的一種緩存,但需要在代碼中采用額外步驟才能使用它。數據緩存的基本原則是把創建代價高的項加入到一個特殊的內置集合對象內(Cache)。
這個對象和 Application 對象相似,對應用程序中所有客戶的所有請求都有效。
不過他們還是有幾個主要區別:
- Cache 對象是線程安全的:這意味着添加或移除項目不需要顯式的鎖定和解鎖 Cache 集合。但 Cache 集合里的對象還需要自身是線程安全的。
- 緩存中的項目是自動移除的:因為會自動移除,所以每次使用緩存對象都需要檢查是否仍在緩存中,否則會得到一個 NullRenfrenceException 異常。
- 緩存內的項目支持依賴性:可以把緩存的對象鏈接到文件、數據庫表或其他資源。如果這個資源發生了變化,緩存的對象會被自動標識為無效並被移除。
緩存對象保存在進程內,如果應用程序域重啟,它不會持續,同時它也不能在 Web 集群的服務器間共享。這種行為源於設計,因為多台計算機使用進程外緩存的代價大於它帶來的利益,而讓每台服務器擁有自己的緩存會更有意義。
向緩存添加項目
和 Application 或 Session 一樣,可以用一個鍵值往 Cache 集合中添加對象:
Cache["key"] = item;
不鼓勵使用這種方式!因為不能控制對象在緩存中保存的時間。更好的辦法是使用 Cache.Insert()方法。
Insert()方法的重載:
Insert(string key, object value) | 使用默認的優先級和過期策略。等同於:Cache["key"] = value; |
Insert(string key, object value, CacheDependency dependencies) |
使用默認的優先級和過期策略。CacheDependency 對象指向其他文件或緩存對象,它在這些項目發生變化時令緩存的對象無效。 |
Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration) |
使用默認的優先級,並指定可調或絕對過期策略。 這是最常用的一個 Insert()方法版本。 |
Insert(string key, object value, CacheDependency dependencies, DateTime absoluteExpiration, TimeSpan slidingExpiration, CacheItemPriority priority, CacheItemRemovedCallback onRemoveCallback) |
允許你為項目的緩存策略配置每個方面,包括過期策略,依賴關系和優先級。 此外,還可以提交一個指向項目移除時想要調用的委托。 |
把項目放入緩存時要做的一個重要選擇是過期策略。ASP.NET 允許你設置 可調過期策略 或 絕對過期策略,但不能同時使用這兩者。
使用絕對過期策略:把 slidingExpiration 參數設置為 TimeSpan.Zero
使用可調過期策略:把 absoluteExpiration 參數設置為 DataTime.Max
使用可調過期策略時,ASP.NET 等待一段不活動的時間后釋放那些被“遺忘”的緩存項目。可調過期策略適合那些基本上總是有效且又不怎么被訪問的信息,比如歷史數據或產品目錄。
Cache.Insert("My Item", obj, null, DateTime.MaxValue, TimeSpan.FromMinutes(10));
當你明確知道一個項目在多長時間內可以認為是有效的時候,使用絕對過期策略是最合適的。比如股票圖表或天氣預報。
Cache.Insert("My Item", obj, null, DateTime.Now.AddMinutes(60), TimeSpan.Zero);
從緩存讀取項目時,必須檢查項目是否為空引用,因為 ASP.NET 可能在任意時間移除緩存項目。解決這個問題的方法是添加一個用於再建項目時所需的特殊方法。下面是一個示例:
private DataSet GetCustomerData()
{
DataSet ds = Cache["CustomerData"] as DataSet;
if (ds == null)
{
ds = QueryCustomerDataFromDatabase();
Cache.Insert("CustomerData", ds);
}
return ds;
}
private DataSet QueryCustomerDataFromDatabase()
{
// some code
}
現在你就可以在代碼中任何需要的地方用如下的語法獲取 DataSet 了,而不必擔心緩存的細節:
GridView1.DataSource = GetCustomerData();
GridView1.DataBind();
提示:
把 QueryCustomerDataFromDatabase()方法移到一個單獨的數據組件可以獲得更好的設計。
沒有辦法一次清空整個數據緩存,不過可以用 DictionaryEntry 類枚舉整個集合:
// 清空 Cache
foreach (DictionaryEntry item in Cache)
{
Cache.Remove(item.Key.ToString());
}
// 獲得 Cache 集合所有項的鍵值
string itemList = "";
foreach (DictionaryEntry item in Cache)
{
itemList += item.Key.ToString() + " ";
}
簡單的緩存測試
下面的示例展示了一個簡單的緩存。一個項目被緩存 30 秒並在這段時間內被重用。頁面代碼一直運行(因為頁面自身沒有被緩存),檢查緩存,按需獲取或創建緩存項目,它還報告項目是否是從緩存獲得的。
protected void Page_Load(object sender, EventArgs e)
{
if (this.IsPostBack)
{
lblInfo.Text += "Page posted back.<br />";
}
else
{
lblInfo.Text += "Page Created.<br />";
}
DateTime? testItem = (DateTime?)(Cache["TestItem"]);
if (testItem == null)
{
lblInfo.Text += "Creating TestItem...<br />";
testItem = DateTime.Now;
lblInfo.Text += "Storing TestItem in cache for 30 seconds.<br />";
Cache.Insert("TestItem", testItem, null, DateTime.Now.AddSeconds(30), TimeSpan.Zero);
}
else
{
lblInfo.Text += "Retrieving TestItem...<br />";
lblInfo.Text += "TestItem is '" + testItem.ToString() + "'<br />";
}
lblInfo.Text += "<br />";
}
緩存優先級
把項目加入到緩存時還可以設置一個優先級。優先級只有在 ASP.NET 需要執行緩存清理時有效,這是一個因為內存緊張而提前移除緩存項目的過程。通常,應該為重建比較耗時的項目設置較高的緩存優先級以表明它們更加重要。
CacheItemPriority 枚舉類型可以選擇一個值設置緩存優先級:
Low | 在服務器釋放系統內存時,具有該優先級級別的緩存項最有可能被從緩存刪除。 |
BelowNormal | 在服務器釋放系統內存時,具有該優先級級別的緩存項比分配了 Normal 優先級的項更有可能被從緩存刪除。 |
Normal | 在服務器釋放系統內存時,具有該優先級級別的緩存項很有可能被從緩存刪除。(這是默認選項) |
AboveNormal | 在服務器釋放系統內存時,具有該優先級級別的緩存項被刪除的可能性比分配了 Normal 優先級的項要小。 |
High | 在服務器釋放系統內存時,具有該優先級級別的緩存項最不可能被從緩存刪除。 |
NotRemovable | 在服務器釋放系統內存時,具有該優先級級別的緩存項將不會被自動從緩存刪除。 |
使用數據源控件的緩存
SqlDataSource、ObjectDataSource、XmlDataSource 都支持內置的數據緩存。強烈推薦結合這些控件一起使用緩存。因為數據源控件通常生成額外的查詢請求。例如,在回發后如果參數發生變化,它們就重新執行查詢,它們為所有的綁定控件執行單獨的查詢,即使這些控件使用的是完全相同的命令,即便一點點緩存也能夠減少這樣的負載。
所有數據源都通過相同的屬性支持緩存:
EnableCaching | 為 true 時打開緩存,默認為 false |
CacheExpirationPolicy | 使用 DataSourceCacheExpiry 枚舉定義的一個值,Absolute(絕對過期) Sliding(可調過期) |
CacheDuration | 以秒為單位的數據對象緩存持續時間。如果使用可調過期策略,時限在每次緩存對象唄訪問時重置 |
CacheKeyDependency | 允許讓緩存的項目依賴數據緩存的其他項目 |
SqlCacheDependency | 允許讓緩存的項目依賴數據庫中的表 |
1. 使用 SqlDataSource 的緩存
啟用 SqlDataSource 控件的緩存意味着緩存 SelectQuery 的結果。不過,如果你的選擇查詢需要接受參數,那么 SqlDataSource 為每組參數緩存一個單獨的結果。
在下面這個示例中,每選擇一個城市就會執行一次查詢,查詢填充 DataSet,DateSet 被緩存。這個過程重復發生,新的 DataSet 被單獨緩存。但如果訪問一個其他用戶已經請求過的城市,相應的 DataSet 將會從緩存中獲得(如果它還沒有過期)。
<asp:DropDownList ID="ddlCity" runat="server" DataSourceID="sourceCity" DataTextField="City"
AutoPostBack="true">
</asp:DropDownList>
<asp:SqlDataSource ID="sourceCity" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="select distinct City from Employees"></asp:SqlDataSource>
<br />
<br />
<asp:GridView ID="GridView1" runat="server" DataSourceID="sourceEmployees">
</asp:GridView>
<asp:SqlDataSource ID="sourceEmployees" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees where City=@City"
EnableCaching="true">
<SelectParameters>
<asp:ControlParameter ControlID="ddlCity" Name="City" PropertyName="SelectedValue" />
</SelectParameters>
</asp:SqlDataSource>
如果所有的參數以近乎相同的頻率被使用,上述的辦法就不太適用了。它帶來的問題是緩存中的項目過期時,你需要多次查詢數據庫才能重新填充緩存(每個參數一次),這比執行一個查詢同時返回多個結果集的效率要差很多。
這種情況下,可以修改 SqlDataSource 讓它獲取所有雇員紀錄並把結果緩存。當有請求時,SqlDataSource 從中抽取符合用戶條件的記錄。這樣,只要緩存一個含有所有記錄的 DataSet ,就可以滿足所有的參數。
SqlDataSource 需要定義過濾表達式,這通常是 SQL 查詢中 WHERE 子句。這里要注意,如果你的過濾值來自其他數據源(比如某個控件),就必須定義一個或多個占位符,
看以下改寫后的完整的 SqlDataSource 標簽:
<asp:SqlDataSource ID="sourceEmployees" runat="server" ConnectionString="<%$ ConnectionStrings:Northwind %>"
ProviderName="System.Data.SqlClient" SelectCommand="select EmployeeID,FirstName,LastName,Title,City from Employees"
FilterExpression="City='{0}'" EnableCaching="true">
<FilterParameters>
<asp:ControlParameter ControlID="ddlCity" Name="City" PropertyName="SelectedValue" />
</FilterParameters>
</asp:SqlDataSource>
提示:
除非正在使用緩存,否則千萬不要使用過濾。如果在沒有緩存的情況下使用過濾,每次都要讀取完整的記錄集,然后從中取出部分記錄。
2. 使用 ObjectDataSource 的緩存
ObjectDataSource 緩存用於從 SelectedMethod 返回的數據對象。如果你正在使用參數化的查詢,ObjectDataSource 區別帶有不同參數值的請求分別緩存它們。遺憾的是,ObjectDataSource 緩存有一個非常大局限性,它只對返回 DataSet 或 DataTable 的選擇方法有效。如果你的方法返回其他對象類型,會得到一個 NotSupportedException 異常。這一局限性讓人遺憾。因為從技術的角度講,沒有任何理由不在數據緩存內緩存自定義對象。
如果你需要這樣的功能,就不得不在自己的方法中實現數據緩存。可以在獲得數據后手工插入到數據緩存內。