隨着時間的流逝,數據源可能會因為其他活動而發生變化。如果你的代碼使用了緩存,你可能並沒有意識到這一變化而繼續使用了緩存中過期的信息。為了幫助解決這一問題,ASP.NET 支持緩存依賴。
緩存依賴允許你讓被緩存的項目依賴其他資源,這樣當那個資源發生變化時,緩存項目就會被自動移除。
ASP.NET 有 3 種類型的依賴:
- 依賴於其他緩存項目
- 依賴於文件或文件夾
- 依賴於數據庫查詢
文件和緩存項目依賴
要創建緩存依賴,你需要創建一個 CacheDependency 對象並在添加依賴的緩存項目時使用它。例如,下面的代碼創建一個緩存項目,它在一個 XML 文件被修改、刪除、覆蓋時自動從緩存中移除:
CacheDependency prodDependency = new CacheDependency(Server.MapPath("ProductList.xml"));
Cache.Insert("ProductInfo", prodInfo, prodDependency);
如果把 CacheDependency 指向某個文件夾,它會監視文件中所有文件和第一級的子文件夾。
CacheDependency 有幾個構造函數,可以用帶有文件名的構造函數創建文件依賴,還可以指定一個目錄,或者接收一個字符串數組的構造函數,同時監視多個文件或目錄。
CacheDependency 還有一個構造函數,接收一個文件名的數組和一個緩存鍵值的數組。下面這個示例使用該構造函數創建了一個依賴於緩存中其他項目的項目:
Cache["Key1"] = "Cache Item 1";
string[] dependencyKey = new string[1];
dependencyKey[0] = "Key2";
CacheDependency dependency = new CacheDependency(null, dependencyKey);
Cache.Insert("Key2", "Cache Item 2", dependency);
此后,當 Cache["Key1"] 發生變化或從緩存中移除時,Cache["Key2"] 就會被自動移除。
聚合依賴
有時你可能會希望組合多個依賴創建一個項目,它依賴多個其他資源。例如,它在 3 個文件中的任意一個發生變化時就無效等等。
使用 AggregateCacheDependency 可以包含任意多個 CacheDependency 對象。你所需要做的只是使用 AggregateCacheDependency .Add()方法提供一個 CacheDependency 對象的數組。
下面這個示例使一個緩存項目依賴於兩個文件:
CacheDependency dep1 = new CacheDependency(Server.MapPath("ProductList1.xml"));
CacheDependency dep2 = new CacheDependency(Server.MapPath("ProductList2.xml"));
CacheDependency[] deps = new CacheDependency[] { dep1, dep2 };
AggregateCacheDependency aggregateDep = new AggregateCacheDependency();
aggregateDep.Add(deps);
Cache.Insert("ProductInfo", prodInfo, aggregateDep);
其實,上述的示例不太符合實際,因為你完全可以在創建 CacheDependency 對象時就提供一個文件數組,它具有相同的作用。
AggregateCacheDependency 的真正價值體現在能夠同時包含多個從 CacheDependency 繼承的任意對象。所以你可以創建一個依賴,它同時包含文件依賴、SQL 緩存依賴甚至自定義緩存依賴。
移除回調項目
ASP.NET 還允許你編寫一個回調方法,它在項目從緩存中移除時觸發。處理回調的方法可以放在 Web 頁面類里,也可以使用其他輔助類的靜態方法。不過,需要記住的是:這段代碼不會作為 Web 請求的一部分執行。也就是說,你不能和 Web 頁面對象交互,也不能通知用戶。
下面的示例使用緩存回調創建兩個相互依賴的項目,兩個項目首先被加入到緩存里,然后當任意一個被移除時,在回調中立即移除另一個。
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
lblInfo.Text += "Creating items...<br />";
string itemA = "item A";
string itemB = "item B";
Cache.Insert("itemA", itemA, null, DateTime.Now.AddMinutes(60),
TimeSpan.Zero, CacheItemPriority.Default,
new CacheItemRemovedCallback(ItemRemovedCallback));
Cache.Insert("itemB", itemB, null, DateTime.Now.AddMinutes(60),
TimeSpan.Zero, CacheItemPriority.Default,
new CacheItemRemovedCallback(ItemRemovedCallback));
}
}
private void ItemRemovedCallback(string key, object value, CacheItemRemovedReason reason)
{
if (key == "itemA" || key == "itemB")
{
Cache.Remove("itemA");
Cache.Remove("itemB");
}
}
protected void btnCheckItem_Click(object sender, EventArgs e)
{
string itemList = "";
foreach (DictionaryEntry item in Cache)
{
itemList += item.Key.ToString() + " ";
}
lblInfo.Text += "<br />Found: " + itemList + "<br />";
}
protected void btnRemoveItem_Click(object sender, EventArgs e)
{
lblInfo.Text += "<br />Removing itemA.<br />";
Cache.Remove("itemA");
}
但單擊頁面的移除按鈕時,你會注意到項目移除回調其實發生了兩次:一次是那個移除的項目(itemA),還有一次是依賴的項目(itemB)。這不會產生問題,因為不存在的項目調用 Cache.Remove()是安全的。不過,如果還有其他的清理步驟(比如刪除文件),就需要確保他們不會執行兩次。
CacheItemRemovedReason 的枚舉值:
namespace System.Web.Caching
{
// 摘要:
// 指定從 System.Web.Caching.Cache 對象移除項的原因。
public enum CacheItemRemovedReason
{
// 摘要:
// 該項是通過指定相同鍵的 System.Web.Caching.Cache.Insert(System.String,System.Object)
// 方法調用或 System.Web.Caching.Cache.Remove(System.String) 方法調用從緩存中移除的。
Removed = 1,
//
// 摘要:
// 從緩存移除該項的原因是它已過期。
Expired = 2,
//
// 摘要:
// 之所以從緩存中移除該項,是因為系統要通過移除該項來釋放內存。
Underused = 3,
//
// 摘要:
// 從緩存移除該項的原因是與之關聯的緩存依賴項已更改。
DependencyChanged = 4,
}
}
你還可以使用項目移除回調去重新創建已經過期的項目。如果該項目的創建特別耗時,這就特別有用,因為你會希望在請求中使用它之前能夠創建它。不過,此時應該檢查 CacheItemRemovedReason 的值以判斷項目移除的原因。如果項目時因為正常過期(Expired)或依賴(DependencyChanged)被移除的,通常可以安全的創建它。否則,最好不要創建,因為項目可能很快又會被釋放。總之,應該確保代碼不會陷入到短期內不斷創建同一個項目的循環中。
理解 SQL 緩存通知
SQL 緩存依賴是一項當數據庫中的相關數據被修改時自動使緩存的數據對象失效的技術。
要理解 SQL 緩存依賴是如何工作的,首先要理解開發者以前不得不使用的幾個有缺陷的解決方法。
- 方法一:使用一個標志文件。你需要往緩存中加入數據對象並建立一個文件依賴。不過這個文件是空的,它只是一個用於表示數據庫狀態何時發生變化的標志文件。當用戶調用某個會修改你所關注的表的數據的存儲過程時,該存儲過程刪除或修改標志文件。ASP.NET 會立即發現文件發生了變化,它會移除相應的數據對象。這個笨方法不太具有擴展性:
- 當多個用戶同時調用存儲過程並同時移除文件時它還會帶來並發訪問的問題。
- 它還會使你的存儲過程代碼混亂,所有修改數據庫的存儲過程都需要類似的文件修改邏輯。
- 讓數據庫和文件系統交互本來就不是一個好主意,增加了復雜性,降低了整個系統的安全性。
- 方法二:在請求時使用一個自定義的 HTTP 處理程序移除緩存項目。同樣,需要修改對應表的存儲過程提供某種程度的支持才行。使用這個方法便不再和文件系統交互了。而是由存儲過程調用自定義的 HTTP 處理程序並在查詢字符串中表明什么發生了變化或者哪個緩存鍵值受到了影響,然后 HTTP 處理程序就可以使用 Cache.Remove()刪除相應的數據。這個辦法的問題在於:
- 需要一個非常復雜的擴展存儲過程
- 對 HTTP 的請求是同步的,會帶來顯著的延時,更糟的是每次執行存儲過程都會帶來延遲,因為存儲過程無法判斷是否需要調用處理程序或者緩存的項目是否已被移除,最終,用於執行存儲過程的時間顯著延長,數據庫的擴展性也受到了影響。
現在,所需要的是一個能夠異步傳送通知的方法,且具有足夠的擴展性和可靠性。數據庫服務器應該在不影響當前連接的情況下通知 ASP.NET。同樣重要的是,它要能夠以松耦合的方式建立緩存依賴,存儲過程不需要知道所使用的緩存。數據庫服務器要監視所有的變更,包括腳本、嵌入的 SQL 命令或批處理過程。即使變化不由存儲過程產生,它還是要提供變更通知,且通知要分發到 ASP.NET。最后,通知方法要支持 Web 集群。
為了提供一個良好的解決方案,微軟組建了一個由來自 ASP.NET、SQL Server、ADO.NET、IIS 團隊的架構師組成的團隊。根據你所使用的服務器的不同,他們最終提供了兩種架構,一種用於 SQL Server 2000,一種用於 SQL Server 的后期版本。這兩種架構使用 SQLCacheDependency 類,它繼承 CacheDependency 類。
緩存通知的工作方式
SQL Server 2005 引入了通知基礎結構和消息系統,它內建在數據庫里,叫做服務代理。服務代理管理隊列,他們是具有相同標准的表、存儲過程或視圖等數據庫對象。
使用服務代理,可以接收到特定數據庫事件的通知,其中最直接的方式是使用 CREATE EVENT NOTIFICATION 命令指定要檢測的事件。不過,.NET 提供了一個和 ADO.NET 集成的高級的模型。使用這個模型,你只要注冊一個查詢命令,然后 .NET 自動告訴 SQL Server 為所有影響那個查詢結果的操作發送通知。ASP.NET 提供了一個基於這個基礎結構的更高級模型,它允許你在查詢失效時使緩存的項目自動失效。
啟用通知
唯一需要進行的配置是確保數據庫具有 ENABLE_BROKER 標記設置,可以運行下面的 SQL 來執行這個動作(假設是 Northwind 數據庫):
use Northwind
alter database Northwind set enable_broker
通知可以和 SELECT 查詢以及存儲過程一起使用。但可以使用的 SELECT 語法有所限制。
為了正確支持通知,你的命令必須遵循下面的規則:
- 必須按 [Owner].table 的格式完整限定表名
- 查詢不能使用聚合函數
- 不能使用通配符 * 選擇所有列
下面是一個可接受的命令:
select EmployeeID,FirstName,LastName,City from dbo.Employees
創建 SQL 緩存依賴
創建緩存依賴時,SQL Server 需要知道正確的數據庫命令來獲取數據。如果使用可編程的緩存,必須使用接收 SqlCommand 對象的構造函數創建 SqlCacheDependency。下面是一個示例:
string conStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(conStr);
string sql = "select EmployeeID,FirstName,LastName,City from dbo.Employees";
SqlCommand cmd = new SqlCommand(sql, conn);
SqlDataAdapter sda = new SqlDataAdapter(cmd);
DataSet ds = new DataSet();
sda.Fill(ds, "Employees");
SqlCacheDependency empDepentency = new SqlCacheDependency(cmd);
Cache.Insert("Employees", ds, empDepentency);
還要調用靜態的 SqlDependency.Start()方法初始化 Web 服務器上的監聽服務。每個數據庫連接只要執行一次。可以調用 Start()方法的一個地方是在 global.asax 文件的 Application_Start()事件處理程序中:
SqlDependency.Start(conStr);
該方法打開一個新的沒有加入連接池的數據庫連接。ASP.NET 使用該連接檢查通知隊列。第一次調用 Start()時,會創建一個新的具有自動生成的唯一名稱的隊列,並為該隊列創建一個新的通知服務。然后,開始監聽。獲得一個通知時,Web 服務器把其從隊列中取出,引發 SqlDependency.OnChange 事件,使緩存的項目無效。
即使在幾個不同的表中有依賴,它們也都使用同一個隊列。也就是說,只要調用一次(多調也沒關系,不會出錯) SqlDependency.Start()。
通常,會在 global.asax 文件的 Application_End()事件處理程序中釋放監聽器:
SqlDependency.Stop(conStr);
自定義緩存依賴
ASP.NET 允許你繼承 CacheDependency 來創建自定義的緩存依賴,這和 SqlCacheDependency 類所做的差不多。這個功能允許你(或第三方開發者)創建封裝其他數據庫的依賴或資源,如消息隊列、活動目錄查詢甚至 Web 服務調用。
設計一個自定義的 CacheDependency 非常簡單。你要做的只是啟動一個異步任務,它檢查依賴項目何時發生變化。依賴項目發生變化時,調用基方法 CacheDependency.NotifyDependencyChanged(),作為回應,基類更新 HasChanged 和 UtcLastModified 屬性值,並且 ASP.NET 自動從緩存中移除所有相關項目。
你可以使用若干技術中的某一個來創建自定義的緩存依賴,下面是幾個典型的示例:
- 開始一個計時器:計時器觸發時,輪詢你的資源看它是否變化了。
- 開始一個獨立的線程:在這個線程里檢查你的資源,並且如果需要的話,讓線程在檢查間暫停。
- 附加到另一個組件的事件處理程序:例如,可以用這項技術借助 FileSystemWatcher 來監視特定類型文件的變更。
基本的自定義緩存依賴
下面的示例演示了一個非常簡單的自定義緩存依賴類。這個類使用計時器定期檢查緩存的項目是否依然有效:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
public class TimerTestCacheDependency : System.Web.Caching.CacheDependency
{
// 第一次創建依賴時,建立一個計時器。在這個示例里,輪詢時間硬編碼為 5 秒
private System.Threading.Timer timer;
private Int32 pollTime = 5000;
public TimerTestCacheDependency()
{
timer = new System.Threading.Timer(
new System.Threading.TimerCallback(CheckDependencyCallback),
this, 0, pollTime);
}
// 作為一個測試,依賴只檢查被統計的次數,它在被調用5次后(大約25秒)就使緩存項目失效
// 示例里最重要的部分是它如何通知 ASP.NET 移除依賴的項目
// 你所要做的只是調用基方法 NotifyDependencyChanged()
// 傳送事件發送者(當前對象)的引用以及所有參數
private Int32 count = 0;
private void CheckDependencyCallback(object sender)
{
count++;
if (count > 4)
{
base.NotifyDependencyChanged(this, EventArgs.Empty);
timer.Dispose();
}
}
// 最后一步是重寫 DependencyDispose()方法以執行所有必需的清理工作
// 用 NotifyDependencyChanged()方法使緩存的項目失效后,很快就會調用 DependencyDispose()
// 此時,已經不再需要依賴了
protected override void DependencyDispose()
{
if (timer != null)
{
timer.Dispose();
}
}
}
創建了自定義的依賴類后,就可以像用 CacheDependency 類一樣使用它,把它作為調用 Cache.Insert()的一個參數:
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
string str = "Hello world!";
TimerTestCacheDependency dependency = new TimerTestCacheDependency();
Cache.Insert("Key", str, dependency);
}
}
protected void Button1_Click(object sender, EventArgs e)
{
object obj = Cache["Key"];
if (obj != null)
{
lblInfo.Text = "Cache is exist.";
}
else
{
lblInfo.Text = "Cache is removed.";
}
}
使用消息隊列的自定義緩存依賴
你已經知道了如何創建一個基本的自定義緩存依賴,現在值得考慮的是一個更為實際的示例。
下面的 MessageQueueCacheDependency 監視微軟消息隊列(MSMQ)的隊列。一旦隊列接收到一個消息,項目就被認為是過期的(你可以很方便的擴展這個類,從而讓它等待接收一個特定的消息)。如果你正在建立一個分布式系統的框架,並且你需要在不同計算機組件間傳送消息以通知它們執行了某個動作或發生了某項變更,MessageQueueCacheDependency 類會非常方便。
在這個示例里,MessageQueueCacheDependency 能夠監視所有的隊列。實例化依賴時,你要提供隊列的名字(它包含位置信息)。為了實現監視,MessageQueueCacheDependency 異步觸發其私有方法 WaitForMessage()。這個方法一直等待直到隊列接收到新消息為止,這時它調用 NotifyDependencyChanged()使緩存的項目失效。
下面是 MessageQueueCacheDependency 的完整代碼:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Caching;
using System.Messaging;
using System.Threading;
// 需引用 System.Messaging.dll 程序集
public class MessageQueueCacheDependency : CacheDependency
{
// The queue to monitor.
private MessageQueue queue;
public MessageQueueCacheDependency(string queueName)
{
queue = new MessageQueue(queueName);
// Wait for a message on another thread.
WaitCallback callback = new WaitCallback(WaitForMessage);
ThreadPool.QueueUserWorkItem(callback);
}
private void WaitForMessage(object state)
{
// Check your resource here (the polling 輪詢)
// This blocks(阻塞) until a message is sent to the queue.
Message msg = queue.Receive();
// (If you're looking for something specific,
// you could perform a loop and check the Message object here
// before invalidating the cached item.)
base.NotifyDependencyChanged(this, EventArgs.Empty);
}
}
這個頁面在當前的計算機上創建一個私有的緩存,然后往緩存內添加一個依賴隊列的新項目:
public partial class Chapter11_CustomDependencyTest : System.Web.UI.Page
{
private string queueName = @".\Private$\TestQueue";
protected void Page_Load(object sender, EventArgs e)
{
if (!this.IsPostBack)
{
MessageQueue queue;
if (MessageQueue.Exists(queueName))
{
queue = new MessageQueue(queueName);
}
else
{
queue = MessageQueue.Create(queueName);
}
lblInfo.Text += "Creating dependent item...<br />";
Cache.Remove("Item");
MessageQueueCacheDependency dependency = new MessageQueueCacheDependency(queueName);
string item = "Dependent cached item";
lblInfo.Text += "Adding dependent item.<br />";
Cache.Insert("Item", item, dependency);
}
}
protected void btnSendMessage_Click(object sender, EventArgs e)
{
MessageQueue queue = new MessageQueue(queueName);
queue.Send("Invalidate!");
lblInfo.Text += "Message sent <br />";
}
protected void btnCheckCache_Click(object sender, EventArgs e)
{
if (Cache["Item"] != null)
{
lblInfo.Text += "Retrieved item with text: " + Cache["Item"].ToString();
lblInfo.Text += "<br />";
}
else
{
lblInfo.Text += "Cache removed Item.<br />";
}
}
}