Вопрос по c#, .net – Как бороться с дорогостоящими операциями построения с использованием MemoryCache?

56

В проекте ASP.NET MVC у нас есть несколько экземпляров данных, которые требуют значительного количества ресурсов и времени для сборки. Мы хотим их кешировать.

MemoryCache обеспечивает определенный уровень безопасности потоков, но этого недостаточно, чтобы избежать параллельного запуска нескольких экземпляров кода. Вот пример:

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

Как вы можете видеть на загруженном веб-сайте, сотни потоков могут идти внутри оператора if одновременно до тех пор, пока данные не будут собраны, и сделать процесс сборки еще медленнее, излишне потребляя ресурсы сервера.

Есть атомнаяAddOrGetExisting реализация в MemoryCache, но она неправильно требует & quot; значение для установки & quot; вместо & quot; кода для извлечения значения для установки & quot; который, я думаю, делает данный метод почти полностью бесполезным.

Мы использовали наши собственные специальные леса вокруг MemoryCache, чтобы сделать это правильно, однако это требует явногоlocks. Использовать объекты блокировки для каждой записи неудобно, и мы обычно избегаем совместного использования объектов блокировки, что далеко от идеала. Это заставило меня думать, что причины избегать такой конвенции могут быть преднамеренными.

Итак, у меня есть два вопроса:

  • Is it a better practice not to lock building code? (That could have been proven more responsive for one, I wonder)

  • What's the right way to achieve per-entry locking for MemoryCache for such a lock? The strong urge to use key string as the lock object is dismissed at ".NET locking 101".

Ваш Ответ

5   ответов
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 (он доступен только с этого момента), я начал преобразовывать любые из тех, с которыми сталкивался.

ConcurrentDictionaryпроизводительность восхитительна, если не сказать больше :)

Update Наивная реализация с семантикой истечения срока действия, основанной только на возрасте. Также следует убедиться, что отдельные элементы создаются только один раз - согласно предложению @ usr.Update again - как предложил @usr - просто используяLazy<T> было бы намного проще - вы можете просто переслать делегат создания этому при добавлении его в параллельный словарь. Я должен изменить код, так как фактически мой словарь блокировок не сработал бы в любом случае. Но я действительно должен был подумать об этом сам(past midnight here in the UK though and I'm beat. Any sympathy? No of course not. Being a developer, I have enough caffeine coursing through my veins to wake the dead).

Я рекомендую реализовать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; 
  }
}
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded
Error: User Rate Limit Exceeded Sedat Kapanoglu
Error: User Rate Limit Exceeded
Error: User Rate Limit ExceededObjectCacheError: User Rate Limit ExceededMemoryCacheError: User Rate Limit Exceeded
66

Мы решили эту проблему путем объединения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 выдает исключение. Однако одним из преимуществ является возможность кэширования нулевых значений.

если ключ не существуетnewValue.ValueError: User Rate Limit ExceededvalueError: User Rate Limit Exceededvalue.ValueError: User Rate Limit ExceededLazy<T>Error: User Rate Limit Exceeded.ValueError: User Rate Limit Exceeded Sedat Kapanoglu
Error: User Rate Limit Exceeded Sedat Kapanoglu
Error: User Rate Limit Exceededas Lazy<T>, В противном случае, хороший!
Error: User Rate Limit ExceededLazyThreadSafetyMode.PublicationOnlyError: User Rate Limit ExceededLazy<T>Error: User Rate Limit Exceeded
Красиво сделано. +1
1

вот моя реализация, которая позволяет хранить данные из любого типа источника.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;
}
Usage
/// <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 });
0

 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
}
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;
}
Error: User Rate Limit Exceeded Sedat Kapanoglu

Похожие вопросы