在ASP.NET中,缓存有两种形式:应用程序数据的缓存和页面输出的缓存,这两种形式相互独立。
应用程序数据的缓存
ASP.NET缓存API是以Cache对象为中心的全局数据容器,由所有会话共享。Cache对象是线程安全的容器,能自动移除无用的数据项,支持各种形式的信赖关系,还可以指定移除回调和优先级。
Cache类
Cache类位于System.Web.Caching命名空间中,可以将Cache看作是应用程序级数据和对象的仓库。
Cache类的实例是针对每个AppDomain而创建的,只要所处AppDomain运行,它就一直有效。应用程序的ASP.NET Cache的当前实例,可以由HttpContext对象的Cache属性,也可以由Page对象的Cache属性返回。
尽管Cache和HttpApplicationState类的目标都是作为ASP.NET应用程序全局的数据仓库,但二者之间存在许多不同。
1. Cache是一种线程安全的对象,不需要在访问前后显式的加锁与解锁。该对象内容代码中的所有关键部分会通过同步构造得到充分的保护。
2. 存储在Cache中的数据不必与应用程序保持相同的生存期,我们可以通过Cache对象为其中存储的项指定缓存时间和优先级。
我们可以对被缓存项进行配置,使其在指定的秒数逝去后过期,以便回收相应的内存空间。通过设置优先级,可以帮助Cache对象在内存空间短缺的情况下选择安全释放项的顺序。数据项可以与多种类型的信赖关联,若这种关联被打破,相应的缓存项便会失效。
Cache类的属性
Cache类的属性见下表:
NoAbsoluteExpiration和NoSlidingExpiration公共字段是内部常量,用于配置缓存项的过期策略。
NoAbsoluteExpiration字段类型为DataTime,其值默认为DateTime.MaxValue返回的日期,即默认永不超时。NoSlidingExpiration字段类型为TimeSpan。其值默认为TimeSpan.Zero,表可调过期被禁用。
Item是一种可读/写属性,可用于向缓存中添加新项。如果传入Item属性的健不存在,会创建新项。通过Item属性向缓存插入新项会应用许多默认属性设置:不会为该项指定过期策略和移除回调函数,但会被设置一个常规的优先级。因此,该项一直存在于缓存中,直到以编程方式将其移除或应用程序终止。使用Cache类的Insert方法可在添加新项时做更多的设置。
Cache类的方法
下表列出了Cache的主要方法:
传入Add和Insert方法的键和缓存项不能为null,可调过期时间不能超过1年,否则将抛出异常。
不能对同一个对象同时设置可变过期策略和绝对过期策略。
Add和Insert的工作方式基本一致,但有两点不同:如果新项的键已存在,Add执行会失败(但不会抛出异常),而Insert会改写现有的缓存项;Add只有一种签名,但Insert有多个重载方法。
内部视图
Cache类继承于Object,实现了IEnumerable接口,它是一个内部类的包装器,而该类才是真正的数据容器。具体实现ASP.NET缓存功能的类取决的CPU的数目。如果只有一个CPU可用,则选用CacheSingle,否则选用CacheMultiple。不认选择哪个,数据项都会存储在哈希表中,每个哈希表对应一个CPU。下图表现了Cache对象的架构:
每个哈希表被分为两个部分:公共部分和私有部分。在哈希表的公共部分中的所有数据对用户应用程序可见,而私有部分存储的是系统级数据。缓存是一种资源,ASP.NET运行库本身也会广泛使用它,但系统数据与应用程序数据完全分离,且应用程序无法访问缓存中的私有元素。
Cache对象会将应用程序的读取和写入限制在数据存储的公共部分,缓存类内部的get和set方法接受一个标志,指示缓存项是否为公共的(默认为公共)。若调用来自Cache类,那么这些内部方法总会将该标志设置为默认值,以便选择公共元素。
在配有多处理器的计算机上,ASP.NET工作进程与多个CPU紧密关联,每个处理器都会对应一个Cache对象。不同的缓存对象间不会进行同步。
ASP.NET缓存的使用
每个运行当中的应用程序与一个Cache对象的实例相关联,其生存期也与应用程序相同。缓存中保存的是数据的引用,能够主动校验是否有效及是否过期。当系统内存不中时,Cache对象会自动移除某些不经常使用的缓存项。每个存储在缓存中的项会被附加一些特殊的属性,以便确定其优先级和过期策略。
新项的插入
缓存项的特性由一些属性来刻画,它们作为Add和Insert方法的参数传入。具体来说包含这几种属性:
1. 键(key)一种区分大小写的字符串
2. 值(value)object类型的非空值
3. 依赖项(dependency)CacheDependency类型对象,用于跟踪添加到缓存中的项与各依赖项(如:文件、目录、数据库表、应用程序缓存中的其他对象)的物理依赖关系。
4. 绝对过期(absolute expiration)DateTime类型对象,代表被添加项的绝对过期时间。
5. 可调过期(sliding expiration)TimeSpan类型对象,代表被添加项的相对过期时间。
6. 优先级(priority)取自CacheItemPriority枚举的值,代表缓存项的优先级。
7. 移除回调函数(removal callback)当相应的缓存项被移除时,ASP.NET Cache对象会调用该函数。
向ASP.NET Cache对象中添加新项有三种方法:Item的set访问方法;Add方法;Insert方法。
Insert方法最灵活,提供了以下4个重载:
public void Insert( string , object , CacheDependency);
public void Insert( string , object , CacheDependency, DateTime, TimeSpan);
public void Insert( string , object , CacheDependency, DateTime, TimeSpan, CacheItemPriority, CacheItemRemovedCallback);
Item属性的set方法实际调用了以下代码:
现有项的移除
Remove方法用于移除缓存项。
若从缓存中移除带有回调函数的项,将有一个CacheItemRemovedReason枚举值传入回调函数,用于提示执行移除操作的原因。下表列出了该枚举类型包含的值:
依赖项的跟踪
通过Add方法或Insert方法添加到缓存中的项,可以与一组文件、目录、现有的项、数据库表或外部事件关联。新项与其缓存依赖间的关系可通过CacheDependency类的实例来维护。CacheDependency对象可用于关联单个文件或目录,也能关联多个。此外,它还能关联一组“缓存键”(存储在Cache中其他对象的键)和其他要监视的自定义依赖项(如,数据库表或外部事件)。
CacheDependency类有很多构造函数,详见下表:
下面的代码中,新创建的项与一个文件相关联:
Cache.Insert(key, value, dep);
在为CacheDepandency对象提供文件名或目录名时,需要用文件系统路径来表达。
移除回调函数的定义
缓存项被移除后,由于应用程序不知道该项被移除,所以当它试图访问该项时,会得到null值。为解决这个问题,可以在访问某缓存项前对该项的是否存在进行检查。更好的办法是为该缓存项的移除事件注册一个回调函数,以便在该项失效时重新加载它。
下面的代码演示了如何读取Web服务器中文件的内容,并将其缓存。该缓存项连同一个回调函数一起添加:
{
AddFileContentsToCache( " data.xml " );
}
void Read_Click( object sender, EventArgs e)
{
object data = Cache[ " MyData " ];
if (data == null )
{
contents.Text = " [No data available] " ;
return ;
}
contents.Text = ( string )data;
}
void AddFileContentsToCache( string fileName)
{
// 读取文件到buf中
string file = Server.MapPath(fileName);
StreamReader reader = new StreamReader(file);
string buf = reader.ReadToEnd();
reader.Close();
// 将buf添加到Cache中
CreateAndCacheItem(buf, file);
contents.Text = Cache[ " MyData " ].ToString();
}
void CreateAndCacheItem( object buf, string file)
{
// 创建回调委托
CacheItemRemovedCallback removal = new CacheItemRemoveCallback(ReloadItemRemoved);
// 创建缓存依赖
CacheDependency dep = new CacheDependency(file);
// 添加缓存项
Cache.Insert( " MyData " , buf, dep, Cache.NoAbsoluteExpiration, Cache.NoSlidingExpiration, CacheItemPriority.Nomal, removal);
}
void ReloadItemRemoved( string key, object value, CacheItemRemovedReason reason)
{
// 检查删除缓存的原因是否因为文件被修改
if (reason == CacheItemRemovedReason.DependencyChanged)
{
// 检查缓存键
if (key == " MyData " )
// 重新创建缓存
AddFileContentsToCache( " data.xml " );
}
}
void Remove_Click( object sender, EventArgs e)
{
Cache.Remove( " MyData " );
}
如果向Cache中添加缓存项,并使其与一个不存在的文件、文件或键相关联,该项仍会以常规方式缓存,并像平常一样关联该依赖项。如果该文件、目录或键之后被创建,该依赖关系便会被打破,缓存的项也会失效。也就是说,如果依赖项实际不存在,则会以空内容来创建它。
缓存项的优先级设置
可以为缓存项指定一个优先级,在系统资源不中时,缓存项的优先级越高,它在内存中被保留下来的机会也就越高。
下表列出了可用的优先级:
数据的过期控制
“可调过期”是一种相对过期策略,其思想是,对象在某个时间间隔(以TimeSpan表示)过后过期。但在这种情况下,每次访问该项都会刷新该时间间隔。如果要将缓存项的过期时间设置为10分钟,可使用以下代码:
在内部,该缓存项的过期时间是通过绝对时间实现的。绝对过期时间被设置为当前时间与指定的TimeSpan值之和。每次访问该项后,都会被绝对过期时间重置为最后访问时间与指定的时间间隔之和。
在Cache类初始化后,它会立即采集系统内存的统计信息。随后,它会向一个计时器(timer)注册一个回调函数,使它每隔一秒被调用一次。该回调函数会周期性地更新和监视内存的统计数字,并在必要时激活“清理模块”。内存的统计结果是通过一系统Win32 API函数获取当前物理内存和虚拟内存的使用情况得到的。
Cache对象将这些系统资源的状态分为高压(high pressure)和低压(low pressure)。每个指标对应于不同的内存占用比例。一般来讲,低压指示内存占用率在15%--40%之间,而高压在45%--65%之间。当内存压力达到警戒级别时,根据对象的优先级大小,不常用的对象会先被移除。
缓存中项的枚举
Cache类是一种集合,其内容可以很方便地通过for...each语句进行枚举。如下所示:
{
DataTable dt = CreateDataTable();
foreach (DictionaryEntry elem in HttpContext.Current.Cache)
AddItemToTable(dt, elem);
return dt;
}
private DataTable CreateDataTable()
{
DataTable dt = new DataTable();
dt.Columns.Add( " Key " , typeof ( string ));
dt.Columns.Add( " Value " , typeof ( string ));
}
private void AddItemToTable(DataTable dt, DictionaryEntry elem)
{
DataRow row = dt.NewRow();
row[ " Key " ] = elem.Key.ToString();
row[ " Value " ] = elem.Value.ToString();
dt.Rows.Add(row);
}
在枚举缓存中的项时,只能得到两种信息:键和值。用户无法读取给定项的优先级或过期策略。在枚举Cache对象的内容时,返回的是DictionaryEntry,这个泛化的对象没有针对具体信息的属性或方法。为获取更多信息,我们可以考虑使用反射。
缓存的清除
.NET没有直接提供清除Cache类内容的方法,下面的代码演示了如何清除Cache:
{
foreach (DictionaryEntry elem in Cache)
{
string s = elem.Key.ToString();
Cache.Remove(s);
}
}
缓存的同步
对缓存项,不管是读取还是写入,从线程的角度来看,都是绝对安全的。Cache对象能够确保同时运行的线程不会干扰这些操作。如果需要保证对Cache对象的多个操作以原子方式执行,则另当别论。考虑以下代码:
object o = Cache[ " Counter " ];
if (o == null )
{
counter = RetrieveLastKnownValue();
}
else
{
counter = ( int )Cache[ " Counter " ];
counter ++ ;
Cache[ " Counter " ] = counter;
}
Cache对象在这个原子操作(对计数器进行累加)的上下文中被重复访问。虽然内存访问Cache都是线程安全的,但并不能保证其他线程不会在期间介入。如果缓存项具有潜在的连接,应考虑使用一种锁定机制(如lock语句)。