深刻理解.NET MemoryCache

摘要

MemoryCache是.Net Framework 4.0開始提供的內存緩存類,使用該類型能夠方便的在程序內部緩存數據並對於數據的有效性進行方便的管理,藉助該類型能夠實現ASP.NET中經常使用的Cache類的類似功能,而且能夠適應更加豐富的使用場景。在使用MemoryCache時經常有各類疑問,數據是怎麼組織的?有沒有可能用更高效的組織和使用方式?數據超時如何控制?爲了知其因此然,本文中對於MemoryCache的原理和實現方式進行了深刻分析,同時在分析的過程當中學習到了許多業界成熟組件的設計思想,爲從此的工做打開了更加開闊的思路html

本文面向的是.net 4.5.1的版本,在後續的.net版本中MemoryCache有略微的不一樣,歡迎補充git

文章內容較長,預計閱讀時間1小時左右github


MemoryCache類繼承自ObjectCache抽象類,而且實現了IEnumerableIDisposable接口。跟ASP.NET經常使用的Cache類實現了類似的功能,可是MemoryCache更加通用。使用它的時候沒必要依賴於System.Web類庫,而且在同一個進程中可使用MemoryCache建立多個實例。redis

在使用MemoryCache的時候一般會有些疑問,這個類到底內部數據是如何組織的?緩存項的超時是如何處理的?它爲何宣傳本身是線程安全的?爲了回答這些問題,接下來藉助Reference Source對於MemoryCache的內部實現一探究竟。數據庫

MemoryCache內部數據結構

在MemoryCache類內部,數據的組織方式跟MemoryCacheStore、MemoryCacheKey和MemoryCacheEntry這三個類有關,它們的做用分別是:c#

  • MemoryCacheStore:承載數據
  • MemoryCacheKey:構造檢索項
  • MemoryCacheEntry:緩存內部數據的真實表現形式

MemoryCache和MemoryCacheStore的關係大體以下圖所示:緩存

MemoryCache主要的三個類型的關係圖

從圖上能夠直觀的看出,一個MemoryCache實例對象能夠包含多個MemoryCacheStore對象,具體有幾個須要取決於程序所在的硬件環境,跟CPU數目有關。在MemoryCache的內部,MemoryCacheStore對象就像一個個的小數據庫同樣,承載着各類數據。因此,要理解MemoryCache內部的數據結構,就須要先理解MemoryCacheStore的地位和做用。安全

MemoryCacheStore

該類型是MemoryCache內部真正用於承載數據的容器。它直接管理着程序的內存緩存項,既然要承載數據,那麼該類型中必然有些屬性與數據存儲有關。其具體表現是:MemoryCache中有一個類型爲HashTable的私有屬性_entries,在該屬性中存儲了它所管理的全部緩存項。markdown

Hashtable _entries = new Hashtable(new MemoryCacheEqualityComparer());

當須要去MemoryCache中獲取數據的時候,MemoryCache所作的第一步就是尋找存儲被查找key的MemoryCacheStore對象,而並不是是咱們想象中的直接去某個Dictionary類型或者HashTable類型的對象中直接尋找結果。數據結構

在MemoryCache中查找MemoryCacheStore的方式也挺有趣,主要的邏輯在MemoryCache的GetStore方法中,源碼以下(爲了理解方便增長了部分註釋):

internal MemoryCacheStore GetStore(MemoryCacheKey cacheKey) {
    int hashCode = cacheKey.Hash;//獲取key有關的hashCode值
    if (hashCode < 0) {
        //避免出現負數
        hashCode = (hashCode == Int32.MinValue) ? 0 : -hashCode;
    }
    int idx = hashCode & _storeMask;
    //_storeMask跟CPU的數目一致,經過&進行按位與計算獲取到對應的Store
    //本處代碼是.NET 4.5的樣子,在.NET Framework 4.7.2版本已經改爲了使用%進行取餘計算,對於正整數來講實際結果是同樣的。
    return _stores[idx];
}

既然可能存在多個MemoryCacheStore對象,那麼就須要有必定的規則來決定每一個Store中存儲的內容。從源碼中能夠看出,MemoryCache使用的是CPU的核數做爲掩碼,並利用該掩碼和key的hashcode來計算緩存項的歸屬地,確實是簡單而高效。

MemoryCacheKey

MemoryCacheKey的類功能相對比較簡單,主要用於封裝緩存項的key及相關的經常使用方法。

上文提到了MemoryCacheStore中_entries的初始化方式,在構造函數的參數是一個MemoryCacheEqualityComparer對象,這是個什麼東西,又是起到什麼做用的呢?

MemoryCacheEqualityComparer類實現了IEqualityComparer接口,其中便定義了哈希表中判斷值相等的方法,來分析下源碼:

internal class MemoryCacheEqualityComparer: IEqualityComparer {

    bool IEqualityComparer.Equals(Object x, Object y) {
        Dbg.Assert(x != null && x is MemoryCacheKey);
        Dbg.Assert(y != null && y is MemoryCacheKey);

        MemoryCacheKey a, b;
        a = (MemoryCacheKey)x;
        b = (MemoryCacheKey)y;
        //MemoryCacheKey的Key屬性就是咱們在獲取和設置緩存時使用的key值
        return (String.Compare(a.Key, b.Key, StringComparison.Ordinal) == 0);
    }

    int IEqualityComparer.GetHashCode(Object obj) {
        MemoryCacheKey cacheKey = (MemoryCacheKey) obj;
        return cacheKey.Hash;
    }
}

從代碼中能夠看出,MemoryCacheEqualityComparer的真正做用就是定義MemoryCacheKey的比較方法。判斷兩個兩個MemoryCacheKey是否相等使用的就是MemoryCacheKey中的Key屬性。所以咱們在MemoryCache中獲取和設置相關的內容時,使用的都是對於MemoryCacheKey的相關運算結果。

MemoryCacheEntry

此類型是緩存項在內存中真正的存在形式。它繼承自MemoryCacheKey類型,並在此基礎上增長了不少的屬性和方法,好比判斷是否超時等。

先來看下該類的總體狀況:

MemoryCacheEntry的方法和屬性

總的來講,MemoryCacheEntry中的屬性和方法主要爲三類:

  1. 緩存的內容相關,如Key、Value
  2. 緩存內容的狀態相關,如State、HasExpiration方法等
  3. 緩存內容的相關事件相關,如CallCacheEntryRemovedCallback方法、CallNotifyOnChanged方法等

理解了MemoryCache中數據的組織方式後,能夠幫助理解數據是如何從MemoryCache中被一步步查詢獲得的。

如何從MemoryCahe中查詢數據

從MemoryCache中獲取數據經歷了哪些過程呢?從總體來說,大體能夠分爲兩類:獲取數據和驗證有效性。

以流程圖的方式表達上述步驟以下:

MemoryCache查詢數據主要流程

詳細的步驟是這樣的:

  1. 校驗查詢參數RegionName和Key,進行有效性判斷
  2. 構造MemoryCacheKey對象,用於後續步驟查詢和比對現有數據
  3. 獲取MemoryCacheStore對象,縮小查詢範圍
  4. 從MemoryCacheStore的HashTable類型屬性中提取MemoryCacheEntry對象,獲得key對應的數據
  5. 判斷MemoryCacheEntry對象的有效性,進行數據驗證工做
  6. 處理MemoryCacheEntry的滑動超時時間等訪問相關的邏輯

看到此處,不由想起以前瞭解的其餘緩存系統中的設計,就像歷史有時會有驚人的類似性,進行了良好設計的緩存系統在某些時候看起來確實有不少類似的地方。經過學習他人的優良設計,從中能夠學到不少的東西,好比接下來的緩存超時機制。

MemoryCache超時機制

MemoryCache在設置緩存項時能夠選擇永久緩存或者在超時後自動消失。其中緩存策略能夠選擇固定超時時間和滑動超時時間的任意一種(注意這兩種超時策略只能二選一,下文中會解釋爲何有這樣的規則)。

緩存項的超時管理機制是緩存系統(好比Redis和MemCached)的必備功能,Redis中有主動檢查和被動觸發兩種,MemCached採用的是被動觸發檢查,那麼內存緩存MemoryCache內部是如何管理緩存項的超時機制?

MemoryCache對於緩存項的超時管理機制與Redis相似,也是有兩種:按期刪除和惰性刪除。

按期刪除

既然MemoryCache內部的數據是以MemoryCacheStore對象爲單位進行管理,那麼按期檢查也頗有多是MemoryCacheStore對象內部的一種行爲。

經過仔細閱讀源碼,發現MemoryCacheStore的構造函數中調用了InitDisposableMembers()這個方法,該方法的代碼以下:

private void InitDisposableMembers() {
    //_insertBlock是MemoryCacheStore的私有屬性
    //_insertBlock的聲明方式是:private ManualResetEvent _insertBlock;
    _insertBlock = new ManualResetEvent(true);
    //_expires是MemoryCacheStore的私有屬性
    //_expires的聲明方式是:private CacheExpires _expires;
    _expires.EnableExpirationTimer(true);
}

其中跟本章節討論的超時機制有關的就是_expires這個屬性。因爲《.NET reference source》中並無這個CacheExpires類的相關源碼,沒法得知具體的實現方式,所以從Mono項目中找到同名的方法探索該類型的具體實現。

class CacheExpires : CacheEntryCollection
{

    public static TimeSpan MIN_UPDATE_DELTA = new TimeSpan (0, 0, 1);
    public static TimeSpan EXPIRATIONS_INTERVAL = new TimeSpan (0, 0, 20);
    public static CacheExpiresHelper helper = new CacheExpiresHelper ();

    Timer timer;

    public CacheExpires (MemoryCacheStore store)
        : base (store, helper)
    {
    }

    public new void Add (MemoryCacheEntry entry)
    {
        entry.ExpiresEntryRef = new ExpiresEntryRef ();
        base.Add (entry);
    }

    public new void Remove (MemoryCacheEntry entry)
    {
        base.Remove (entry);
        entry.ExpiresEntryRef = ExpiresEntryRef.INVALID;
    }

    public void UtcUpdate (MemoryCacheEntry entry, DateTime utcAbsExp)
    {
        base.Remove (entry);
        entry.UtcAbsExp = utcAbsExp;
        base.Add (entry);
    }

    public void EnableExpirationTimer (bool enable)
    {
        if (enable) {
            if (timer != null)
                return;

            var period = (int) EXPIRATIONS_INTERVAL.TotalMilliseconds;
            timer = new Timer ((o) => FlushExpiredItems (true), null, period, period);
        } else {
            timer.Dispose ();
            timer = null;
        }
    }

    public int FlushExpiredItems (bool blockInsert)
    {
        return base.FlushItems (DateTime.UtcNow, CacheEntryRemovedReason.Expired, blockInsert);
    }
}

經過Mono中的源代碼能夠看出,在CacheExpires內部使用了一個定時器,經過定時器觸發定時的檢查。在觸發時使用的是CacheEntryCollection類的FlushItems方法。該方法的實現以下;

protected int FlushItems (DateTime limit, CacheEntryRemovedReason reason, bool blockInsert, int count = int.MaxValue)
{
    var flushedItems = 0;
    if (blockInsert)
        store.BlockInsert ();

    lock (entries) {
        foreach (var entry in entries) {
            if (helper.GetDateTime (entry) > limit || flushedItems >= count)
                break;

            flushedItems++;
        }

        for (var f = 0; f < flushedItems; f++)
            store.Remove (entries.Min, null, reason);
    }

    if (blockInsert)
        store.UnblockInsert ();

    return flushedItems;
}

FlushItems(***)的邏輯中,經過遍歷全部的緩存項而且比對了超時時間,將發現的超時緩存項執行Remove操做進行清理,實現緩存項的按期刪除操做。經過Mono項目中該類的功能推斷,在.net framework中的實現應該也是有相似的功能,即每個MemoryCache的實例都會有一個負責定時檢查的任務,負責處理掉全部超時的緩存項。

惰性刪除

除了定時刪除之外,MemoryCache還實現了惰性刪除的功能,這項功能的實現相對於定時刪除簡單的多,並且很是的實用。

惰性刪除是什麼意思呢?簡單的講就是在使用緩存項的時候判斷緩存項是否應該被刪除,而不用等到被專用的清理任務清理。

前文描述過MemoryCache中數據的組織方式,既然是在使用時觸發的邏輯,所以惰性刪除必然與MemoryCacheStore獲取緩存的方法有關。來看下它的Get方法的內部邏輯:

internal MemoryCacheEntry Get(MemoryCacheKey key) {
    MemoryCacheEntry entry = _entries[key] as MemoryCacheEntry;
    // 判斷是否超時
    if (entry != null && entry.UtcAbsExp <= DateTime.UtcNow) {
        Remove(key, entry, CacheEntryRemovedReason.Expired);
        entry = null;
    }
    // 更新滑動超時的時間和相關的計數器
    UpdateExpAndUsage(entry);
    return entry;
}

從代碼中能夠看出,MemoryCacheStore查找到相關的key對應的緩存項之後,並無直接返回,而是先檢查了緩存項目的超時時間。若是緩存項超時,則刪除該項並返回null。這就是MemoryCache中惰性刪除的實現方式。

MemoryCache的緩存過時策略

向MemoryCache實例中添加緩存項的時候,能夠選擇三種過時策略

  1. 永不超時
  2. 絕對超時
  3. 滑動超時

緩存策略在緩存項添加/更新緩存時(不管是使用Add或者Set方法)指定,經過在操做緩存時指定CacheItemPolicy對象來達到設置緩存超時策略的目的。

緩存超時策略並不能隨意的指定,在MemoryCache內部對於CacheItemPolicy對象有內置的檢查機制。先看下源碼:

private void ValidatePolicy(CacheItemPolicy policy) {
    //檢查過時時間策略的組合設置
    if (policy.AbsoluteExpiration != ObjectCache.InfiniteAbsoluteExpiration
        && policy.SlidingExpiration != ObjectCache.NoSlidingExpiration) {
        throw new ArgumentException(R.Invalid_expiration_combination, "policy");
    }
    //檢查滑動超時策略
    if (policy.SlidingExpiration < ObjectCache.NoSlidingExpiration || OneYear < policy.SlidingExpiration) {
        throw new ArgumentOutOfRangeException("policy", RH.Format(R.Argument_out_of_range, "SlidingExpiration", ObjectCache.NoSlidingExpiration, OneYear));
    }
    //檢查CallBack設置
    if (policy.RemovedCallback != null
        && policy.UpdateCallback != null) {
        throw new ArgumentException(R.Invalid_callback_combination, "policy");
    }
    //檢查優先級的設置
    if (policy.Priority != CacheItemPriority.Default && policy.Priority != CacheItemPriority.NotRemovable) {
        throw new ArgumentOutOfRangeException("policy", RH.Format(R.Argument_out_of_range, "Priority", CacheItemPriority.Default, CacheItemPriority.NotRemovable));
    }
}

總結下源碼中的邏輯,超時策略的設置有以下幾個規則:

  1. 絕對超時和滑動超時不能同時存在(這是前文中說二者二選一的緣由)
  2. 若是滑動超時時間小於0或者大於1年也不行
  3. RemovedCallbackUpdateCallback不能同時設置
  4. 緩存的Priority屬性不能是超出枚舉範圍(Default和NotRemovable)

MemoryCache線程安全機制

根據MSDN的描述:MemoryCache是線程安全的。那麼說明,在操做MemoryCache中的緩存項時,MemoryCache保證程序的行爲都是原子性的,而不會出現多個線程共同操做致使的數據污染等問題。

那麼,MemoryCache是如何作到這一點的?

MemoryCache在內部使用加鎖機制來保證數據項操做的原子性。該鎖以每一個MemoryCacheStore爲單位,即同一個MemoryCacheStore內部的數據共享同一個鎖,而不一樣MemoryCacheStore之間互不影響。

存在加鎖邏輯的有以下場景:

  1. 遍歷MemoryCache緩存項
  2. 向MemoryCache添加/更新緩存項
  3. 執行MemoryCache析構
  4. 移除MemoryCache中的緩存項

其餘的場景都比較好理解,其中值得一提的就是場景1(遍歷)的實現方式。在MemoryCache中,使用了鎖加複製的方式來處理遍歷的須要,保證在遍歷過程當中不會發生異常。

在.net 4.5.1中的遍歷的實現方式是這樣的:

protected override IEnumerator<KeyValuePair<string, object>> GetEnumerator() {
    Dictionary<string, object> h = new Dictionary<string, object>();
    if (!IsDisposed) {
        foreach (MemoryCacheStore store in _stores) {
            store.CopyTo(h);
        }
    }
    return h.GetEnumerator();
}

其中store.CopyTo(h);的實現方式是在MemoryCacheStore中定義的,也就是說,每一個Store的加鎖解鎖都是獨立的過程,縮小鎖機制影響的範圍也是提高性能的重要手段。CopyTo方法的主要邏輯是在鎖機制控制下的簡單的遍歷:

internal void CopyTo(IDictionary h) {
    lock (_entriesLock) {
        if (_disposed == 0) {
            foreach (DictionaryEntry e in _entries) {
                MemoryCacheKey key = e.Key as MemoryCacheKey;
                MemoryCacheEntry entry = e.Value as MemoryCacheEntry;
                if (entry.UtcAbsExp > DateTime.UtcNow) {
                    h[key.Key] = entry.Value;
                }
            }
        }
    }
}

有些出乎意料,在遍歷MemoryCache的時候,爲了實現遍歷過程當中的線程安全,實現的方式竟然是將數據另外拷貝了一份。固然了,說是徹底拷貝一份也不盡然,若是緩存項原本就是引用類型,被拷貝的也只是個指針而已。不過看起來最好仍是少用爲妙,萬一緩存的都是些基礎類型,一旦數據量較大,在遍歷過程當中的內存壓力就不是能夠忽略的問題了。

總結

在本文中以MemoryCache對於數據的組織管理和使用爲軸線,深刻的分析了MemoryCache對於一些平常應用有直接關聯的功能的實現方式。MemoryCache經過多個MemoryCacheStore對象將數據分散到不一樣的HastTable中,而且使用加鎖的方式在每一個Store內部保證操做是線程安全的,同時這種邏輯也在必定程度上改善了全局鎖的性能問題。爲了實現對於緩存項超時的管理,MemoryCache採起了兩種不一樣的管理措施,左右開弓,有效保證了緩存項的超時管理的有效性,並在超時後及時移除相關的緩存以釋放內存資源。經過對於這些功能的分析,瞭解了MemoryCache內部的數據結構和數據查詢方式,爲從此的工做掌握了許多有指導性意義的經驗。

本文還會有後續的篇章,敬請期待~~

參考資料

相關文章
相關標籤/搜索