深入学习Enterprise Library for .NET Framework 2.0的Cache机制——分析篇

深入学习Enterprise Library for .NET Framework 2.0的Cache机制——分析篇

发布日期: 4/18/2006 | 更新日期: 4/18/2006

PanQi@ultrapower

 

简介:

在服务器负载均衡的环境下,客户端读、写缓存时,一定要保证多台服务器间缓存数据的一致性和同步性;撰写本文档的目的就是为了探讨利用Enterprise Library for .NET Framework 2.0的Caching Application Block如何在服务器负载均衡的环境下实现有效的数据缓存。

 

提出问题:

1、 负载均衡时缓存数据应以何种方式保存?

2、  磁盘驻留型缓存和内存缓存之间的状态是如何同步的?

分析问题:

1.      负载均衡时缓存数据应以何种方式保存?

        在决定我们的缓存数据保存到哪里之前我们有必要先了解一下Caching Application Block定义的两种缓存类型,它们分别是内存驻留型缓存和磁盘驻留型缓存,顾名思义,这两种类型的缓存是以存贮位置来命名的,功能上则以是否能将缓存数据持久化来区别使用。

        在Caching Application Block中,具体提供以下四种保存缓存数据的途径,分别是:内存存储(默认)、独立存储(Isolated Storage)、数据库存储(DataBase Cache Storage)和自定义存储(Custom Cache Storage)。要解决负载均衡时缓存数据保存到哪里的问题,首先我们先详细了解一下这些上面的这四种缓存途径。

1、  内存存储:内存存储缓存是以上四种方式中唯一的内存驻留型缓存,也是我们开发中最常用到的一种途径,其响应速度快的优势是其它方式无法匹敌的,但单一得采用这种方式的话会有如下弊端:1、缓存数据不能持久化,服务器重起后缓存数据会全部丢失。2、服务器采用负载均衡时采用内存缓存的话,一定要保证多台服务器间的内存缓存状态同步,但这样做会对IO造成较大压力,容易造成系统瓶颈,故,从系统性能和开发成本的角度讲,负载均衡的环境下不易采用内存缓存。

2 、独立缓存(Isolated Storage):Isolated Storage是缓存数据持久化的一种选择方式,它是磁盘驻留型缓存,如果您足够细心的话会在每一台机器上找到一个IsolatedStorage文件夹;采用独立缓存的话,我们的缓存信息就会以二进制文件的形式就保存在这个文件夹中,如下是我所用的机器上生成Cache数据后的缓存信息所在的具体目录:C:\Documents and Settings\Administrator\Local Settings\Application Data\IsolatedStorage\5njvvogn.4c4\mccvvdtb.kxw\Url.xhd4g43erd3ww4y5ybmfacgje044tgdp\Url.ium543vzoj1lgo4qvgjeocev4cjb5agq\Files\IsolatedCache\......

 

(图一:二进制格式的缓存文件)

       Caching Application Block没有为我们提供向指定机器读、写独立缓存的功能,因此,这种方式只适合需要缓存持久化和大数据量缓存的场合,并不适用于负载均衡的环境。

3、自定义存储(Custom Cache Storage):关于自定义存储并未深入研究,在此暂不详细讨论。

4、数据库存储(DataBase Cache Storage):如若想采用数据库存储缓存信息,首先第一步要建立缓存数据库Caching,在安装Enterprise Library后并未默认安装此数据库,若想安装,我们首先找到Enterprise Library的安装文件夹,会发现一个建立数据库的CreateCachingDb.cmd文件,执行该文件后您就会在您的SQL-Server上新建一个名为Caching的数据库,该文件在2.0版中详细地址为:安装盘符:\Program Files\Microsoft Enterprise Library January 2006\src\Caching\Database\Scripts,在Caching数据库中只有一个CacheData表,这个表就保存了我们所读写得缓存信息,表结构如下:

(图二 数据库缓存的表结构)

        分析到这里,我们可以得到如下结论:在服务器负载均衡的环境下,可以采用Enterprise Library for .NET Framework 2.0中Caching Application Block的DataBase Cache Storage将缓存信息写到一台指定的数据库服务器中,但是,在负载均衡的这种较为复杂的环境下,如何将数据库缓存准确的提供给每一个缓存数据查询者呢??也就是读数据的问题。在MSND中提到的CAB的一个特性是:“可以使用独立存储或企业程序库数据访问应用程序块来配置永久存储位置,它的状态与内存缓存同步。”我们会在接下来的文章中分析一下CAB的这种数据库缓存数据和内存缓存数据可否在多台服务器间进行状态同步。

2.      磁盘驻留型缓存和内存缓存之间的状态是如何同步的?

        我们暂时先放下负载均衡的场景,研究一下一台服务器时,采用DataBase Cache Storage,数据库缓存和内存缓存是如何同步。首先,我们先来看一下在开发过程中应用Caching Application Block向缓存中写对象的操作是如何实现的,代码如下:

 

PresenceInfo objPre         = new PresenceInfo();

objPre.Data             = this.txtcData.Text;

objPre.UserURI          = this.txtCUserURI.Text;

objPre.LastChangeTime = Convert.ToDateTime(this.txtcLCT.Text);

 

CacheManager myCacheManager = null;

myCacheManager = CacheFactory.GetCacheManager("DB Cache Manager");            

myCacheManager.Add(objPre.UserURI,objPre);
 

 

         代码的前半部分是为演示new了一个PresenceInfo的对象objPre;紧接着,我们创建了一个CacheManager的对象myCacheManager,这个对象是由CacheFactory的GetCacheManager("DB Cache Manager")方法得到的,您一定注意到,GetCachemanager 方法的参数“DB Cache Manager”,这个参数是和您的Config文件对应得(后面我们会详细介绍),由此指出了采用数据库存储缓存的方式来持久化缓存;然后,我们把对象objPre用CacheManager的Add方法写入缓存和数据库,代码的大致意思如此;接下来,我们针对代码详细的分析一下CacheFactory.GetCacheManager("DB Cache Manager")方法和

myCacheManager.Add(objPre.UserURI,objPre)这两个方法究竟进行了那些操作。

        GetCachemanager()方法是CacheFactory类的静态方法,它进行了两次重载,两次重载的目的是针对默认的内存存储和磁盘存储进行了区别处理,下面是针对磁盘驻留型缓存返回CacheManager的代码:

 

 

private static CacheManagerFactory factory = new CacheManagerFactory(ConfigurationSourceFactory.Create());

public static CacheManager GetCacheManager(string cacheManagerName)
{
    try
    {
        lock (lockObject)
        {
            return factory.Create(cacheManagerName);
        }
    }
    catch (ConfigurationErrorsException configurationException)
    {
        TryLogConfigurationError(configurationException, cacheManagerName);
        throw;
    }
}


    我们看到第一段代码,它利用CacheManagerFactory的构造函数实例化了CacheManagerFactory工厂类,生成factory静态对象,然后利用该对象的Creat(cacheManagerName)方法返回类型CacheManager,CacheManagerFactory的构造函数代码如下:

 

public CacheManagerFactory(IConfigurationSource configurationSource) : base(configurationSource)
{ }

这个构造函数是路由到了基类LocatorNameTypeFactoryBase来实现的。

这段代码的最终目的是通过基类的构造函数建立了一个CacheManagerFactory的工厂类,然后由工厂的Create方法针对参数cacheManagerName返回CacheManager。

     CacheManager,是执行缓存操作的核心类,有了CacheManager,我们就可以使用它的Add()方法,也就是我们先前提到的:

 

 

myCacheManager.Add(objPre.UserURI,objPre);


我们分析的目的是为了弄清楚Caching Application Block采用数据库持久化缓存时在负载均衡的环境下,写缓存可否在多台服务器间保持内存缓存和数据库缓存的同步,这也是本文章的根本目的。

通过前面的代码中我们得到了CacheManager类的实例对象myCacheManager,然后我们调用该对象的Add()方法写进行写缓存的操作。Add()方法重载了两次,两次重载的方法都是调用的Cache类的Add方法,代码如下:

private Cache realCache;

public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
{
     realCache.Add(key, value, scavengingPriority, refreshAction, expirations);
}

在Cache类的Add()方法中,首先用方法ValidateKey(key)对输入的key进行了是否为空的验证,代码如下:
private static void ValidateKey(string key)
{
    if (string.IsNullOrEmpty(key))
    {
        throw new ArgumentException(Resources.EmptyParameterName, "key");
    }            
}
 

经过一系列的验证后,接下来的代码将执行向内存缓存写数据的工作,代码如下:

do
{
    lock (inMemoryCache.SyncRoot)
    {
        if (inMemoryCache.Contains(key) == false)
        {
            cacheItemBeforeLock = new CacheItem(key, addInProgressFlag, CacheItemPriority.NotRemovable, null);
            inMemoryCache[key] = cacheItemBeforeLock;
        }
        else
        {
            cacheItemBeforeLock = (CacheItem)inMemoryCache[key];
        }
            lockWasSuccessful = Monitor.TryEnter(cacheItemBeforeLock);
    }
    if (lockWasSuccessful == false)
    {
        Thread.Sleep(0);
    }
} while (lockWasSuccessful == false);

inMemoryCache是内存中的HashTable,针对我们前面提出的同步问题,分析这段代码的重点就是要了解这个HashTable中的数据是由何而来,通过分析Cache类中构造函数的下面的这段代码:

Hashtable initialItems = backingStore.Load();

inMemoryCache = Hashtable.Synchronized(initialItems);

我们可以得出:inMemoryCache是由后备存储中的内存哈希表initialItems同步得到的!这样,后面的写内存的操作对我们来书已经不是问题的重点了;它所做的事情就是如果在磁盘驻留型内存中没有找到相同关键字的项,则会在新的内存存储中添加已Key为关键字的存储项;如果磁盘驻留型存储中有相同关键字的话就不会重复的写内存缓存,而只是把inMemoryCache表中的相应内存项拆箱转换成cacheItemBeforeLock,cacheItemBeforeLock是CacheItem类,保存内存项的详细信息。

由此,我们得出结论,当应用程序使用 Add 方法向 CacheManager 对象发送向缓存添加某项的请求时,CacheManager 对象同样会将该请求转发给 Cache 对象。如果已经有一个具有相同关键字的项,则在将新项添加到内存存储和后备存储中之前,Cache 对象会首先删除它。如果后备存储是默认的后备存储 NullBackingStore,则数据只写到内存中。在添加项时,如果缓存项的数量超过预定的限制,那么 BackgroundScheduler 对象就开始进行清理。在添加项时,应用程序可以使用 Add 方法的重载来指定过期策略数组、清除的优先级,以及实现 ICacheItemRefreshAction 接口的对象。还可以使用该对象来刷新缓存中的过期项。

    分析清楚了Add方法的后,获取缓存GateData的思路我们已经基本清晰了,看一下它GetData方法的核心代码:

do
{
    lock (inMemoryCache.SyncRoot)
    {
        cacheItemBeforeLock = (CacheItem)inMemoryCache[key];
        if (IsObjectInCache(cacheItemBeforeLock))
        {    instrumentationProvider.FireCacheAccessed(key, false);
            return null;
        }
        lockWasSuccessful = Monitor.TryEnter(cacheItemBeforeLock);
    }
    if (lockWasSuccessful == false)
    {
        Thread.Sleep(0);
    }
} while (lockWasSuccessful == false);

        当应用程序使用 GetData 方法向 CacheManager 对象发送检索某项的请求时,CacheManager 对象就会将该请求转发给 Cache 对象。Cache对象中的GetData方法首先将数据库中的缓存数据用backingStore的Load方法将数据库Caching表中缓存项加载到哈希表 initialItems中,在将这个哈希表与内存缓存哈希表inMemoryCache同步,如果该项在哈希表inMemoryCache中存在中,它就会从缓存的内存表中返回到应用程序。如果它不在缓存中,该请求会返回 NULL。如果该项位于缓存中但已过期,它也会返回 NULL。

posted @ 2010-06-01 03:46  星火卓越  阅读(526)  评论(0编辑  收藏  举报