Подтвердить что ты не робот

Как бороться с дорогостоящими операциями с использованием MemoryCache?

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

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

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

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

В MemoryCache существует атомная реализация AddOrGetExisting, но для нее неверно требуется "значение для установки" вместо "кода для извлечения значения для установки", которое, как я думаю, делает данный метод почти бесполезным.

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

У меня есть два вопроса:

  • Лучше ли использовать код lock для построения? (Возможно, это было более восприимчивым к одному, интересно)

  • Каков правильный способ блокировки блокировки для MemoryCache для такой блокировки? Сильное желание использовать строку key в качестве объекта блокировки отклоняется в ".NET locking 101".

4b9b3361

Ответ 1

Мы решили эту проблему, объединив 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
}

Это не полно. Есть gotchas вроде "кэширования исключений", поэтому вам нужно решить, что вы хотите сделать, если ваш valueFactory выдает исключение. Однако одним из преимуществ является возможность кэширования нулевых значений.

Ответ 2

Для условного требования добавления я всегда использую 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 производительность замечательна, если не сказать больше:)

Обновить Наивная реализация с семантикой истечения срока действия только по возрасту. Также следует убедиться, что отдельные элементы создаются только один раз - согласно предложению @usr. Обновить снова - как предположил @usr - просто с помощью Lazy<T> было бы намного проще - вы можете просто переслать делегат создания на него при добавлении его в параллельный словарь. Я изменил код, так как на самом деле мой словарь замков не сработал бы. Но я действительно должен был подумать об этом сам (в полночь здесь, в Великобритании, хотя и я избил. Любая симпатия? Нет, конечно, нет. Будучи разработчиком, у меня достаточно кофеина, проходящего по моим венам, чтобы разбудить мертвых).

Я рекомендую использовать этот IRegisteredObject интерфейс, а затем зарегистрировать его с помощью HostingEnvironment.RegisterObject - это обеспечит более чистый способ отключения потока poller, когда пул приложений будет отключен/перезаписан.

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; 
  }
}

Ответ 3

Вот проект, который следует за тем, что вы, похоже, имеете в виду. Первый замок происходит только на короткое время. Окончательный вызов 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;
}

Ответ 4

Взяв верхний ответ на С# 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 });

Ответ 5

Вот простое решение как метод расширения 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
}

Ответ 6

Решение 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
}