事情的初衷很简单,就是想不用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 ==
0)
return;
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 ==
null)
throw
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