正确使用 .NET MemoryCache 的锁定模式

Posted

技术标签:

【中文标题】正确使用 .NET MemoryCache 的锁定模式【英文标题】:Locking pattern for proper use of .NET MemoryCache 【发布时间】:2014-02-11 16:46:15 【问题描述】:

我认为这段代码存在并发问题:

const string CacheKey = "CacheKey";
static string GetCachedData()

    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    
        expensiveString = MemoryCache.Default[CacheKey] as string;
    
    else
    
        CacheItemPolicy cip = new CacheItemPolicy()
        
            AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
        ;
        expensiveString = SomeHeavyAndExpensiveCalculation();
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
    
    return expensiveString;

并发问题的原因是多个线程可以获取一个空键然后尝试将数据插入缓存。

使此代码并发证明的最短和最简洁的方法是什么?我喜欢在我的缓存相关代码中遵循一个好的模式。在线文章的链接会很有帮助。

更新:

我根据@Scott Chamberlain 的回答想出了这段代码。任何人都可以找到任何性能或并发问题吗? 如果这可行,它将节省许多代码行和错误。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Runtime.Caching;

namespace CachePoc

    class Program
    
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        
            string xyzData = MemoryCacheHelper.GetCachedData<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedData<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
        

        private static string SomeHeavyAndExpensiveXYZCalculation() return "Expensive";
        private static string SomeHeavyAndExpensiveABCCalculation() return "Expensive";

        public static class MemoryCacheHelper
        
            public static T GetCachedData<T>(string cacheKey, object cacheLock, int cacheTimePolicyMinutes, Func<T> GetData)
                where T : class
            
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                
                    return cachedData;
                

                lock (cacheLock)
                
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                    
                        return cachedData;
                    

                    //The value still did not exist so we now write it in to the cache.
                    CacheItemPolicy cip = new CacheItemPolicy()
                    
                        AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(cacheTimePolicyMinutes))
                    ;
                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, cip);
                    return cachedData;
                
            
        
    

【问题讨论】:

你为什么不用ReaderWriterLockSlim 我同意 DarthVader... 我认为您倾向于 ReaderWriterLockSlim... 但我也会使用 this 技术来避免 try-finally 声明。 对于您的更新版本,我不会再锁定单个 cacheLock,而是锁定每个键。这可以通过Dictionary&lt;string, object&gt; 轻松完成,其中密钥与您在MemoryCache 中使用的密钥相同,字典中的对象只是您锁定的基本Object。但是,话虽如此,我建议您通读 Jon Hanna 的回答。如果没有适当的分析,您可能会通过锁定来减慢程序的速度,而不是让两个 SomeHeavyAndExpensiveCalculation() 实例运行并丢弃一个结果。 在我看来,在获得昂贵的缓存值之后创建 CacheItemPolicy 会更准确。在最坏的情况下,例如创建一个需要 21 分钟才能返回“昂贵字符串”(可能包含 PDF 报告的文件名)的摘要报告,在返回之前就已经“过期”了。 @Wonderbird 好点,我更新了我的答案。 【参考方案1】:

这是我的代码的第二次迭代。因为MemoryCache 是线程安全的,所以您不需要在初始读取时锁定,您可以直接读取,如果缓存返回 null,则执行锁定检查以查看是否需要创建字符串。它大大简化了代码。

const string CacheKey = "CacheKey";
static readonly object cacheLock = new object();
private static string GetCachedData()


    //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
    var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

    if (cachedString != null)
    
        return cachedString;
    

    lock (cacheLock)
    
        //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
        cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        
            return cachedString;
        

        //The value still did not exist so we now write it in to the cache.
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        CacheItemPolicy cip = new CacheItemPolicy()
                              
                                  AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
                              ;
        MemoryCache.Default.Set(CacheKey, expensiveString, cip);
        return expensiveString;
    


编辑:下面的代码是不必要的,但我想保留它以显示原始方法。对于使用具有线程安全读取但非线程安全写入的不同集合的未来访问者可能有用(System.Collections 命名空间下的几乎所有类都是这样)。

以下是我将如何使用ReaderWriterLockSlim 来保护访问。您需要执行一种“Double Checked Locking”来查看是否有其他人在我们等待获取锁时创建了缓存项。

const string CacheKey = "CacheKey";
static readonly ReaderWriterLockSlim cacheLock = new ReaderWriterLockSlim();
static string GetCachedData()

    //First we do a read lock to see if it already exists, this allows multiple readers at the same time.
    cacheLock.EnterReadLock();
    try
    
        //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        
            return cachedString;
        
    
    finally
    
        cacheLock.ExitReadLock();
    

    //Only one UpgradeableReadLock can exist at one time, but it can co-exist with many ReadLocks
    cacheLock.EnterUpgradeableReadLock();
    try
    
        //We need to check again to see if the string was created while we where waiting to enter the EnterUpgradeableReadLock
        var cachedString = MemoryCache.Default.Get(CacheKey, null) as string;

        if (cachedString != null)
        
            return cachedString;
        

        //The entry still does not exist so we need to create it and enter the write lock
        var expensiveString = SomeHeavyAndExpensiveCalculation();
        cacheLock.EnterWriteLock(); //This will block till all the Readers flush.
        try
        
            CacheItemPolicy cip = new CacheItemPolicy()
            
                AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
            ;
            MemoryCache.Default.Set(CacheKey, expensiveString, cip);
            return expensiveString;
        
        finally 
        
            cacheLock.ExitWriteLock();
        
    
    finally
    
        cacheLock.ExitUpgradeableReadLock();
    

【讨论】:

@DarthVader 上面的代码会以什么方式不起作用?这也不是严格意义上的“双重检查锁定”,我只是遵循类似的模式,这是我能想到的描述它的最佳方式。这就是为什么我说它是一种双重检查锁定。 我没有评论你的代码。我评论说双重检查锁定不起作用。你的代码很好。 我发现很难看出这种锁定和这种存储在什么情况下是有意义的:如果你锁定所有进入 MemoryCache 的值的创建,机会是这两件事中至少有一件是错的。 这段代码的一个缺点是,如果两个缓存键“A”都没有被缓存,缓存键“A”将阻止对缓存键“B”的请求。要解决这个问题,您可以使用 concurrentDictionary 来存储要锁定的缓存键 正确。两种安全状态是无限的读者和没有作家或单一作家和没有读者。 ReaderWriterLock 遵循该模式。【参考方案2】:

有一个开源库 [免责声明:我写的]:LazyCache IMO 用两行代码满足了您的要求:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());

默认情况下它内置了锁定功能,因此可缓存方法只会在每次缓存未命中时执行一次,并且它使用 lambda,因此您可以一次性执行“获取或添加”。默认为 20 分钟滑动到期。

甚至还有a NuGet package ;)

【讨论】:

缓存的精巧。 这让我成为了一个懒惰的开发者,这是最好的答案! 值得一提的是 LazyCache 的 github 页面所指向的文章,其背后的原因非常值得一读。 alastaircrabtree.com/… 它是按键还是按缓存锁定? @DirkBoer 不,由于在lazycache中使用锁和惰性的方式,它不会被阻塞【参考方案3】:

我通过在 MemoryCache 上使用 AddOrGetExisting 方法和使用 Lazy initialization 解决了这个问题。

基本上,我的代码如下所示:

static string GetCachedData(string key, DateTimeOffset offset)

    Lazy<String> lazyObject = new Lazy<String>(() => SomeHeavyAndExpensiveCalculationThatReturnsAString());
    var returnedLazyObject = MemoryCache.Default.AddOrGetExisting(key, lazyObject, offset); 
    if (returnedLazyObject == null)
       return lazyObject.Value;
    return ((Lazy<String>) returnedLazyObject).Value;

这里最糟糕的情况是您两次创建相同的Lazy 对象。但这很微不足道。使用AddOrGetExisting 保证您只会获得Lazy 对象的一个​​实例,因此您也保证只调用一次昂贵的初始化方法。

【讨论】:

这种方法的问题是可以插入无效数据。如果SomeHeavyAndExpensiveCalculationThatResultsAString() 抛出异常,它就会卡在缓存中。即使是暂时的异常也会被缓存到 Lazy&lt;T&gt;: msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx 虽然如果初始化异常失败,Lazy 确实可以返回错误,但这是一件很容易检测的事情。然后,您可以从缓存中逐出任何解析为错误的 Lazy,创建一个新的 Lazy,将其放入缓存中并解决它。在我们自己的代码中,我们做了类似的事情。在抛出错误之前,我们会重试一定次数。 AddOrGetExisting 如果该项目不存在则返回 null,因此在这种情况下您应该检查并返回lazyObject 使用 LazyThreadSafetyMode.PublicationOnly 将避免缓存异常。 根据this blog post中的cmets,如果初始化缓存条目的成本非常高,最好只驱逐异常(如博客文章中的示例所示)而不是使用PublicationOnly,因为有可能所有线程都可以同时调用初始化器。【参考方案4】:

我认为这段代码存在并发问题:

实际上,它很可能还不错,尽管可能会有所改进。

现在,一般来说,我们有多个线程在第一次使用时设置共享值,而不锁定正在获取和设置的值的模式可以是:

    灾难性的 - 其他代码将假定仅存在一个实例。 灾难性的——获取实例的代码不是只能容忍一个(或者可能是少数几个)并发操作。 灾难性的 - 存储方式不是线程安全的(例如,将两个线程添加到字典中,您可能会遇到各种令人讨厌的错误)。 次优 - 与锁定确保只有一个线程完成获取值的工作相比,整体性能更差。 最佳 - 让多个线程执行冗余工作的成本低于防止冗余工作的成本,尤其是因为这种情况只能在相对较短的时间内发生。

但是,考虑到这里MemoryCache 可能会驱逐条目:

    如果拥有多个实例是灾难性的,那么MemoryCache 是错误的方法。 如果您必须防止同时创建,您应该在创建时这样做。 MemoryCache 在对该对象的访问方面是线程安全的,所以这里不用担心。

当然,这两种可能性都必须考虑,但唯一存在相同字符串的两个实例可能会成为问题的情况是,如果您正在执行不适用于此处的非常特殊的优化*。

所以,我们剩下的可能性是:

    避免重复调用SomeHeavyAndExpensiveCalculation() 的成本更低。 不避免重复调用SomeHeavyAndExpensiveCalculation() 的成本会更便宜。

解决这个问题可能很困难(确实,值得分析而不是假设你可以解决的事情)。值得考虑的是,最明显的插入锁定方法会阻止 所有 向缓存添加,包括那些不相关的。

这意味着如果我们有 50 个线程尝试设置 50 个不同的值,那么我们将不得不让所有 50 个线程相互等待,即使它们甚至不会进行相同的计算。

因此,与使用避免竞态条件的代码相比,使用现有代码可能会更好,如果竞态条件有问题,您很可能需要在其他地方处理,或者需要一种不同于驱逐旧条目的缓存策略†。

我要更改的一件事是将对Set() 的调用替换为对AddOrGetExisting() 的调用。从上面可以清楚地看出,这可能不是必需的,但它会允许收集新获得的项目,减少整体内存使用,并允许低代与高代收集的比例更高。

所以是的,您可以使用双锁定来防止并发,但是并发实际上不是问题,或者您以错误的方式存储值,或者在存储上双锁定不是最好的方法解决它。

*如果您知道一组字符串中只有一个存在,您可以优化相等比较,这大约是唯一一次拥有两个字符串副本可能是不正确的,而不仅仅是次优的,但您想要做非常不同类型的缓存才有意义。例如。 XmlReader 在内部进行排序。

†很可能是一个无限期存储的,或者一个利用弱引用的,所以它只会在没有现有用途的情况下驱逐条目。

【讨论】:

【参考方案5】:

很难选择哪个更好;锁或 ReaderWriterLockSlim。您需要真实世界的读写数字和比率等统计数据。

但是,如果您认为使用“锁定”是正确的方法。那么这里是针对不同需求的不同解决方案。我还在代码中包含了 Allan Xu 的解决方案。因为两者都可以满足不同的需求。

以下是促使我采用此解决方案的要求:

    出于某种原因,您不想或不能提供“GetData”函数。也许“GetData”函数位于其他具有大量构造函数的类中,并且您甚至不想创建实例,直到确保它无法逃避。 您需要从应用程序的不同位置/层访问相同的缓存数据。并且那些不同的位置无法访问相同的储物柜对象。 您没有固定的缓存键。例如;需要使用 sessionId 缓存键缓存一些数据。

代码:

using System;
using System.Runtime.Caching;
using System.Collections.Concurrent;
using System.Collections.Generic;

namespace CachePoc

    class Program
    
        static object everoneUseThisLockObject4CacheXYZ = new object();
        const string CacheXYZ = "CacheXYZ";
        static object everoneUseThisLockObject4CacheABC = new object();
        const string CacheABC = "CacheABC";

        static void Main(string[] args)
        
            //Allan Xu's usage
            string xyzData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheXYZ, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);
            string abcData = MemoryCacheHelper.GetCachedDataOrAdd<string>(CacheABC, everoneUseThisLockObject4CacheXYZ, 20, SomeHeavyAndExpensiveXYZCalculation);

            //My usage
            string sessionId = System.Web.HttpContext.Current.Session["CurrentUser.SessionId"].ToString();
            string yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
            if (string.IsNullOrWhiteSpace(yvz))
            
                object locker = MemoryCacheHelper.GetLocker(sessionId);
                lock (locker)
                
                    yvz = MemoryCacheHelper.GetCachedData<string>(sessionId);
                    if (string.IsNullOrWhiteSpace(yvz))
                    
                        DatabaseRepositoryWithHeavyConstructorOverHead dbRepo = new DatabaseRepositoryWithHeavyConstructorOverHead();
                        yvz = dbRepo.GetDataExpensiveDataForSession(sessionId);
                        MemoryCacheHelper.AddDataToCache(sessionId, yvz, 5);
                    
                
            
        


        private static string SomeHeavyAndExpensiveXYZCalculation()  return "Expensive"; 
        private static string SomeHeavyAndExpensiveABCCalculation()  return "Expensive"; 

        public static class MemoryCacheHelper
        
            //Allan Xu's solution
            public static T GetCachedDataOrAdd<T>(string cacheKey, object cacheLock, int minutesToExpire, Func<T> GetData) where T : class
            
                //Returns null if the string does not exist, prevents a race condition where the cache invalidates between the contains check and the retreival.
                T cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                if (cachedData != null)
                    return cachedData;

                lock (cacheLock)
                
                    //Check to see if anyone wrote to the cache while we where waiting our turn to write the new value.
                    cachedData = MemoryCache.Default.Get(cacheKey, null) as T;

                    if (cachedData != null)
                        return cachedData;

                    cachedData = GetData();
                    MemoryCache.Default.Set(cacheKey, cachedData, DateTime.Now.AddMinutes(minutesToExpire));
                    return cachedData;
                
            

            #region "My Solution"

            readonly static ConcurrentDictionary<string, object> Lockers = new ConcurrentDictionary<string, object>();
            public static object GetLocker(string cacheKey)
            
                CleanupLockers();

                return Lockers.GetOrAdd(cacheKey, item => (cacheKey, new object()));
            

            public static T GetCachedData<T>(string cacheKey) where T : class
            
                CleanupLockers();

                T cachedData = MemoryCache.Default.Get(cacheKey) as T;
                return cachedData;
            

            public static void AddDataToCache(string cacheKey, object value, int cacheTimePolicyMinutes)
            
                CleanupLockers();

                MemoryCache.Default.Add(cacheKey, value, DateTimeOffset.Now.AddMinutes(cacheTimePolicyMinutes));
            

            static DateTimeOffset lastCleanUpTime = DateTimeOffset.MinValue;
            static void CleanupLockers()
            
                if (DateTimeOffset.Now.Subtract(lastCleanUpTime).TotalMinutes > 1)
                
                    lock (Lockers)//maybe a better locker is needed?
                    
                        try//bypass exceptions
                        
                            List<string> lockersToRemove = new List<string>();
                            foreach (var locker in Lockers)
                            
                                if (!MemoryCache.Default.Contains(locker.Key))
                                    lockersToRemove.Add(locker.Key);
                            

                            object dummy;
                            foreach (string lockerKey in lockersToRemove)
                                Lockers.TryRemove(lockerKey, out dummy);

                            lastCleanUpTime = DateTimeOffset.Now;
                        
                        catch (Exception)
                         
                    
                

            
            #endregion
        
    

    class DatabaseRepositoryWithHeavyConstructorOverHead
    
        internal string GetDataExpensiveDataForSession(string sessionId)
        
            return "Expensive data from database";
        
    


【讨论】:

【参考方案6】:

为了避免全局锁,您可以使用 SingletonCache 实现每个键一个锁,而不会爆炸内存使用(当不再引用时,锁对象会被删除,并且获取/释放是线程安全的,保证只有 1 个实例在通过比较和交换使用)。

使用它看起来像这样:

SingletonCache<string, object> keyLocks = new SingletonCache<string, object>();

const string CacheKey = "CacheKey";
static string GetCachedData()

    string expensiveString =null;
    if (MemoryCache.Default.Contains(CacheKey))
    
        return MemoryCache.Default[CacheKey] as string;
    

    // double checked lock
    using (var lifetime = keyLocks.Acquire(url))
    
        lock (lifetime.Value)
        
           if (MemoryCache.Default.Contains(CacheKey))
           
              return MemoryCache.Default[CacheKey] as string;
           

           cacheItemPolicy cip = new CacheItemPolicy()
           
              AbsoluteExpiration = new DateTimeOffset(DateTime.Now.AddMinutes(20))
           ;
           expensiveString = SomeHeavyAndExpensiveCalculation();
           MemoryCache.Default.Set(CacheKey, expensiveString, cip);
           return expensiveString;
        
          

代码在 GitHub 上:https://github.com/bitfaster/BitFaster.Caching

Install-Package BitFaster.Caching

还有一个 LRU 实现,它比 MemoryCache 更轻,并且有几个优点 - 更快的并发读取和写入、有限的大小、没有后台线程、内部性能计数器等(免责声明,我写的)。

【讨论】:

【参考方案7】:

控制台示例 of MemoryCache,“如何保存/获取简单的类对象”

启动并按下 任何键 除了 Esc 后的输出:

保存到缓存! 从缓存中获取! 一些1 一些2

    class Some
    
        public String text  get; set; 

        public Some(String text)
        
            this.text = text;
        

        public override string ToString()
        
            return text;
        
    

    public static MemoryCache cache = new MemoryCache("cache");

    public static string cache_name = "mycache";

    static void Main(string[] args)
    

        Some some1 = new Some("some1");
        Some some2 = new Some("some2");

        List<Some> list = new List<Some>();
        list.Add(some1);
        list.Add(some2);

        do 

            if (cache.Contains(cache_name))
            
                Console.WriteLine("Getting from cache!");
                List<Some> list_c = cache.Get(cache_name) as List<Some>;
                foreach (Some s in list_c) Console.WriteLine(s);
            
            else
            
                Console.WriteLine("Saving to cache!");
                cache.Set(cache_name, list, DateTime.Now.AddMinutes(10));                   
            

         while (Console.ReadKey(true).Key != ConsoleKey.Escape);

    

【讨论】:

【参考方案8】:
public interface ILazyCacheProvider : IAppCache

    /// <summary>
    /// Get data loaded - after allways throw cached result (even when data is older then needed) but very fast!
    /// </summary>
    /// <param name="key"></param>
    /// <param name="getData"></param>
    /// <param name="slidingExpiration"></param>
    /// <typeparam name="T"></typeparam>
    /// <returns></returns>
    T GetOrAddPermanent<T>(string key, Func<T> getData, TimeSpan slidingExpiration);


/// <summary>
/// Initialize LazyCache in runtime
/// </summary>
public class LazzyCacheProvider: CachingService, ILazyCacheProvider

    private readonly Logger _logger = LogManager.GetLogger("MemCashe");
    private readonly Hashtable _hash = new Hashtable();
    private readonly List<string>  _reloader = new List<string>();
    private readonly ConcurrentDictionary<string, DateTime> _lastLoad = new ConcurrentDictionary<string, DateTime>();  


    T ILazyCacheProvider.GetOrAddPermanent<T>(string dataKey, Func<T> getData, TimeSpan slidingExpiration)
    
        var currentPrincipal = Thread.CurrentPrincipal;
        if (!ObjectCache.Contains(dataKey) && !_hash.Contains(dataKey))
        
            _hash[dataKey] = null;
            _logger.Debug($"dataKey - first start");
            _lastLoad[dataKey] = DateTime.Now;
            _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
            _lastLoad[dataKey] = DateTime.Now;
           _logger.Debug($"dataKey - first");
        
        else
        
            if ((!ObjectCache.Contains(dataKey) || _lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) < DateTime.Now) && _hash[dataKey] != null)
                Task.Run(() =>
                
                    if (_reloader.Contains(dataKey)) return;
                    lock (_reloader)
                    
                        if (ObjectCache.Contains(dataKey))
                        
                            if(_lastLoad[dataKey].AddMinutes(slidingExpiration.Minutes) > DateTime.Now)
                                return;
                            _lastLoad[dataKey] = DateTime.Now;
                            Remove(dataKey);
                        
                        _reloader.Add(dataKey);
                        Thread.CurrentPrincipal = currentPrincipal;
                        _logger.Debug($"dataKey - reload start");
                        _hash[dataKey] = ((object)GetOrAdd(dataKey, getData, slidingExpiration)).CloneObject();
                        _logger.Debug($"dataKey - reload");
                        _reloader.Remove(dataKey);
                    
                );
        
        if (_hash[dataKey] != null) return (T) (_hash[dataKey]);

        _logger.Debug($"dataKey - dummy start");
        var data = GetOrAdd(dataKey, getData, slidingExpiration);
        _logger.Debug($"dataKey - dummy");
        return (T)((object)data).CloneObject();
    

【讨论】:

非常快的 LazyCache :) 我为 REST API 存储库编写了这段代码。【参考方案9】:

虽然有点晚了... 全面实施:

    [HttpGet]
    public async Task<HttpResponseMessage> GetPageFromUriOrBody(RequestQuery requestQuery)
    
        log(nameof(GetPageFromUriOrBody), nameof(requestQuery));
        var responseResult = await _requestQueryCache.GetOrCreate(
            nameof(GetPageFromUriOrBody)
            , requestQuery
            , (x) => getPageContent(x).Result);
        return Request.CreateResponse(System.Net.HttpStatusCode.Accepted, responseResult);
    
    static MemoryCacheWithPolicy<RequestQuery, string> _requestQueryCache = new MemoryCacheWithPolicy<RequestQuery, string>();

这里是getPageContent签名:

async Task<string> getPageContent(RequestQuery requestQuery);

这是MemoryCacheWithPolicy 的实现:

public class MemoryCacheWithPolicy<TParameter, TResult>

    static ILogger _nlogger = new AppLogger().Logger;
    private MemoryCache _cache = new MemoryCache(new MemoryCacheOptions() 
    
        //Size limit amount: this is actually a memory size limit value!
        SizeLimit = 1024 
    );

    /// <summary>
    /// Gets or creates a new memory cache record for a main data
    /// along with parameter data that is assocciated with main main.
    /// </summary>
    /// <param name="key">Main data cache memory key.</param>
    /// <param name="param">Parameter model that assocciated to main model (request result).</param>
    /// <param name="createCacheData">A delegate to create a new main data to cache.</param>
    /// <returns></returns>
    public async Task<TResult> GetOrCreate(object key, TParameter param, Func<TParameter, TResult> createCacheData)
    
        // this key is used for param cache memory.
        var paramKey = key + nameof(param);

        if (!_cache.TryGetValue(key, out TResult cacheEntry))
        
            // key is not in the cache, create data through the delegate.
            cacheEntry = createCacheData(param);
            createMemoryCache(key, cacheEntry, paramKey, param);

            _nlogger.Warn(" cache is created.");
        
        else
        
            // data is chached so far..., check if param model is same (or changed)?
            if(!_cache.TryGetValue(paramKey, out TParameter cacheParam))
            
                //exception: this case should not happened!
            

            if (!cacheParam.Equals(param))
            
                // request param is changed, create data through the delegate.
                cacheEntry = createCacheData(param);
                createMemoryCache(key, cacheEntry, paramKey, param);
                _nlogger.Warn(" cache is re-created (param model has been changed).");
            
            else
            
                _nlogger.Trace(" cache is used.");
            

        
        return await Task.FromResult<TResult>(cacheEntry);
    
    MemoryCacheEntryOptions createMemoryCacheEntryOptions(TimeSpan slidingOffset, TimeSpan relativeOffset)
    
        // Cache data within [slidingOffset] seconds, 
        // request new result after [relativeOffset] seconds.
        return new MemoryCacheEntryOptions()

            // Size amount: this is actually an entry count per 
            // key limit value! not an actual memory size value!
            .SetSize(1)

            // Priority on removing when reaching size limit (memory pressure)
            .SetPriority(CacheItemPriority.High)

            // Keep in cache for this amount of time, reset it if accessed.
            .SetSlidingExpiration(slidingOffset)

            // Remove from cache after this time, regardless of sliding expiration
            .SetAbsoluteExpiration(relativeOffset);
        //
    
    void createMemoryCache(object key, TResult cacheEntry, object paramKey, TParameter param)
    
        // Cache data within 2 seconds, 
        // request new result after 5 seconds.
        var cacheEntryOptions = createMemoryCacheEntryOptions(
            TimeSpan.FromSeconds(2)
            , TimeSpan.FromSeconds(5));

        // Save data in cache.
        _cache.Set(key, cacheEntry, cacheEntryOptions);

        // Save param in cache.
        _cache.Set(paramKey, param, cacheEntryOptions);
    
    void checkCacheEntry<T>(object key, string name)
    
        _cache.TryGetValue(key, out T value);
        _nlogger.Fatal("Key: 0, Name: 1, Value: 2", key, name, value);
    

nlogger 只是 nLog 跟踪 MemoryCacheWithPolicy 行为的对象。 如果请求对象 (RequestQuery requestQuery) 通过委托 (Func&lt;TParameter, TResult&gt; createCacheData) 更改或在滑动或绝对时间达到其限制时重新创建,我将重新创建内存缓存。请注意,一切都是异步的;)

【讨论】:

也许你的回答与这个问题更相关:Async threadsafe Get from MemoryCache 我猜是这样,但还是有用的经验交流;)【参考方案10】:

有些过时的问题,但也许仍然有用:您可以看看我最近发布的FusionCache ⚡?。

您正在寻找的功能描述为here,您可以这样使用它:

const string CacheKey = "CacheKey";
static string GetCachedData()

    return fusionCache.GetOrSet(
        CacheKey,
        _ => SomeHeavyAndExpensiveCalculation(),
        TimeSpan.FromMinutes(20)
    );

您可能还会发现其他一些有趣的功能,例如 fail-safe、advanced timeouts,它们具有后台工厂完成功能并支持可选的分布式 2nd level 缓存。

如果你愿意给它一个机会,请告诉我你的想法。

/无耻之塞

【讨论】:

以上是关于正确使用 .NET MemoryCache 的锁定模式的主要内容,如果未能解决你的问题,请参考以下文章

MemoryCache.Set() 是线程安全的吗?

MemoryCache.Default 在 .NET Core 中不可用?

在 .NET 中,为啥 ObjectCache 是 MemoryCache 的首选类型?

.Net Core缓存组件(MemoryCache)缓存篇

.net MemoryCache - 通知删除的项目

ASP.NET MVC 和 MemoryCache - 我如何使用它?