Как реализовать поточно-безопасный механизм кэширования при работе с коллекциями? - PullRequest
3 голосов
/ 26 мая 2011

Сценарий

  • У меня есть куча дочерних объектов, все связанные с данным Parent .
  • Я работаю над веб-приложением ASP.NET MVC 3 (например, многопоточным)
  • Одна из страниц - это страница «поиска», на которой мне нужно получить заданное подмножество детей и «сделать что-то» в памяти (вычисления, упорядочение, перечисление)
  • Вместо того, чтобы вводить каждого потомка в отдельный вызов, я делаю один вызов в базу данных, чтобы получить всех потомков для данного родителя, кэшировать результат и «делать вещи» в результате.
  • Проблема в том, что «do stuff» включает в себя операции LINQ (перечисление, добавление / удаление элементов из коллекции), которые при реализации с использованием List<T> не являются поточно-ориентированными.

Я читал о ConcurrentBag<T> и ConcurrentDictionary<T>, но не уверен, должен ли я использовать один из них или сам выполнить синхронизацию / блокировку.

Я нахожусь на .NET 4, поэтому я использую ObjectCache в качестве одноэлементного экземпляра MemoryCache.Default. У меня есть сервисный уровень, который работает с кешем, и сервисы принимают экземпляр ObjectCache, который выполняется через конструктор DI. Таким образом, все службы используют один и тот же экземпляр ObjectCache.

Основная проблема безопасности потока заключается в том, что мне нужно перебрать текущую «кэшированную» коллекцию, и если дочерний элемент, с которым я работаю, уже есть, мне нужно удалить его и добавить тот, с которым я работаю, это перечисление является причиной проблем.

Есть рекомендации?

Ответы [ 3 ]

8 голосов
/ 26 мая 2011

Да, для эффективной реализации кэша необходим механизм быстрого поиска, поэтому List<T> является неправильной структурой данных.Dictionary<TKey, TValue> - это идеальная структура данных для кэша, поскольку она позволяет заменить:

var value = instance.GetValueExpensive(key);

на:

var value = instance.GetValueCached(key);

, используя кэшированные значения в словаре и используясловарь, чтобы сделать тяжелую работу для поиска.Вызывающий не мудр.

Но, если вызывающие абоненты могут звонить из нескольких потоков, тогда .NET4 предоставляет ConcurrentDictionary<TKey, TValue>, который отлично работает в этой ситуации.Но что кэширует словарь?Похоже, в вашей ситуации ключом словаря является child , а значениями словаря являются результаты базы данных для этого потомка.

OK, так что теперь у нас есть поток-безопасный и эффективный кеш результатов базы данных по ключу ребенка.Какую структуру данных мы должны использовать для результатов базы данных?

Вы еще не сказали, как выглядят эти результаты, но, поскольку вы используете LINQ, мы знаем, что они по крайней мере IEnumerable<T> и, возможно, даже List<T>.Итак, мы вернулись к той же проблеме, верно?Поскольку List<T> не является потокобезопасным, мы не можем использовать его для значения словаря.Или мы можем?

Кэш должен быть доступен только для чтения с точки зрения вызывающей стороны.Вы говорите с LINQ, что вы «делаете вещи», такие как add / remove, но это не имеет смысла для кэшированного значения .Имеет смысл выполнять такие вещи в реализации самого кэша, как замена устаревшей записи новыми результатами.

Значение словаря, поскольку оно доступно только для чтения, может бытьList<T> с без побочных эффектов , даже если к нему будут обращаться из нескольких потоков.Вы можете использовать List<T>.AsReadOnly, чтобы повысить свою уверенность и добавить некоторую проверку безопасности во время компиляции.

Но важный момент заключается в том, что List<T> не является поточно-ориентированным, только если оно изменчиво.Поскольку по определению метод, реализованный с использованием кэша, должен возвращать одно и то же значение, если он вызывается несколько раз (до тех пор, пока значение не будет аннулировано самим кэшем), клиент не может изменить возвращенное значение, и поэтому List<T> должен быть заморожен, фактически неизменяемым.

Если клиенту крайне необходимо изменить результаты кэшированной базы данных, а значение - List<T>, то единственный безопасный способ сделать это:

  1. Сделать копию
  2. Внести изменения в копию
  3. Попросить кэш обновить значение

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

2 голосов
/ 27 мая 2011

Попробуйте изменить RefreshFooCache следующим образом:

    public ReadOnlyCollection<Foo> RefreshFooCache(Foo parentFoo)
       {
         ReadOnlyCollection<Foo> results;


        try {
// Prevent other writers but allow read operations to proceed
        Lock.EnterUpgradableReaderLock();

    // Recheck your cache: it may have already been updated by a previous thread before we gained exclusive lock
    if (_cache.Get(parentFoo.Id.ToString()) != null) {
      return parentFoo.FindFoosUnderneath(uniqueUri).AsReadOnly();
    }

        // Get the parent and everything below.
        var parentFooIncludingBelow = _repo.FindFoosIncludingBelow(parentFoo.UniqueUri).ToList();

// Now prevent both other writers and other readers
        Lock.EnterWriteLock();

        // Remove the cache
        _cache.Remove(parentFoo.Id.ToString());

        // Add the cache.
        _cache.Add(parentFoo.Id.ToString(), parentFooIncludingBelow );


       } finally {
    if (Lock.IsWriteLockHeld) {
            Lock.ExitWriteLock();
    }
        Lock.ExitUpgradableReaderLock();
       }
        results = parentFooIncludingBelow.AsReadOnly();
        return results;
       }
1 голос
/ 27 мая 2011

РЕДАКТИРОВАТЬ - обновлено для использования ReadOnlyCollection вместо ConcurrentDictionary

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

public class FooService
{
   private static ReaderWriterLockSlim _lock;
   private static readonly object SyncLock = new object();

   private static ReaderWriterLockSlim Lock
   {
      get
      {
         if (_lock == null)
         {
           lock(SyncLock)
           {
              if (_lock == null)
                 _lock = new ReaderWriterLockSlim();
           }
         } 

         return _lock;
      }
   }

   public ReadOnlyCollection<Foo> RefreshFooCache(Foo parentFoo)
   {
       // Get the parent and everything below.
       var parentFooIncludingBelow = repo.FindFoosIncludingBelow(parentFoo.UniqueUri).ToList().AsReadOnly();

       try
       {
          Lock.EnterWriteLock();

          // Remove the cache
         _cache.Remove(parentFoo.Id.ToString());

         // Add the cache.
         _cache.Add(parentFoo.Id.ToString(), parentFooIncludingBelow);
       }
       finally
       {
          Lock.ExitWriteLock();
       }

       return parentFooIncludingBelow;
   }

   public ReadOnlyCollection<Foo> FindFoo(string uniqueUri)
   {
      var parentIdForFoo = uniqueUri.GetParentId();
      ReadOnlyCollection<Foo> results;

      try
      {
         Lock.EnterReadLock();
         var cachedFoo = _cache.Get(parentIdForFoo);

         if (cachedFoo != null)
            results = cachedFoo.FindFoosUnderneath(uniqueUri).ToList().AsReadOnly();
      }
      finally
      {
         Lock.ExitReadLock();
      }

      if (results == null)
         results = RefreshFooCache(parentFoo).FindFoosUnderneath(uniqueUri).ToList().AsReadOnly();
      }

      return results;
   }
}

Резюме:

  • Каждый Контроллер (например, каждый HTTP-запрос) получает новый экземпляр FooService .
  • FooService имеет статическую блокировку, поэтому все запросы HTTPиспользовать один и тот же экземпляр.
  • Я использовал ReaderWriterLockSlim, чтобы гарантировать, что несколько потоков могут читать , но только один поток может писать .Я надеюсь, что это должно избежать "чтения" тупиков.Если двум потокам нужно «писать» одновременно, то, конечно, придется подождать.Но цель здесь состоит в том, чтобы быстро обслуживать операции чтения.
  • Цель состоит в том, чтобы иметь возможность найти «Foo» из кэша, пока что-то над ним было извлечено.

Какие-либо советы / предложения / проблемы с этим подходом?Я не уверен, что я блокирую области записи.Но поскольку мне нужно запустить LINQ для словаря, я решил, что мне нужна блокировка чтения.

...