MemoryCache是.Net Framework 4.0開始提供的記憶體緩存類,使用該類型可以友善的在程式内部緩存資料并對于資料的有效性進行友善的管理,借助該類型可以實作ASP.NET中常用的Cache類的相似功能,并且可以适應更加豐富的使用場景。在使用MemoryCache時常常有各種疑問,資料是怎麼組織的?有沒有可能用更高效的組織和使用方式?資料逾時如何控制?為了夠知其是以然,本文中對于MemoryCache的原理和實作方式進行了深入分析,同時在分析的過程中學習到了許多業界成熟元件的設計思想,為今後的工作打開了更加開闊的思路
MemoryCache是.Net Framework 4.0開始提供的記憶體緩存類,使用該類型可以友善的在程式内部緩存資料并對于資料的有效性進行友善的管理,借助該類型可以實作ASP.NET中常用的Cache類的相似功能,并且可以适應更加豐富的使用場景。在使用MemoryCache時常常有各種疑問,資料是怎麼組織的?有沒有可能用更高效的組織和使用方式?資料逾時如何控制?為了知其是以然,本文中對于MemoryCache的原理和實作方式進行了深入分析,同時在分析的過程中學習到了許多業界成熟元件的設計思想,為今後的工作打開了更加開闊的思路
本文面向的是.net 4.5.1的版本,在後續的.net版本中MemoryCache有略微的不同,歡迎補充
文章内容較長,預計閱讀時間1小時左右
MemoryCache類繼承自ObjectCache抽象類,并且實作了<code>IEnumerable</code>和<code>IDisposable</code>接口。跟ASP.NET常用的Cache類實作了相似的功能,但是MemoryCache更加通用。使用它的時候不必依賴于<code>System.Web</code>類庫,并且在同一個程序中可以使用MemoryCache建立多個執行個體。
在使用MemoryCache的時候通常會有些疑問,這個類到底内部資料是如何組織的?緩存項的逾時是如何處理的?它為什麼宣傳自己是線程安全的?為了回答這些問題,接下來借助Reference Source對于MemoryCache的内部實作一探究竟。
在MemoryCache類内部,資料的組織方式跟MemoryCacheStore、MemoryCacheKey和MemoryCacheEntry這三個類有關,它們的作用分别是:
MemoryCacheStore:承載資料
MemoryCacheKey:構造檢索項
MemoryCacheEntry:緩存内部資料的真實表現形式
MemoryCache和MemoryCacheStore的關系大緻如下圖所示:

從圖上可以直覺的看出,一個MemoryCache執行個體對象可以包含多個MemoryCacheStore對象,具體有幾個需要取決于程式所在的硬體環境,跟CPU數目有關。在MemoryCache的内部,MemoryCacheStore對象就像一個個的小資料庫一樣,承載着各種資料。是以,要了解MemoryCache内部的資料結構,就需要先了解MemoryCacheStore的地位和作用。
該類型是MemoryCache内部真正用于承載資料的容器。它直接管理着程式的記憶體緩存項,既然要承載資料,那麼該類型中必然有些屬性與資料存儲有關。其具體表現是:MemoryCache中有一個類型為<code>HashTable</code>的私有屬性<code>_entries</code>,在該屬性中存儲了它所管理的所有緩存項。
當需要去MemoryCache中擷取資料的時候,MemoryCache所做的第一步就是尋找存儲被查找key的MemoryCacheStore對象,而并非是我們想象中的直接去某個<code>Dictionary</code>類型或者<code>HashTable</code>類型的對象中直接尋找結果。
在MemoryCache中查找MemoryCacheStore的方式也挺有趣,主要的邏輯在MemoryCache的<code>GetStore</code>方法中,源碼如下(為了了解友善增加了部分注釋):
既然可能存在多個MemoryCacheStore對象,那麼就需要有一定的規則來決定每個Store中存儲的内容。從源碼中可以看出,MemoryCache使用的是CPU的核數作為掩碼,并利用該掩碼和key的hashcode來計算緩存項的歸屬地,确實是簡單而高效。
MemoryCacheKey的類功能相對比較簡單,主要用于封裝緩存項的key及相關的常用方法。
上文提到了MemoryCacheStore中<code>_entries</code>的初始化方式,在構造函數的參數是一個MemoryCacheEqualityComparer對象,這是個什麼東西,又是起到什麼作用的呢?
MemoryCacheEqualityComparer類實作了<code>IEqualityComparer</code>接口,其中便定義了哈希表中判斷值相等的方法,來分析下源碼:
從代碼中可以看出,MemoryCacheEqualityComparer的真正作用就是定義MemoryCacheKey的比較方法。判斷兩個兩個MemoryCacheKey是否相等使用的就是MemoryCacheKey中的Key屬性。是以我們在MemoryCache中擷取和設定相關的内容時,使用的都是對于MemoryCacheKey的相關運算結果。
此類型是緩存項在記憶體中真正的存在形式。它繼承自MemoryCacheKey類型,并在此基礎上增加了很多的屬性和方法,比如判斷是否逾時等。
先來看下該類的整體情況:
總的來說,MemoryCacheEntry中的屬性和方法主要為三類:
緩存的内容相關,如Key、Value
緩存内容的狀态相關,如State、HasExpiration方法等
緩存内容的相關事件相關,如CallCacheEntryRemovedCallback方法、CallNotifyOnChanged方法等
了解了MemoryCache中資料的組織方式後,可以幫助了解資料是如何從MemoryCache中被一步步查詢得到的。
從MemoryCache中擷取資料經曆了哪些過程呢?從整體來講,大緻可以分為兩類:擷取資料和驗證有效性。
以流程圖的方式表達上述步驟如下:
詳細的步驟是這樣的:
校驗查詢參數RegionName和Key,進行有效性判斷
構造MemoryCacheKey對象,用于後續步驟查詢和比對現有資料
擷取MemoryCacheStore對象,縮小查詢範圍
從MemoryCacheStore的HashTable類型屬性中提取MemoryCacheEntry對象,得到key對應的資料
判斷MemoryCacheEntry對象的有效性,進行資料驗證工作
處理MemoryCacheEntry的滑動逾時時間等通路相關的邏輯
看到此處,不禁想起之前了解的其他緩存系統中的設計,就像曆史有時會有驚人的相似性,進行了良好設計的緩存系統在某些時候看起來确實有很多相似的地方。通過學習他人的優良設計,從中可以學到很多的東西,比如接下來的緩存逾時機制。
MemoryCache在設定緩存項時可以選擇永久緩存或者在逾時後自動消失。其中緩存政策可以選擇固定逾時時間和滑動逾時時間的任意一種(注意這兩種逾時政策隻能二選一,下文中會解釋為什麼有這樣的規則)。
緩存項的逾時管理機制是緩存系統(比如Redis和MemCached)的必備功能,Redis中有主動檢查和被動觸發兩種,MemCached采用的是被動觸發檢查,那麼記憶體緩存MemoryCache内部是如何管理緩存項的逾時機制?
MemoryCache對于緩存項的逾時管理機制與Redis類似,也是有兩種:定期删除和惰性删除。
既然MemoryCache内部的資料是以MemoryCacheStore對象為機關進行管理,那麼定期檢查也很有可能是MemoryCacheStore對象内部的一種行為。
通過仔細閱讀源碼,發現MemoryCacheStore的構造函數中調用了<code>InitDisposableMembers()</code>這個方法,該方法的代碼如下:
其中跟本章節讨論的逾時機制有關的就是<code>_expires</code>這個屬性。由于《.NET reference source》中并沒有這個CacheExpires類的相關源碼,無法得知具體的實作方式,是以從Mono項目中找到同名的方法探索該類型的具體實作。
通過Mono中的源代碼可以看出,在CacheExpires内部使用了一個定時器,通過定時器觸發定時的檢查。在觸發時使用的是CacheEntryCollection類的<code>FlushItems</code>方法。該方法的實作如下;
在<code>FlushItems(***)</code>的邏輯中,通過周遊所有的緩存項并且比對了逾時時間,将發現的逾時緩存項執行Remove操作進行清理,實作緩存項的定期删除操作。通過Mono項目中該類的功能推斷,在.net framework中的實作應該也是有類似的功能,即每一個MemoryCache的執行個體都會有一個負責定時檢查的任務,負責處理掉所有逾時的緩存項。
除了定時删除以外,MemoryCache還實作了惰性删除的功能,這項功能的實作相對于定時删除簡單的多,而且非常的實用。
惰性删除是什麼意思呢?簡單的講就是在使用緩存項的時候判斷緩存項是否應該被删除,而不用等到被專用的清理任務清理。
前文描述過MemoryCache中資料的組織方式,既然是在使用時觸發的邏輯,是以惰性删除必然與MemoryCacheStore擷取緩存的方法有關。來看下它的<code>Get</code>方法的内部邏輯:
從代碼中可以看出,MemoryCacheStore查找到相關的key對應的緩存項以後,并沒有直接傳回,而是先檢查了緩存項目的逾時時間。如果緩存項逾時,則删除該項并傳回null。這就是MemoryCache中惰性删除的實作方式。
向MemoryCache執行個體中添加緩存項的時候,可以選擇三種過期政策:
永不逾時
絕對逾時
滑動逾時
緩存政策在緩存項添加/更新緩存時(無論是使用Add或者Set方法)指定,通過在操作緩存時指定<code>CacheItemPolicy</code>對象來達到設定緩存逾時政策的目的。
緩存逾時政策并不能随意的指定,在MemoryCache内部對于<code>CacheItemPolicy</code>對象有内置的檢查機制。先看下源碼:
總結下源碼中的邏輯,逾時政策的設定有如下幾個規則:
絕對逾時和滑動逾時不能同時存在(這是前文中說兩者二選一的原因)
如果滑動逾時時間小于0或者大于1年也不行
<code>RemovedCallback</code>和<code>UpdateCallback</code>不能同時設定
緩存的<code>Priority</code>屬性不能是超出枚舉範圍(Default和NotRemovable)
根據MSDN的描述:MemoryCache是線程安全的。那麼說明,在操作MemoryCache中的緩存項時,MemoryCache保證程式的行為都是原子性的,而不會出現多個線程共同操作導緻的資料污染等問題。
那麼,MemoryCache是如何做到這一點的?
MemoryCache在内部使用加鎖機制來保證資料項操作的原子性。該鎖以每個MemoryCacheStore為機關,即同一個MemoryCacheStore内部的資料共享同一個鎖,而不同MemoryCacheStore之間互不影響。
存在加鎖邏輯的有如下場景:
周遊MemoryCache緩存項
向MemoryCache添加/更新緩存項
執行MemoryCache析構
移除MemoryCache中的緩存項
其他的場景都比較好了解,其中值得一提的就是場景1(周遊)的實作方式。在MemoryCache中,使用了鎖加複制的方式來處理周遊的需要,保證在周遊過程中不會發生異常。
在.net 4.5.1中的周遊的實作方式是這樣的:
其中<code>store.CopyTo(h);</code>的實作方式是在MemoryCacheStore中定義的,也就是說,每個Store的加鎖解鎖都是獨立的過程,縮小鎖機制影響的範圍也是提升性能的重要手段。CopyTo方法的主要邏輯是在鎖機制控制下的簡單的周遊:
有些出乎意料,在周遊MemoryCache的時候,為了實作周遊過程中的線程安全,實作的方式居然是将資料另外拷貝了一份。當然了,說是完全拷貝一份也不盡然,如果緩存項本來就是引用類型,被拷貝的也隻是個指針而已。不過看起來最好還是少用為妙,萬一緩存的都是些基礎類型,一旦資料量較大,在周遊過程中的記憶體壓力就不是可以忽略的問題了。
在本文中以MemoryCache對于資料的組織管理和使用為軸線,深入的分析了MemoryCache對于一些日常應用有直接關聯的功能的實作方式。MemoryCache通過多個MemoryCacheStore對象将資料分散到不同的HastTable中,并且使用加鎖的方式在每個Store内部保證操作是線程安全的,同時這種邏輯也在一定程度上改善了全局鎖的性能問題。為了實作對于緩存項逾時的管理,MemoryCache采取了兩種不同的管理措施,雙管齊下,有效保證了緩存項的逾時管理的有效性,并在逾時後及時移除相關的緩存以釋放記憶體資源。通過對于這些功能的分析,了解了MemoryCache内部的資料結構和資料查詢方式,為今後的工作掌握了許多有指導性意義的經驗。
本文還會有後續的篇章,敬請期待~~
MemoryCache 類
MemoryCache類源碼
基于多級緩存的充電系統優化實踐
深入了解redis_memcached失效原理
CacheExpires源碼
CacheEntryCollection源碼
CacheItemPolicy 類
線程安全