深入理解.NET MemoryCache

摘要

MemoryCache是.Net Framework 4.0开始提供的内存缓存类,使用该类型可以方便的在程序内部缓存数据并对于数据的有效性进行方便的管理,借助该类型可以实现ASP.NET中常用的Cache类的相似功能,并且可以适应更加丰富的使用场景。在使用MemoryCache时常常有各种疑问,数据是怎么组织的?有没有可能用更高效的组织和使用方式?数据超时如何控制?为了知其所以然,本文中对于MemoryCache的原理和实现方式进行了深入分析,同时在分析的过程中学习到了许多业界成熟组件的设计思想,为今后的工作打开了更加开阔的思路

本文面向的是.net 4.5.1的版本,在后续的.net版本中MemoryCache有略微的不同,欢迎补充

文章内容较长,预计阅读时间1小时左右



MemoryCache类继承自ObjectCache抽象类,并且实现了IEnumerableIDisposable接口。跟ASP.NET常用的Cache类实现了相似的功能,但是MemoryCache更加通用。使用它的时候不必依赖于System.Web类库,并且在同一个进程中可以使用MemoryCache创建多个实例。

在使用MemoryCache的时候通常会有些疑问,这个类到底内部数据是如何组织的?缓存项的超时是如何处理的?它为什么宣传自己是线程安全的?为了回答这些问题,接下来借助Reference Source对于MemoryCache的内部实现一探究竟。

MemoryCache内部数据结构

在MemoryCache类内部,数据的组织方式跟MemoryCacheStore、MemoryCacheKey和MemoryCacheEntry这三个类有关,它们的作用分别是:

  • MemoryCacheStore:承载数据
  • MemoryCacheKey:构造检索项
  • MemoryCacheEntry:缓存内部数据的真实表现形式

其关系大致如下图所示:

从图上可以直观的看出,一个MemoryCache实例对象可以包含多个MemoryCacheStore对象,具体有几个需要取决于程序所在的硬件环境,跟CPU数目有关。在MemoryCache的内部,MemoryCacheStore对象就像一个个的小数据库一样,承载着各种数据。所以,要理解MemoryCache内部的数据结构,就需要先理解MemoryCacheStore的地位和作用。

MemoryCacheStore

该类型是MemoryCache内部真正用于承载数据的容器。它直接管理着程序的内存缓存项,既然要承载数据,那么该类型中必然有些属性与数据存储有关。其具体表现是:MemoryCache中有一个类型为HashTable的私有属性_entries,在该属性中存储了它所管理的所有缓存项。

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 _storeRefs[idx].Target;
}

既然可能存在多个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中的属性和方法主要为三类:

  1. 缓存的内容相关,如Key、Value
  2. 缓存内容的状态相关,如State、HasExpiration方法等
  3. 缓存内容的相关事件相关,如CallCacheEntryRemovedCallback方法、CallNotifyOnChanged方法等

理解了MemoryCache中数据的组织方式后,可以帮助理解数据是如何从MemoryCache中被一步步查询得到的。

如何从MemoryCahe中查询数据

从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)

MemoryCahce线程安全机制

根据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内部的数据结构和数据查询方式,为今后的工作掌握了许多有指导性意义的经验。

参考资料

原文地址:https://www.cnblogs.com/zhu-wj/p/9166758.html

时间: 2024-10-13 13:20:54

深入理解.NET MemoryCache的相关文章

Android中图片加载框架Glide解析2----从源码的角度理解Glide的执行流程

转载地址:http://blog.csdn.net/guolin_blog/article/details/53939176 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一张图片只需要一行代码就能实现,如下所示: Glide.with(this).load(url).into(i

Android图片加载库的理解

前言 这是“基础自测”系列的第三篇文章,以Android开发需要熟悉的20个技术点为切入点,本篇重点讲讲Android中的ImageLoader这个库的一些理解,在Android上最让人头疼是从网络中获取图片,显示,回收,任何一个环节有问题都可能直接OOM,当需要加载大量的图片的时候,每当快速滑,有时候会很卡,甚至会因为内存溢出而崩溃.这里讲解的库是:Universal_Image_Loader. 内容目录 ImageLoader设计原理 ImageLoader流程图 ImageLoader的使

Android图片加载框架最全解析(二),从源码的角度理解Glide的执行流程

转载请注明出处:http://blog.csdn.net/guolin_blog/article/details/53939176 本文同步发表于我的微信公众号,扫一扫文章底部的二维码或在微信搜索 郭霖 即可关注,每天都有文章更新. 在本系列的上一篇文章中,我们学习了Glide的基本用法,体验了这个图片加载框架的强大功能,以及它非常简便的API.还没有看过上一篇文章的朋友,建议先去阅读 Android图片加载框架最全解析(一),Glide的基本用法 . 在多数情况下,我们想要在界面上加载并展示一

Python——深入理解urllib、urllib2及requests(requests不建议使用?)

深入理解urllib.urllib2及requests            python Python 是一种面向对象.解释型计算机程序设计语言,由Guido van Rossum于1989年底发明,第一个公开发行版发行于1991年,Python 源代码同样遵循 GPL(GNU General Public License)协议[1] .Python语法简洁而清晰,具有丰富和强大的类库. urllib and urllib2 区别 urllib和urllib2模块都做与请求URL相关的操作,但

关于SVM数学细节逻辑的个人理解(三) :SMO算法理解

第三部分:SMO算法的个人理解 接下来的这部分我觉得是最难理解的?而且计算也是最难得,就是SMO算法. SMO算法就是帮助我们求解: s.t.   这个优化问题的. 虽然这个优化问题只剩下了α这一个变量,但是别忘了α是一个向量,有m个αi等着我们去优化,所以还是很麻烦,所以大神提出了SMO算法来解决这个优化问题. 关于SMO最好的资料还是论文<Sequential Minimal Optimization A Fast Algorithm for Training Support Vector

2.2 logistic回归损失函数(非常重要,深入理解)

上一节当中,为了能够训练logistic回归模型的参数w和b,需要定义一个成本函数 使用logistic回归训练的成本函数 为了让模型通过学习来调整参数,要给出一个含有m和训练样本的训练集 很自然的,希望通过训练集找到参数w和b,来得到自己得输出 对训练集当中的值进行预测,将他写成y^(I)我们希望他会接近于训练集当中的y^(i)的数值 现在来看一下损失函数或者叫做误差函数 他们可以用来衡量算法的运行情况 可以定义损失函数为y^和y的差,或者他们差的平方的一半,结果表明你可能这样做,但是实际当中

理解信息管理系统

1.信息与数据的区别是什么? 数据是记录客观事物,可鉴别的符号,而信息是具有关联性和目的性的结构化,组织化的数据.数据经过处理仍是数据,而信息经过加工可以形成知识.处理数据是为了便于更好的解释,只有经过解释,数据才有意义,才可以成为信息.可以说信息是经过加工以后,对客观世界产生影响的数据. 2.信息与知识的区别是什么? 信息是具有关联性和目的性的结构化,组织化的数据,知识是对信息的进一步加工和应用,是对事物内在规律和原理的认识.信息经过加工可以形成知识. 3.举一个同一主题不同级别的数据.信息.

深度理解div+css布局嵌套盒子

1. 网页布局概述 网页布局的概念是把即将出现在网页中的所有元素进行定位,而CSS网页排版技术有别于传统的网页排版方法,它将页面首先在整体上使用<div>标记进行分块,然后对每个快进行CSS定位以及设置显示效果,最后在每个块中添加相应的内容.利用CSS排版方法更容易地控制页面每个元素的效果,更新也更容易,甚至页面的拓扑结构也可以通过修改相应的CSS属性来重新定位.  2. 盒子模型 盒子模型是CSS控制页面元素的一个重要概念,只有掌握了盒子模型,才能让CSS很好地控制页面上每一个元素,达到我们

深入理解Java:类加载机制及反射

一.Java类加载机制 1.概述 Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数,属性和方法等,Java允许用户借由这个Class相关的元信息对象间接调用Class对象的功能. 虚拟机把描述类的数据从class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类加载机制. 2.工作机制 类装载器就是寻找类的字节码文件,并构造出类在JVM内部表示