代码级浅析企业库缓存组件

       事情的初衷很简单,就是想不用xml配置来使用其缓存组件,试了很多遍都无法成功.不得已安装了其源码大略分析一遍,才总算成功.后来又一想,既然分析就分析的彻底一点吧,顺便看看国外的高手们是怎么架构组件,书写代码的,于是就有了这篇文章.企业库为5.0版本.

      首先是类关系图:

 

 

      缓存组件的整体结构为CacheManager -> Cache -> CacheItem,其中CacheItem为缓存项,其有Key有Value,还有本缓存项的过期策略及删除时的回调函数.Cache为缓存,除管理CacheItem外,还负责管理缓存性能计算器及缓存持久化.CacheManager为Cache类的包装类,用户调用接口,也是最为我们熟悉的,其代理了Cache类的缓存操作方法,此外还有过期轮询等.下面就来一步一步的分析.

      一.缓存创建


 

       常见的缓存创建方式为:

ICacheManager manager = Microsoft.Practices.EnterpriseLibrary.Caching.CacheFactory.GetCacheManager();

      其实还有一种创建方式:

CacheManagerFactory factory = new CacheManagerFactory();
ICacheManager manager = factory.CreateDefault();

      这两种方式创建缓存,本质上调用的都是这段代码:

EnterpriseLibraryContainer.Current.GetInstance<ICacheManager>(cacheManagerName)

      EnterpriseLibraryContainer对象,又称企业库容器对象,说白了就是个依赖注入的容器,封装了unity框架.更具体的说明,请参见我另写的一篇文章:代码级浅析企业库对象创建

      这段代码的意思,就是返回一个注册了的实现了ICacheManager接口的类.这里实际返回的是CacheManager类.

 

      二.缓存轮询

 

      缓存配置中有两个参数用在了这里:numberToRemoveWhenScavenging和maximumElementsInCacheBeforeScavenging.参数的名字已经把他们的用途说的很明白了:缓存里存储了多少项数据后启动清理及每次移除多少项数据.

 

1 public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
2 {
3     realCache.Add(key, value, scavengingPriority, refreshAction, expirations);
4     
5     backgroundScheduler.StartScavengingIfNeeded();
6 }

     

      代码第六行说的很清楚,每次新增缓存项时,就会检查缓存项是否超过了配置值.如果超过了,就会通过多线程的方式在线程池中执行以下方法

 

internal void Scavenge()
{
    int pendingScavengings = Interlocked.Exchange(ref scavengePending, 0);
    int timesToScavenge = ((pendingScavengings - 1) / scavengerTask.NumberOfItemsToBeScavenged) + 1;
    while (timesToScavenge > 0)
    {
        scavengerTask.DoScavenging();
        --timesToScavenge;
    }
}

      然后又调用了ScavengerTask类的DoScavenging方法

 1 public void DoScavenging()
 2 {
 3     if (NumberOfItemsToBeScavenged == 0return;
 4 
 5     if (IsScavengingNeeded())
 6     {
 7         Hashtable liveCacheRepresentation = cacheOperations.CurrentCacheState;
 8 
 9         ResetScavengingFlagInCacheItems(liveCacheRepresentation);
10         SortedList scavengableItems = SortItemsForScavenging(liveCacheRepresentation);
11         RemoveScavengableItems(scavengableItems);
12     }
13 }


      这是实际实现功能的方法.如果缓存项多于配置值时就会执行.第9行代码将缓存项的eligibleForScavenging字段设为true,表示可以对其做扫描移除工作.其实与这个字段相对应的EligibleForScavenging属性并不是简单的返回这个字段,其还考虑了缓存项的优先级,只有eligibleForScavenging为true且优先级不为最高(NotRemovable),才返回true.第10行即对缓存项做排序工作,以优先级为排序字段将缓存排序,优先级最高的排在后面,表示最后才被删除.第11行则是真正删除方法.在其方法体内会遍例排序之后的缓存项,如果EligibleForScavenging属性为true则删除,还有个变量记录了删除的个数.如果其等于配置值,则停止删除.

      可以看到缓存轮询与缓存过期无关,缓存优先级与缓存过期也没关系.那么经过扫描后的缓存,仍然可能存在已过期项.

 

      三.缓存过期

      

      在配置文件中有一个配置与此有关:expirationPollFrequencyInSeconds,则每隔多长时间对缓存项进行一次过期检查.

pollTimer.StartPolling(backgroundScheduler.ExpirationTimeoutExpired);

      在缓存容器CacheManger创建时就会开始计时

pollTimer = new Timer(callbackMethod, null, expirationPollFrequencyInMilliSeconds, expirationPollFrequencyInMilliSeconds);

      其本质是一个Timer对象,定时回调指定的函数.这里的回调函数其实是BackgroundScheduler对象的Expire方法:

 

internal void Expire()
{
    expirationTask.DoExpirations();
}

      其又调用了ExpirationTask对象的DoExpirations方法:

 

1 public void DoExpirations()
2 {
3     Hashtable liveCacheRepresentation = cacheOperations.CurrentCacheState;
4     MarkAsExpired(liveCacheRepresentation);
5     PrepareForSweep();
6     int expiredItemsCount = SweepExpiredItemsFromCache(liveCacheRepresentation);
7     
8     if(expiredItemsCount > 0) instrumentationProvider.FireCacheExpired(expiredItemsCount);
9 }

      这里是过期的实际功能方法.代码第四行遍例缓存,将已过期的缓存的WillBeExpired属性标记为true,第6行则是将所有WillBeExpired属性标记为true的缓存项进行删除.下面来看如何判断一个缓存项是否过期.

      其实新增缓存的方法有多个重载,其中一个就是

public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)


 

      常见的有绝对时间,相对时间,文件依赖等.可以看到,一个缓存项,是可以有多个缓存依赖的,或者叫缓存过期策略.如果其中任意一个过期,则缓存项过期.

public bool HasExpired()
{
    foreach (ICacheItemExpiration expiration in expirations)
    {
        if (expiration.HasExpired())
        {
            return true;
        }
    }

    return false;
}

      对缓存项过期的管理,除定时轮询外,在取值的时候,也会判断.

 

      四.缓存回调

 

      如果缓存项从缓存中移除,则会触发回调:

RefreshActionInvoker.InvokeRefreshAction(cacheItemBeforeLock, removalReason, instrumentationProvider);

      实际上是以多线程的方式在线程池中执行回调函数

 

public void InvokeOnThreadPoolThread()
{
    ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolRefreshActionInvoker));
}

private void ThreadPoolRefreshActionInvoker(object notUsed)
{
    try
    {
        RefreshAction.Refresh(KeyToRefresh, RemovedData, RemovalReason);
    }
    catch (Exception e)
    {
        InstrumentationProvider.FireCacheCallbackFailed(KeyToRefresh, e);
    }
}

 

      五.线程安全

      企业库用了大量的代码来实现了缓存增,删,取值的线程安全.它用了两个锁来实现线程安全.新增操作最复杂,就分析它吧

 1 public void Add(string key, object value, CacheItemPriority scavengingPriority, ICacheItemRefreshAction refreshAction, params ICacheItemExpiration[] expirations)
 2 {
 3     ValidateKey(key);
 4 
 5     CacheItem cacheItemBeforeLock = null;
 6     bool lockWasSuccessful = false;
 7 
 8     do
 9     {
10         lock (inMemoryCache.SyncRoot)
11         {
12             if (inMemoryCache.Contains(key) == false)
13             {
14                 cacheItemBeforeLock = new CacheItem(key, addInProgressFlag, CacheItemPriority.NotRemovable, null);
15                 inMemoryCache[key] = cacheItemBeforeLock;
16             }
17             else
18             {
19                 cacheItemBeforeLock = (CacheItem)inMemoryCache[key];
20             }
21 
22             lockWasSuccessful = Monitor.TryEnter(cacheItemBeforeLock);
23         }
24 
25         if (lockWasSuccessful == false)
26         {
27             Thread.Sleep(0);
28         }
29     } while (lockWasSuccessful == false);
30 
31     try
32     {
33         cacheItemBeforeLock.TouchedByUserAction(true);
34 
35         CacheItem newCacheItem = new CacheItem(key, value, scavengingPriority, refreshAction, expirations);
36         try
37         {
38             backingStore.Add(newCacheItem);
39             cacheItemBeforeLock.Replace(value, refreshAction, scavengingPriority, expirations);
40             inMemoryCache[key] = cacheItemBeforeLock;
41         }
42         catch
43         {
44             backingStore.Remove(key);
45             inMemoryCache.Remove(key);
46             throw;
47         }
48         instrumentationProvider.FireCacheUpdated(1, inMemoryCache.Count);
49     }
50     finally
51     {
52         Monitor.Exit(cacheItemBeforeLock);
53     }  
54 
55 }

      代码第10行首先锁住整个缓存,然后新增一个缓存项并把他加入缓存,然后在第22行尝试锁住缓存项并释放缓存锁.如果没有成功锁上缓存项,则重复以上动作.在代码14与15行可以看到,这时加入缓存的缓存项并没有存储实际的值.他相当于一个占位符,表示这个位置即将有值.如果成功锁住了缓存项,代码第39号则是以覆盖的方式将真正的值写入缓存.

      这里为什么要用两个锁呢?我觉得这是考虑到性能.用一个锁锁住整个缓存完成整个操作固然没有问题,但是如果代码第33行或第38号耗时过多的话,会影响整个系统的性能,特别是第38行,涉及IO操作,更是要避免!那为什么在第14行使用的是占位符而不是真正的存储呢?我觉得这也是考虑到性能.这里的新增操作包括两个含义,缓存中不存在则新增,存在则更新.这里考虑的是更新的问题.通常做法是让缓存项指向新对象,这样先前指向的对象就会成为垃圾对象.在高负载的应用程序里,这会产生大量的垃圾对象,影响了系统的性能.如果通过Replace的方式来操作,则可以必免这个问题,让缓存项始终指向一个内存地址,只是更新他的内容而以.

      六.离线存储(缓存持久化)

 

 

      通过这个功能,可以让内存数据保存在硬盘上.在缓存初始化的时候会从硬盘上加载数据

Hashtable initialItems = backingStore.Load();
inMemoryCache = Hashtable.Synchronized(initialItems);

      在新增与删除的时候,会在硬盘上做相应的操作

backingStore.Add(newCacheItem);

 

backingStore.Remove(key);

      在企业库里,是通过.net的IsolatedStorageFile类来实现其功能的.每个缓存都对应一个目录

 

private void Initialize()
{
    store = IsolatedStorageFile.GetUserStoreForDomain();
    if (store.GetDirectoryNames(storageAreaName).Length == 0)
    {
        // avoid creating if already exists - work around for partial trust
        store.CreateDirectory(storageAreaName);
    }
}

      每个缓存项则是一个子目录,缓存项里的每个对象则被序列化成单个文件

 

return Path.Combine(storageAreaName, itemToLocate);

 

 1 public IsolatedStorageCacheItem(IsolatedStorageFile storage, string itemDirectoryRoot, IStorageEncryptionProvider encryptionProvider)
 2 {
 3     if (storage == nullthrow new ArgumentNullException("storage");
 4 
 5     int retriesLeft = MaxRetries;
 6     while (true)
 7     {
 8         // work around - attempt to write a file in the folder to determine whether delayed io
 9         // needs to be processed
10         // since only a limited number of retries will be attempted, some extreme cases may 
11         // still fail if file io is deferred long enough.
12         // while it's still possible that the deferred IO is still pending when the item that failed
13         // to be added is removed by the cleanup code, thus making the cleanup fail, 
14         // the item should eventually be removed (by the original removal)
15         try
16         {
17             storage.CreateDirectory(itemDirectoryRoot);
18 
19             // try to write a file
20             // if there is a pending operation or the folder is gone, this should find the problem
21             // before writing an actual field is attempted
22             using (IsolatedStorageFileStream fileStream =
23                 new IsolatedStorageFileStream(itemDirectoryRoot + @"\sanity-check.txt", FileMode.Create, FileAccess.Write, FileShare.None, storage))
24             { }
25             break;
26         }
27         catch (UnauthorizedAccessException)
28         {
29             // there are probably pending operations on the directory - retry if allowed
30             if (retriesLeft-- > 0)
31             {
32                 Thread.Sleep(RetryDelayInMilliseconds);
33                 continue;
34             }
35 
36             throw;
37         }
38         catch (DirectoryNotFoundException)
39         {
40             // a pending deletion on the directory was processed before creating the file
41             // but after attempting to create it - retry if allowed
42             if (retriesLeft-- > 0)
43             {
44                 Thread.Sleep(RetryDelayInMilliseconds);
45                 continue;
46             }
47 
48             throw;
49         }
50     }
51 
52     keyField = new IsolatedStorageCacheItemField(storage, "Key", itemDirectoryRoot, encryptionProvider);
53     valueField = new IsolatedStorageCacheItemField(storage, "Val", itemDirectoryRoot, encryptionProvider);
54     scavengingPriorityField = new IsolatedStorageCacheItemField(storage, "ScPr", itemDirectoryRoot, encryptionProvider);
55     refreshActionField = new IsolatedStorageCacheItemField(storage, "RA", itemDirectoryRoot, encryptionProvider);
56     expirationsField = new IsolatedStorageCacheItemField(storage, "Exp", itemDirectoryRoot, encryptionProvider);
57     lastAccessedField = new IsolatedStorageCacheItemField(storage, "LA", itemDirectoryRoot, encryptionProvider);
58 }

 

      至于IsolatedStorageFile这个类.我查了一下,这个类在sl或wp中用的比较多.这个类更具体的信息,各位看官自行谷歌吧.

 

      七.性能记数器

 

      这个就没什么说的了,就是将各种缓存的操作次数记录下来,包括成功的次数与失败的次数.CachingInstrumentationProvider类里包含了13个EnterpriseLibraryPerformanceCounter类型的计数器.这种计数器其实是系统计数器PerformanceCounter类型的封装.这13个计数器分别为:命中/秒,总命中数,未命中/秒,总未命中数,命中比,缓存总记问数,过期数/秒,总过期数,轮询清除/秒,总轮询清除数,缓存项总数,更新缓存项/秒,更新缓存项总数.更加具体的信息,各位看官自行谷歌吧.

 

      至此,缓存组件的分析告一段落了.我感觉缓存组件比上一篇写到的对象创建模块要好的很多,代码结构清晰,职责分明.里面涉及的众多技术运用,如多线程,锁,性能,面向接口编程等也较为合理,算的上是一个学习的样本.

 

      文章的最后放上一段我最喜欢的一句话吧:

 

      “设计软件有两种策略,一是做的非常的简单,以至于明显没有缺陷。二是做的非常的复杂,以至于没有明显的缺陷。” – C.A.R. Hoare
 

 

posted @ 2011-12-19 00:50  永远的阿哲  阅读(2075)  评论(6编辑  收藏  举报