缓存(数据缓存)
数据缓存是最灵活的一种缓存,但需要在代码中采用额外步骤才能使用它。数据缓存的基本原则是把创建代价高的项加入到一个特殊的内置集合对象内(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 异常。这一局限性让人遗憾。因为从技术的角度讲,没有任何理由不在数据缓存内缓存自定义对象。
如果你需要这样的功能,就不得不在自己的方法中实现数据缓存。可以在获得数据后手工插入到数据缓存内。