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

All*_* Xu 110 .net c# multithreading memorycache

我假设此代码存在并发问题:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

什么是最简洁,最干净的方法来使这个代码并发证明?我喜欢在缓存相关代码中遵循一个好的模式.链接到在线文章将是一个很大的帮助.

更新:

我根据@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;
                }
            }
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

Sco*_*ain 88

这是我的第二次代码迭代.因为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;
    }
}
Run Code Online (Sandbox Code Playgroud)

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

以下是我将如何使用它ReaderWriterLockSlim来保护访问权限.您需要执行一种" 双重检查锁定 ",以查看是否有其他人在我们等待锁定时创建了缓存项目.

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

  • 此代码的缺点是如果两个缓存尚未缓存,则CacheKey"A"将阻止对CacheKey"B"的请求.要解决此问题,您可以使用concurrentDictionary <string,object>,在其中存储要锁定的缓存键 (7认同)

ala*_*ree 38

有一个开源库[免责声明:我写的]:LazyCache,IMO通过两行代码满足您的需求:

IAppCache cache = new CachingService();
var cachedResults = cache.GetOrAdd("CacheKey", 
  () => SomeHeavyAndExpensiveCalculation());
Run Code Online (Sandbox Code Playgroud)

它默认内置锁定,因此可缓存方法每次缓存未命中时只执行一次,并且它使用lambda,因此您可以一次性"获取或添加".默认为20分钟滑动到期.

甚至还有一个NuGet包 ;)

  • 缓存的Dapper. (4认同)
  • 这使我成为一个懒惰的开发人员,使这成为最好的答案! (3认同)
  • 它是按键锁定还是按缓存锁定? (2认同)

Kei*_*ith 30

我已经通过在MemoryCache上使用AddOrGetExisting方法和使用Lazy初始化来解决了这个问题.

从本质上讲,我的代码看起来像这样:

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;
}
Run Code Online (Sandbox Code Playgroud)

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

  • 如果项目不存在,AddOrGetExisting将返回null,因此在这种情况下应检查并返回lazyObject (12认同)
  • 此类方法的问题是您可以插入无效数据.如果`SomeHeavyAndExpensiveCalculationThatResultsAString()`抛出异常,它就会卡在缓存中.即使是瞬态异常也会被"Lazy <T>"缓存:http://msdn.microsoft.com/en-us/library/vstudio/dd642331.aspx (4认同)
  • 虽然如果初始化异常失败,Lazy <T>可以返回错误,但检测起来非常容易.然后,您可以从缓存中驱逐任何解析为错误的Lazy <T>,创建一个新的Lazy <T>,将其放入缓存中并解析它.在我们自己的代码中,我们做了类似的事情.在我们抛出错误之前,我们会重试一定次数. (2认同)
  • 根据[本博客文章](http://blog.falafel.com/working-system-runtime-caching-memorycache/)中的评论,如果初始化缓存条目非常昂贵,那么最好只驱逐异常(如博客文章中的示例所示)而不是使用PublicationOnly,因为所有线程都有可能同时调用初始化程序. (2认同)

Jon*_*nna 15

我假设此代码存在并发问题:

实际上,尽管可能有所改善,但它可能很好.

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

  1. 灾难性的 - 其他代码假定只存在一个实例.
  2. 灾难性的 - 获取实例的代码不能只容忍一个(或者可能是一定数量的)并发操作.
  3. 灾难性的 - 存储方式不是线程安全的(例如,有两个线程添加到字典中,你可以得到各种令人讨厌的错误).
  4. 次优 - 总体性能比锁定确保只有一个线程完成获取值的工作更糟糕.
  5. 最佳 - 多线程执行冗余工作的成本低于防止它的成本,特别是因为这只能在相对短暂的时间内发生.

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

  1. 如果拥有多个实例MemoryCache是灾难性的,那么这是错误的方法.
  2. 如果必须防止同时创建,则应在创建时执行此操作.
  3. MemoryCache 在访问该对象方面是线程安全的,因此这不是一个问题.

当然,必须考虑这两种可能性,尽管现有的两个相同字符串实例的唯一时间可能是一个问题,如果你正在做非常特殊的优化,这里不适用*.

所以,我们留下了各种可能性:

  1. 避免重复呼叫的成本更便宜SomeHeavyAndExpensiveCalculation().
  2. 避免重复呼叫的成本更便宜SomeHeavyAndExpensiveCalculation().

并且解决这个问题可能很困难(事实上,这种事情值得分析,而不是假设你可以解决它).这里值得考虑,但是最明显的锁定插入方式将阻止对缓存的所有添加,包括那些不相关的缓存.

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

因此,你可能更好地使用你拥有的代码,而不是避免竞争条件的代码,如果竞争条件是一个问题,你很可能需要在其他地方处理,或者需要一个不同的缓存策略比驱逐旧条目的策略†.

我要改变的一件事是我Set()用一个替换呼叫AddOrGetExisting().从上面可以清楚地看出,它可能没有必要,但它将允许收集新获得的项目,减少总体内存使用并允许较低的低代与高代收集率.

所以,是的,您可以使用双锁来防止并发,但要么并发实际上不是问题,要么以错误的方式存储值,或者对商店进行双重锁定将不是解决它的最佳方法.

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

†很可能是无限期存储的,或者是使用弱引用的,因此只有在没有现有用途的情况下才会驱逐条目.