【IT168 技术】许多做过程序性能优化的人,或者关注过程程序性能的人,应该都使用过各类缓存技术。 而我今天所说的Cache是专指ASP.NET的Cache,我们可以使用HttpRuntime.Cache访问到的那个Cache,而不是其它的缓存技术。
以前我在【我心目中的Asp.net核心对象】 这篇博客中简单地提过它,今天我打算为它写篇专题博客,专门来谈谈它,因为它实在是太重要了。在这篇博客中, 我不仅要介绍它的一些常见用法,还将介绍它的一些高级用法。 在上篇博客【在.net中读写config文件的各种方法】 的结尾处,我给大家留了一个问题,今天,我将在这篇博客中给出一个我认为较为完美的答案。
本文提到的【延迟操作】方法(如:延迟合并写入数据库)属于我的经验总结,希望大家能喜欢这个思路。
Cache的基本用途
提到Cache,不得不说说它的主要功能:改善程序性能。
ASP.NET是一种动态页面技术,用ASP.NET技术做出来的网页几乎都是动态的,所谓动态是指:页面的内容会随着不同的用户或者持续更新的数据, 而呈现出不同的显示结果。既然是动态的,那么这些动态的内容是从哪里来的呢?我想绝大多数网站都有自己的数据源, 程序通过访问数据源获取页面所面的数据,然后根据一些业务规则的计算处理,最后变成适合页面展示的内容。
由于这种动态页面技术通常需要从数据源获取数据,并经过一些计算逻辑,最终变成一些HTML代码发给客户端显示。而这些计算过程显然也是有成本的。 这些处理成本最直接可表现为影响服务器的响应速度,尤其是当数据的处理过程变得复杂以及访问量变大时,会变得比较明显。 另一方面,有些数据并非时刻在发生变化,如果我们可以将一些变化不频繁的数据的最终计算结果(包括页面输出)缓存起来, 就可以非常明显地提升程序的性能,缓存的最常见且最重要的用途就体现在这个方面。 这也是为什么一说到性能优化时,一般都将缓存摆在第一位的原因。 我今天要说到的ASP.NET Cache也是可以实现这种缓存的一种技术。 不过,它还有其它的一些功能,有些是其它缓存技术所没有的。
Cache的定义
在介绍Cache的用法前,我们先来看一下Cache的定义:(说明:我忽略了一些意义不大的成员) 『点击此处展开』
ASP.NET为了方便我们访问Cache,在HttpRuntime类中加了一个静态属性Cache,这样,我们就可以在任意地方使用Cache的功能。 而且,ASP.NET还给它增加了二个“快捷方式”:Page.Cache, HttpContext.Cache,我们通过这二个对象也可以访问到HttpRuntime.Cache, 注意:这三者是在访问同一个对象。Page.Cache访问了HttpContext.Cache,而HttpContext.Cache又直接访问HttpRuntime.Cache
Cache常见用法
通常,我们使用Cache时,一般只有二个操作:读,写。
要从Cache中获取一个缓存项,我们可以调用Cache.Get(key)方法,要将一个对象放入缓存,我们可以调用Add, Insert方法。 然而,Add, Insert方法都有许多参数,有时我们或许只是想简单地放入缓存,一切接受默认值,那么还可以调用它的默认索引器, 我们来看一下这个索引器是如何工作的:
{
get
{
return this.Get(key);
}
set
{
this.Insert(key, value);
}
}
可以看到:读缓存,其实是在调用Get方法,而写缓存则是在调用Insert方法的最简单的那个重载版本。
注意了:Add方法也可以将一个对象放入缓存,这个方法有7个参数,而Insert也有一个签名类似的重载版本, 它们有着类似的功能:将指定项添加到 System.Web.Caching.Cache 对象,该对象具有依赖项、过期和优先级策略以及一个委托(可用于在从 Cache 移除插入项时通知应用程序)。 然而,它们有一点小的区别:当要加入的缓存项已经在Cache中存在时,Insert将会覆盖原有的缓存项目,而Add则不会修改原有缓存项。
也就是说:如果您希望某个缓存项目一旦放入缓存后,就不要再被修改,那么调用Add确实可以防止后来的修改操作。 而调用Insert方法,则永远会覆盖已存在项(哪怕以前是调用Add加入的)。
从另一个角度看,Add的效果更像是 static readonly 的行为,而Insert的效果则像 static 的行为。
注意:我只是说【像】,事实上它们比一般的static成员有着更灵活的用法。
由于缓存项可以让我们随时访问,看起来确实有点static成员的味道,但它们有着更高级的特性,比如: 缓存过期(绝对过期,滑动过期),缓存依赖(依赖文件,依赖其它缓存项),移除优先级,缓存移除前后的通知等等。 后面我将会分别介绍这四大类特性。
Cache类的特点
Cache类有一个很难得的优点,用MSDN上的说话就是:
此类型是线程安全的。
为什么这是个难得的优点呢?因为在.net中,绝大多数类在实现时,都只是保证静态类型的方法是线程安全, 而不考虑实例方法是线程安全。这也算是一条基本的.NET设计规范原则。
对于那些类型,MSDN通常会用这样的话来描述:
此类型的公共静态(在 Visual Basic 中为 Shared)成员是线程安全的。但不能保证任何实例成员是线程安全的。
所以,这就意味着我们可以在任何地方读写Cache都不用担心Cache的数据在多线程环境下的数据同步问题。 多线程编程中,最复杂的问题就是数据的同步问题,而Cache已经为我们解决了这些问题。
不过我要提醒您:ASP.NET本身就是一个多线程的编程模型,所有的请求是由线程池的线程来处理的。 通常,我们在多线程环境中为了解决数据同步问题,一般是采用锁来保证数据同步, 自然地,ASP.NET也不例外,它为了解决数据的同步问题,内部也是采用了锁。
说到这里,或许有些人会想:既然只一个Cache的静态实例,那么这种锁会不会影响并发?
答案是肯定的,有锁肯定会在一定程度上影响并发,这是没有办法的事情。
然而,ASP.NET在实现Cache时,会根据CPU的个数创建多个缓存容器,尽量可能地减小冲突, 以下就是Cache创建的核心过程:
{
CacheInternal internal2;
int numSingleCaches = 0;
if( numSingleCaches == 0 ) {
uint numProcessCPUs = (uint)SystemInfo.GetNumProcessCPUs();
numSingleCaches = 1;
for( numProcessCPUs -= 1; numProcessCPUs > 0; numProcessCPUs = numProcessCPUs >> 1 ) {
numSingleCaches = numSingleCaches << 1;
}
}
CacheCommon cacheCommon = new CacheCommon();
if( numSingleCaches == 1 ) {
internal2 = new CacheSingle(cacheCommon, null, 0);
}
else {
internal2 = new CacheMultiple(cacheCommon, numSingleCaches);
}
cacheCommon.SetCacheInternal(internal2);
cacheCommon.ResetFromConfigSettings();
return internal2;
}
说明:CacheInternal是个内部用的包装类,Cache的许多操作都要由它来完成。
在上面的代码中,numSingleCaches的计算过程很重要,如果上面代码不容易理解,那么请看我下面的示例代码:
{
for( uint i = 1; i <= 20; i++ )
ShowCount(i);
}
static void ShowCount(uint numProcessCPUs)
{
int numSingleCaches = 1;
for( numProcessCPUs -= 1; numProcessCPUs > 0; numProcessCPUs = numProcessCPUs >> 1 ) {
numSingleCaches = numSingleCaches << 1;
}
Console.Write(numSingleCaches + ",");
}
程序将会输出:
1,2,4,4,8,8,8,8,16,16,16,16,16,16,16,16,32,32,32,32
CacheMultiple的构造函数如下:
{
this._cacheIndexMask = numSingleCaches - 1;
this._caches = new CacheSingle[numSingleCaches];
for (int i = 0; i < numSingleCaches; i++)
{
this._caches[i] = new CacheSingle(cacheCommon, this, i);
}
}
现在您应该明白了吧:CacheSingle其实是ASP.NET内部使用的缓存容器,多个CPU时,它会创建多个缓存容器。
在写入时,它是如何定位这些容器的呢?请继续看代码:
{
hashCode = Math.Abs(hashCode);
int index = hashCode & this._cacheIndexMask;
return this._caches[index];
}
说明:参数中的hashCode是直接调用我们传的key.GetHashCode() ,GetHashCode是由Object类定义的。
所以,从这个角度看,虽然ASP.NET的Cache只有一个HttpRuntime.Cache静态成员,但它的内部却可能会包含多个缓存容器, 这种设计可以在一定程度上减少并发的影响。
不管如何设计,在多线程环境下,共用一个容器,冲突是免不了的。如果您只是希望简单的缓存一些数据, 不需要Cache的许多高级特性,那么,可以考虑不用Cache 。 比如:可以创建一个Dictionary或者Hashtable的静态实例,它也可以完成一些基本的缓存工作, 不过,我要提醒您:您要自己处理多线程访问数据时的数据同步问题。
顺便说一句:Hashtable.Synchronized(new Hashtable())也是一个线程安全的集合,如果想简单点,可以考虑它。
接下来,我们来看一下Cache的高级特性,这些都是Dictionary或者Hashtable不能完成的。