代碼級淺析企業庫緩存組件
事情的初衷很簡單,就是想不用xml配置來使用其緩存組件,試了很多遍都無法成功.不得已安裝了其源碼大略分析一遍,才總算成功.后來又一想,既然分析就分析的徹底一點吧,順便看看國外的高手們是怎么架構組件,書寫代碼的,于是就有了這篇文章.企業庫為5.0版本.
首先是類關系圖:

緩存組件的整體結構為CacheManager -> Cache -> CacheItem,其中CacheItem為緩存項,其有Key有Value,還有本緩存項的過期策略及刪除時的回調函數.Cache為緩存,除管理CacheItem外,還負責管理緩存性能計算器及緩存持久化.CacheManager為Cache類的包裝類,用戶調用接口,也是最為我們熟悉的,其代理了Cache類的緩存操作方法,此外還有過期輪詢等.下面就來一步一步的分析.
一.緩存創建

常見的緩存創建方式為:
其實還有一種創建方式:
ICacheManager manager = factory.CreateDefault();
這兩種方式創建緩存,本質上調用的都是這段代碼:
EnterpriseLibraryContainer對象,又稱企業庫容器對象,說白了就是個依賴注入的容器,封裝了unity框架.更具體的說明,請參見我另寫的一篇文章:代碼級淺析企業庫對象創建
這段代碼的意思,就是返回一個注冊了的實現了ICacheManager接口的類.這里實際返回的是CacheManager類.
二.緩存輪詢

緩存配置中有兩個參數用在了這里:numberToRemoveWhenScavenging和maximumElementsInCacheBeforeScavenging.參數的名字已經把他們的用途說的很明白了:緩存里存儲了多少項數據后啟動清理及每次移除多少項數據.
2 {
3 realCache.Add(key, value, scavengingPriority, refreshAction, expirations);
4
5 backgroundScheduler.StartScavengingIfNeeded();
6 }
代碼第六行說的很清楚,每次新增緩存項時,就會檢查緩存項是否超過了配置值.如果超過了,就會通過多線程的方式在線程池中執行以下方法
{
int pendingScavengings = Interlocked.Exchange(ref scavengePending, 0);
int timesToScavenge = ((pendingScavengings - 1) / scavengerTask.NumberOfItemsToBeScavenged) + 1;
while (timesToScavenge > 0)
{
scavengerTask.DoScavenging();
--timesToScavenge;
}
}
然后又調用了ScavengerTask類的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,則每隔多長時間對緩存項進行一次過期檢查.
在緩存容器CacheManger創建時就會開始計時
其本質是一個Timer對象,定時回調指定的函數.這里的回調函數其實是BackgroundScheduler對象的Expire方法:
{
expirationTask.DoExpirations();
}
其又調用了ExpirationTask對象的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的緩存項進行刪除.下面來看如何判斷一個緩存項是否過期.
其實新增緩存的方法有多個重載,其中一個就是
常見的有絕對時間,相對時間,文件依賴等.可以看到,一個緩存項,是可以有多個緩存依賴的,或者叫緩存過期策略.如果其中任意一個過期,則緩存項過期.
{
foreach (ICacheItemExpiration expiration in expirations)
{
if (expiration.HasExpired())
{
return true;
}
}
return false;
}
對緩存項過期的管理,除定時輪詢外,在取值的時候,也會判斷.
四.緩存回調
如果緩存項從緩存中移除,則會觸發回調:
實際上是以多線程的方式在線程池中執行回調函數
{
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadPoolRefreshActionInvoker));
}
private void ThreadPoolRefreshActionInvoker(object notUsed)
{
try
{
RefreshAction.Refresh(KeyToRefresh, RemovedData, RemovalReason);
}
catch (Exception e)
{
InstrumentationProvider.FireCacheCallbackFailed(KeyToRefresh, e);
}
}
五.線程安全
企業庫用了大量的代碼來實現了緩存增,刪,取值的線程安全.它用了兩個鎖來實現線程安全.新增操作最復雜,就分析它吧
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的方式來操作,則可以必免這個問題,讓緩存項始終指向一個內存地址,只是更新他的內容而以.
六.離線存儲(緩存持久化)

通過這個功能,可以讓內存數據保存在硬盤上.在緩存初始化的時候會從硬盤上加載數據
inMemoryCache = Hashtable.Synchronized(initialItems);
在新增與刪除的時候,會在硬盤上做相應的操作
在企業庫里,是通過.net的IsolatedStorageFile類來實現其功能的.每個緩存都對應一個目錄
{
store = IsolatedStorageFile.GetUserStoreForDomain();
if (store.GetDirectoryNames(storageAreaName).Length == 0)
{
// avoid creating if already exists - work around for partial trust
store.CreateDirectory(storageAreaName);
}
}
每個緩存項則是一個子目錄,緩存項里的每個對象則被序列化成單個文件
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個計數器分別為:命中/秒,總命中數,未命中/秒,總未命中數,命中比,緩存總記問數,過期數/秒,總過期數,輪詢清除/秒,總輪詢清除數,緩存項總數,更新緩存項/秒,更新緩存項總數.更加具體的信息,各位看官自行谷歌吧.
至此,緩存組件的分析告一段落了.我感覺緩存組件比上一篇寫到的對象創建模塊要好的很多,代碼結構清晰,職責分明.里面涉及的眾多技術運用,如多線程,鎖,性能,面向接口編程等也較為合理,算的上是一個學習的樣本.
文章的最后放上一段我最喜歡的一句話吧:

浙公網安備 33010602011771號