zoukankan      html  css  js  c++  java
  • 代码级浅析企业库缓存组件

           事情的初衷很简单,就是想不用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
     
  • 相关阅读:
    HZNU 2019 Summer training 6
    HZNU 2019 Summer training 5
    HZNU 2019 Summer training 4
    Garlands CodeForces
    HZNU 2019 Summer training 3
    UVA
    HZNU 2019 Summer training 2
    Serge and Dining Room(线段树)
    HZNU 2019 Summer training 1
    【7-10 PAT】树的遍历
  • 原文地址:https://www.cnblogs.com/ljzforever/p/2289909.html
Copyright © 2011-2022 走看看