63

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

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

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

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

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

我们一直在围绕 MemoryCache 使用我们自己的临时脚手架来使其正确,但是它需要显式lock的 s。使用 per-entry 锁对象很麻烦,我们通常通过共享锁对象来逃避,这远非理想。这让我认为避免这种约定的原因可能是故意的。

所以我有两个问题:

  • lock不构建代码是更好的做法吗?(我想知道,这可能被证明对一个人来说更敏感)

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

4

6 回答 6

74

我们通过结合Lazy<T>with解决了这个问题,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 抛出异常。但是,优点之一是也能够缓存空值。

于 2013-04-09T06:38:09.283 回答
11

对于有条件的添加要求,我总是使用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(它只从那以后可用),我就开始转换我遇到的任何字典。

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

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

不过,我确实建议IRegisteredObject用这个实现接口,然后用HostingEnvironment.RegisterObject方法注册它——这样做会提供一种更简洁的方法来在应用程序池关闭/回收时关闭轮询线程。

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; 
  }
}
于 2012-05-11T22:23:15.197 回答
2

将最佳答案带入 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 });
于 2017-07-28T10:37:33.910 回答
2

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

如果你看一下 AddOrGetExisting() 的代码,你会发现 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
}
于 2019-07-05T19:31:47.240 回答
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;
}
于 2013-04-08T21:52:09.500 回答
1

这是作为 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
}
于 2017-08-03T16:25:34.967 回答