缓存(缓存依赖)
随着时间的流逝,数据源可能会因为其他活动而发生变化。如果你的代码使用了缓存,你可能并没有意识到这一变化而继续使用了缓存中过期的信息。为了帮助解决这一问题,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 />";
}
}
}