如何使用MemoryCache处理昂贵的构建操作?

时间:2012-05-11 22:13:05

标签: c# .net memorycache

在ASP.NET MVC项目中,我们有几个需要大量资源和构建时间的数据实例。我们想要缓存它们。

MemoryCache提供了一定程度的线程安全性,但不足以避免并行运行多个构建代码实例。这是一个例子:

var data = cache["key"];
if(data == null)
{
  data = buildDataUsingGoodAmountOfResources();
  cache["key"] = data;
}

正如您在繁忙的网站上看到的那样,数百个线程可以同时进入if语句,直到构建数据并使构建操作更慢,不必要地消耗服务器资源。

MemoryCache中有一个原子AddOrGetExisting实现,但它错误地要求“值设置”而不是“检索要设置的值的代码”,我认为这使得给定的方法几乎完全没用。

我们一直在MemoryCache周围使用我们自己的ad-hoc脚手架来实现它,但它需要明确的lock。使用每个条目的锁定对象很麻烦,我们通常会通过共享远离理想的锁定对象来逃避。这让我觉得避免这种惯例的理由可能是故意的。

所以我有两个问题:

  • 最好不要lock构建代码吗? (我想知道这可能对一个人更具响应性)

  • 对于这样的锁,实现MemoryCache的每个条目锁定的正确方法是什么?使用key字符串作为锁定对象的强烈冲动在“.NET锁定101”中被忽略。

6 个答案:

答案 0 :(得分:69)

我们通过将Lazy<T>AddOrGetExisting相结合来解决此问题,以避免完全需要锁定对象。这是一个示例代码(使用无限到期):

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var value = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (value ?? newValue).Value; // Lazy<T> handles the locking itself
}

那不完整。有一些像“异常缓存”的问题所以你必须决定你想要做什么,以防你的valueFactory抛出异常。但是,其中一个优点是能够缓存空值。

答案 1 :(得分:10)

对于条件添加要求,我总是使用ConcurrentDictionary,它有一个重载的GetOrAdd方法,如果需要构建对象,它接受一个委托。

ConcurrentDictionary<string, object> _cache = new
  ConcurrenctDictionary<string, object>();

public void GetOrAdd(string key)
{
  return _cache.GetOrAdd(key, (k) => {
    //here 'k' is actually the same as 'key'
    return buildDataUsingGoodAmountOfResources();
  });
}

实际上我几乎总是使用static并发词典。我曾经有一个ReaderWriterLockSlim实例保护的“正常”字典,但是一旦我切换到.Net 4(它只能从那个开始提供),我开始转换我遇到的任何一个。

至少可以说,{p> ConcurrentDictionary的表现令人钦佩:)

更新使用基于年龄的过期语义的简单实现。还应该确保只创建一次单个项目 - 根据@ usr的建议。 再次更新 - 正如@usr建议的那样 - 简单地使用Lazy<T>会简单得多 - 您可以在将创建委托添加到并发字典时将其转发给它。我改变了代码,因为实际上我的锁字典无论如何都不会有效。但我真的应该想到自己(虽然在英国已经过了午夜,但我已经被击败了。任何同情?当然不是。作为开发者,我有足够的咖啡因在我的血管中咳嗽醒来死者)

我建议使用此实现IRegisteredObject接口,然后使用HostingEnvironment.RegisterObject方法注册它 - 这样可以在应用程序池关闭时提供更简洁的方法来关闭轮询器线程-down /回收。

public class ConcurrentCache : IDisposable
{
  private readonly ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>> _cache = 
    new ConcurrentDictionary<string, Tuple<DateTime?, Lazy<object>>>();

  private readonly Thread ExpireThread = new Thread(ExpireMonitor);

  public ConcurrentCache(){
    ExpireThread.Start();
  }

  public void Dispose()
  {
    //yeah, nasty, but this is a 'naive' implementation :)
    ExpireThread.Abort();
  }

  public void ExpireMonitor()
  {
    while(true)
    {
      Thread.Sleep(1000);
      DateTime expireTime = DateTime.Now;
      var toExpire = _cache.Where(kvp => kvp.First != null &&
        kvp.Item1.Value < expireTime).Select(kvp => kvp.Key).ToArray();
      Tuple<string, Lazy<object>> removed;
      object removedLock;
      foreach(var key in toExpire)
      {
        _cache.TryRemove(key, out removed);
      }
    }
  }

  public object CacheOrAdd(string key, Func<string, object> factory, 
    TimeSpan? expiry)
  {
    return _cache.GetOrAdd(key, (k) => { 
      //get or create a new object instance to use 
      //as the lock for the user code
        //here 'k' is actually the same as 'key' 
        return Tuple.Create(
          expiry.HasValue ? DateTime.Now + expiry.Value : (DateTime?)null,
          new Lazy<object>(() => factory(k)));
    }).Item2.Value; 
  }
}

答案 2 :(得分:1)

这是一个遵循您似乎想到的设计。第一次锁只会发生很短的时间。对data.Value的最终调用也会锁定(在下面),但客户端只会阻止其中两个同时请求相同的项目。

public DataType GetData()
{      
  lock(_privateLockingField)
  {
    Lazy<DataType> data = cache["key"] as Lazy<DataType>;
    if(data == null)
    {
      data = new Lazy<DataType>(() => buildDataUsingGoodAmountOfResources();
      cache["key"] = data;
    }
  }

  return data.Value;
}

答案 3 :(得分:1)

将最常见的答案带入C#7,这是我的实现,它允许从任何源类型T存储到任何返回类型TResult

/// <summary>
/// Creates a GetOrRefreshCache function with encapsulated MemoryCache.
/// </summary>
/// <typeparam name="T">The type of inbound objects to cache.</typeparam>
/// <typeparam name="TResult">How the objects will be serialized to cache and returned.</typeparam>
/// <param name="cacheName">The name of the cache.</param>
/// <param name="valueFactory">The factory for storing values.</param>
/// <param name="keyFactory">An optional factory to choose cache keys.</param>
/// <returns>A function to get or refresh from cache.</returns>
public static Func<T, TResult> GetOrRefreshCacheFactory<T, TResult>(string cacheName, Func<T, TResult> valueFactory, Func<T, string> keyFactory = null) {
    var getKey = keyFactory ?? (obj => obj.GetHashCode().ToString());
    var cache = new MemoryCache(cacheName);
    // Thread-safe lazy cache
    TResult getOrRefreshCache(T obj) {
        var key = getKey(obj);
        var newValue = new Lazy<TResult>(() => valueFactory(obj));
        var value = (Lazy<TResult>) cache.AddOrGetExisting(key, newValue, ObjectCache.InfiniteAbsoluteExpiration);
        return (value ?? newValue).Value;
    }
    return getOrRefreshCache;
}

用法

/// <summary>
/// Get a JSON object from cache or serialize it if it doesn't exist yet.
/// </summary>
private static readonly Func<object, string> GetJson =
    GetOrRefreshCacheFactory<object, string>("json-cache", JsonConvert.SerializeObject);


var json = GetJson(new { foo = "bar", yes = true });

答案 4 :(得分:1)

Sedat将Lazy与AddOrGetExisting相结合的解决方案令人鼓舞。我必须指出,该解决方案存在性能问题,这对于缓存解决方案而言似乎非常重要。

如果查看AddOrGetExisting()的code,您会发现AddOrGetExisting()不是非锁定方法。与无锁Get()方法相比,它浪费了MemoryCache的优势之一。

我建议您遵循以下解决方案,先使用Get(),然后再使用AddOrGetExisting()以避免多次创建对象。

public T GetFromCache<T>(string key, Func<T> valueFactory) 
{
    T value = (T)cache.Get(key);
    if (value != null)
    {
        return value;
    }

    var newValue = new Lazy<T>(valueFactory);
    // the line belows returns existing item or adds the new value if it doesn't exist
    var oldValue = (Lazy<T>)cache.AddOrGetExisting(key, newValue, MemoryCache.InfiniteExpiration);
    return (oldValue ?? newValue).Value; // Lazy<T> handles the locking itself
}

答案 5 :(得分:0)

这是MemoryCache扩展方法的简单解决方案。

 public static class MemoryCacheExtensions
 {
     public static T LazyAddOrGetExitingItem<T>(this MemoryCache memoryCache, string key, Func<T> getItemFunc, DateTimeOffset absoluteExpiration)
     {
         var item = new Lazy<T>(
             () => getItemFunc(),
             LazyThreadSafetyMode.PublicationOnly // Do not cache lazy exceptions
         );

         var cachedValue = memoryCache.AddOrGetExisting(key, item, absoluteExpiration) as Lazy<T>;

         return (cachedValue != null) ? cachedValue.Value : item.Value;
     }
 }

并将其作为使用说明进行测试。

[TestMethod]
[TestCategory("MemoryCacheExtensionsTests"), TestCategory("UnitTests")]
public void MemoryCacheExtensions_LazyAddOrGetExitingItem_Test()
{
    const int expectedValue = 42;
    const int cacheRecordLifetimeInSeconds = 42;

    var key = "lazyMemoryCacheKey";
    var absoluteExpiration = DateTimeOffset.Now.AddSeconds(cacheRecordLifetimeInSeconds);

    var lazyMemoryCache = MemoryCache.Default;

    #region Cache warm up

    var actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion

    #region Get value from cache

    actualValue = lazyMemoryCache.LazyAddOrGetExitingItem(key, () => expectedValue, absoluteExpiration);
    Assert.AreEqual(expectedValue, actualValue);

    #endregion
}