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

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

      首先是类关系图:

 

代码级浅析企业库缓存组件_第1张图片

 

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

      一.缓存创建

代码级浅析企业库缓存组件_第2张图片
 

       常见的缓存创建方式为:

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类.

 

      二.缓存轮询

 代码级浅析企业库缓存组件_第3张图片

      缓存配置中有两个参数用在了这里: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则删除,还有个变量记录了删除的个数.如果其等于配置值,则停止删除.

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

 

      三.缓存过期

 代码级浅析企业库缓存组件_第4张图片      代码级浅析企业库缓存组件_第5张图片

      在配置文件中有一个配置与此有关: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)


代码级浅析企业库缓存组件_第6张图片 

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

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

     return  false;
}

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

 

      四.缓存回调

代码级浅析企业库缓存组件_第7张图片 

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

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的方式来操作,则可以必免这个问题,让缓存项始终指向一个内存地址,只是更新他的内容而以.

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

 代码级浅析企业库缓存组件_第8张图片

 

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

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中用的比较多.这个类更具体的信息,各位看官自行谷歌吧.

 

      七.性能记数器

 代码级浅析企业库缓存组件_第9张图片

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

 

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

 

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

 

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

 

你可能感兴趣的:(缓存)