技术开发 频道

ASP.net 缓存技术:延迟操作高级用法

        【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方法都有许多参数,有时我们或许只是想简单地放入缓存,一切接受默认值,那么还可以调用它的默认索引器, 我们来看一下这个索引器是如何工作的:

public object this[string key]

  {

  
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创建的核心过程:

internal static CacheInternal Create()
{
    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的计算过程很重要,如果上面代码不容易理解,那么请看我下面的示例代码: 

static void Main()
{
    
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的构造函数如下:

internal CacheMultiple(CacheCommon cacheCommon, int numSingleCaches) : base(cacheCommon)
{
    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时,它会创建多个缓存容器。

  在写入时,它是如何定位这些容器的呢?请继续看代码:

internal CacheSingle GetCacheSingle(int hashCode)
{
    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不能完成的。

 

  

0
相关文章